Merge mc -> pine draft
authorGregor Wagner <anygregor@gmail.com>
Sun, 14 Sep 2014 10:59:54 +0200
changeset 388169 26bb7109f7ee9646d04af7a327839c59e4eb9a27
parent 388168 55b33f2771e4dadd64a131c6f146cddbbff6e80f (current diff)
parent 205188 d070787de8f7b56e32bf5800b30f143c4d02d474 (diff)
child 388170 ca4963893fc5fa740f772da6501c07475e51e3e6
push id23132
push userbmo:lissyx+mozillians@lissyx.dyndns.org
push dateFri, 15 Jul 2016 10:07:12 +0000
milestone35.0a1
Merge mc -> pine
addon-sdk/source/modules/system/moz.build
b2g/app/b2g.js
b2g/config/gaia.json
configure.in
gfx/layers/ImageContainer.cpp
js/src/jsgc.cpp
js/src/vm/Runtime.h
mobile/android/base/resources/color/new_tablet_tab_strip_item_title.xml
mobile/android/base/resources/drawable-hdpi/new_tablet_tabs_count.png
mobile/android/base/resources/drawable-hdpi/new_tablet_tabs_count_foreground.png
mobile/android/base/resources/drawable-mdpi/new_tablet_tabs_count.png
mobile/android/base/resources/drawable-mdpi/new_tablet_tabs_count_foreground.png
mobile/android/base/resources/drawable-xhdpi/new_tablet_tabs_count.png
mobile/android/base/resources/drawable-xhdpi/new_tablet_tabs_count_foreground.png
mobile/android/base/resources/drawable/new_tablet_tab_strip_divider.xml
mobile/android/base/resources/drawable/new_tablet_tab_strip_item_bg.xml
mobile/android/base/resources/layout/new_tablet_tab_strip.xml
mobile/android/base/resources/layout/new_tablet_tabs_counter.xml
testing/web-platform/meta/XMLHttpRequest/data-uri-basic.htm.ini
testing/web-platform/meta/XMLHttpRequest/xmlhttprequest-timeout-overridesexpires.html.ini
testing/web-platform/meta/dom/nodes/Document-createElement-namespace.html.ini
testing/web-platform/meta/html/infrastructure/urls/dynamic-changes-to-base-urls/dynamic-urls.sub.xhtml.ini
testing/web-platform/meta/webmessaging/without-ports/020.html.ini
testing/web-platform/meta/websockets/interfaces/CloseEvent/002.html.ini
testing/web-platform/meta/websockets/interfaces/CloseEvent/003.html.ini
testing/web-platform/meta/websockets/interfaces/WebSocket/bufferedAmount/004.html.ini
testing/web-platform/meta/websockets/interfaces/WebSocket/close/006.html.ini
testing/web-platform/tests/XMLHttpRequest/data-uri-basic.htm
testing/web-platform/tests/html/editing/dnd/resources/crossorigin.js
testing/web-platform/tests/html/editing/dnd/target-origin/003.html
testing/web-platform/tests/html/editing/dnd/target-origin/004.html
testing/web-platform/tests/html/editing/dnd/target-origin/005.html
testing/web-platform/tests/html/editing/dnd/target-origin/006.html
testing/web-platform/tests/html/editing/dnd/target-origin/007.html
testing/web-platform/tests/html/editing/dnd/target-origin/008.html
testing/web-platform/tests/html/editing/dnd/target-origin/009.html
testing/web-platform/tests/html/editing/dnd/target-origin/010.html
testing/web-platform/tests/html/editing/dnd/target-origin/011.html
testing/web-platform/tests/html/editing/dnd/target-origin/012.html
testing/web-platform/tests/html/editing/dnd/target-origin/013.html
testing/web-platform/tests/html/editing/dnd/target-origin/102.html
testing/web-platform/tests/html/editing/dnd/target-origin/103.html
testing/web-platform/tests/html/editing/dnd/target-origin/104.html
testing/web-platform/tests/html/editing/dnd/target-origin/105.html
testing/web-platform/tests/html/editing/dnd/target-origin/106.html
testing/web-platform/tests/html/editing/dnd/target-origin/107.html
testing/web-platform/tests/html/editing/dnd/target-origin/108.html
testing/web-platform/tests/html/editing/dnd/target-origin/109.html
testing/web-platform/tests/html/editing/dnd/target-origin/110.html
testing/web-platform/tests/html/editing/dnd/target-origin/111.html
testing/web-platform/tests/html/editing/dnd/target-origin/112.html
testing/web-platform/tests/html/editing/dnd/target-origin/113.html
testing/web-platform/tests/html/editing/dnd/target-origin/114.html
testing/web-platform/tests/html/editing/dnd/target-origin/115.html
testing/web-platform/tests/html/editing/dnd/target-origin/116.html
testing/web-platform/tests/html/editing/dnd/target-origin/117.html
testing/web-platform/tests/html/editing/dnd/target-origin/118.html
testing/web-platform/tests/html/editing/dnd/target-origin/201.html
testing/web-platform/tests/vibration/TODO.txt
testing/web-platform/tests/websockets/interfaces/CloseEvent/001.html
testing/web-platform/tests/websockets/interfaces/CloseEvent/002.html
testing/web-platform/tests/websockets/interfaces/CloseEvent/003.html
testing/web-platform/tests/websockets/interfaces/CloseEvent/004.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/001.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/002.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/003.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/004.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/005.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/006.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/007.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/008.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/009.html
testing/web-platform/tests/websockets/interfaces/WebSocket/bufferedAmount/010.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/001.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/002.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/003.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/004.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/005.html
testing/web-platform/tests/websockets/interfaces/WebSocket/close/006.html
--- a/accessible/tests/mochitest/relations/test_embeds.xul
+++ b/accessible/tests/mochitest/relations/test_embeds.xul
@@ -53,22 +53,21 @@
     }
 
     function browserReorderChecker()
     {
       this.type = EVENT_REORDER;
 
       this.match = function browserReorderChecker_match(aEvent)
       {
+        if (!isAccessible(currentBrowser()))
+          return false;
+
         // Reorder event might be duped because of temporary document creation.
-        var browserAcc = getAccessible(currentBrowser());
-        if (!browserAcc)
-          ok(false, "opa opa sralslasya");
-
-        if (aEvent.accessible == browserAcc) {
+        if (aEvent.accessible == getAccessible(currentBrowser())) {
           this.cnt++;
           return this.cnt != 2;
         }
 
         return false;
       }
 
       this.cnt = 0;
@@ -82,49 +81,46 @@
       }
 
       this.eventSeq = [
         new browserReorderChecker()
       ];
 
       this.finalCheck = function loadURI_finalCheck()
       {
-        var acc = getAccessible(currentTabDocument());
-        if (!acc)
-          ok(false, "ahahahaha");
-
-        testRelation(browserDocument(), RELATION_EMBEDS, acc);
+        testRelation(browserDocument(), RELATION_EMBEDS,
+                     getAccessible(currentTabDocument()));
       }
 
       this.getID = function loadOneTab_getID()
       {
         return "load uri '" + aURI + "' in new tab";
       }
     }
 
     ////////////////////////////////////////////////////////////////////////////
     // Testing
 
-    gA11yEventDumpToConsole = true; // debug
+    //gA11yEventDumpToConsole = true; // debug
 
     var gQueue = null;
     function doTests()
     {
       testRelation(browserDocument(), RELATION_EMBEDS,
                    getAccessible(currentTabDocument()));
 
-      enableLogging("docload");
+      //enableLogging("docload");
       gQueue = new eventQueue();
 
       gQueue.push(new loadURI("about:about"));
       gQueue.push(new loadOneTab("about:mozilla"));
 
       gQueue.onFinish = function()
       {
-        disableLogging();
+        //disableLogging();
         closeBrowserWindow();
       }
       gQueue.invoke();
     }
 
     SimpleTest.waitForExplicitFinish();
     openBrowserWindow(doTests, "about:");
   ]]>
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -6,22 +6,25 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
-DIRS += ["source/modules/system"]
-
 EXTRA_JS_MODULES.sdk += [
     'source/app-extension/bootstrap.js',
 ]
 
+EXTRA_JS_MODULES.sdk.system += [
+    'source/modules/system/Startup.js',
+    'source/modules/system/XulApp.js',
+]
+
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
     EXTRA_JS_MODULES.commonjs.method.test += [
         'source/lib/method/test/browser.js',
         'source/lib/method/test/common.js',
     ]
 
     EXTRA_JS_MODULES.commonjs.sdk.deprecated += [
         'source/lib/sdk/deprecated/api-utils.js',
--- a/addon-sdk/source/lib/sdk/loader/cuddlefish.js
+++ b/addon-sdk/source/lib/sdk/loader/cuddlefish.js
@@ -9,94 +9,44 @@ module.metadata = {
 
 // This module is manually loaded by bootstrap.js in a sandbox and immediatly
 // put in module cache so that it is never loaded in any other way.
 
 /* Workarounds to include dependencies in the manifest
 require('chrome')                  // Otherwise CFX will complain about Components
 require('toolkit/loader')          // Otherwise CFX will stip out loader.js
 require('sdk/addon/runner')        // Otherwise CFX will stip out addon/runner.js
-require('sdk/system/xul-app')      // Otherwise CFX will stip out sdk/system/xul-app
 */
 
 const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
 
+const {
+  incompatibility
+} = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}).XulApp;
+
 // `loadSandbox` is exposed by bootstrap.js
 const loaderURI = module.uri.replace("sdk/loader/cuddlefish.js",
                                      "toolkit/loader.js");
-const xulappURI = module.uri.replace("loader/cuddlefish.js",
-                                     "system/xul-app.js");
 // We need to keep a reference to the sandbox in order to unload it in
 // bootstrap.js
 
 const loaderSandbox = loadSandbox(loaderURI);
 const loaderModule = loaderSandbox.exports;
 
-const xulappSandbox = loadSandbox(xulappURI);
-const xulappModule = xulappSandbox.exports;
-
 const { override, load } = loaderModule;
 
-/**
- * Ensure the current application satisfied the requirements specified in the
- * module given. If not, an exception related to the incompatibility is
- * returned; `null` otherwise.
- *
- * @param {Object} module
- *  The module to check
- * @returns {Error}
- */
-function incompatibility(module) {
-  let { metadata, id } = module;
-
-  // if metadata or engines are not specified we assume compatibility is not
-  // an issue.
-  if (!metadata || !("engines" in metadata))
-    return null;
-
-  let { engines } = metadata;
-
-  if (engines === null || typeof(engines) !== "object")
-    return new Error("Malformed engines' property in metadata");
-
-  let applications = Object.keys(engines);
-
-  let versionRange;
-  applications.forEach(function(name) {
-    if (xulappModule.is(name)) {
-      versionRange = engines[name];
-      // Continue iteration. We want to ensure the module doesn't
-      // contain a typo in the applications' name or some unknown
-      // application - `is` function throws an exception in that case.
-    }
-  });
-
-  if (typeof(versionRange) === "string") {
-    if (xulappModule.satisfiesVersion(versionRange))
-      return null;
-
-    return new Error("Unsupported Application version: The module " + id +
-            " currently supports only version " + versionRange + " of " +
-            xulappModule.name + ".");
-  }
-
-  return new Error("Unsupported Application: The module " + id +
-            " currently supports only " + applications.join(", ") + ".")
-}
-
 function CuddlefishLoader(options) {
   let { manifest } = options;
 
   options = override(options, {
     // Put `api-utils/loader` and `api-utils/cuddlefish` loaded as JSM to module
     // cache to avoid subsequent loads via `require`.
     modules: override({
       'toolkit/loader': loaderModule,
-      'sdk/loader/cuddlefish': exports,
-      'sdk/system/xul-app': xulappModule
+      'sdk/loader/cuddlefish': exports
     }, options.modules),
     resolve: function resolve(id, requirer) {
       let entry = requirer && requirer in manifest && manifest[requirer];
       let uri = null;
 
       // If manifest entry for this requirement is present we follow manifest.
       // Note: Standard library modules like 'panel' will be present in
       // manifest unless they were moved to platform.
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -37,16 +37,19 @@ const systemPrincipal = CC('@mozilla.org
 const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1'].
                      getService(Ci.mozIJSSubScriptLoader);
 const { notifyObservers } = Cc['@mozilla.org/observer-service;1'].
                         getService(Ci.nsIObserverService);
 const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {});
 const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
 const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm");
+const {
+  incompatibility
+} = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}).XulApp;
 
 // Define some shortcuts.
 const bind = Function.call.bind(Function.bind);
 const getOwnPropertyNames = Object.getOwnPropertyNames;
 const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
 const define = Object.defineProperties;
 const prototypeOf = Object.getPrototypeOf;
 const create = Object.create;
@@ -344,16 +347,22 @@ const load = iced(function load(loader, 
       message: { value: message, writable: true, configurable: true },
       fileName: { value: fileName, writable: true, configurable: true },
       lineNumber: { value: lineNumber, writable: true, configurable: true },
       stack: { value: serializeStack(frames), writable: true, configurable: true },
       toString: { value: function() toString, writable: true, configurable: true },
     });
   }
 
+  let (error = incompatibility(module)) {
+    if (error) {
+      throw error;
+    }
+  }
+
   if (module.exports && typeof(module.exports) === 'object')
     freeze(module.exports);
 
   return module;
 });
 exports.load = load;
 
 // Utility function to normalize module `uri`s so they have `.js` extension.
--- a/addon-sdk/source/modules/system/Startup.js
+++ b/addon-sdk/source/modules/system/Startup.js
@@ -1,34 +1,34 @@
 /* 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/. */
 "use strict";
 
-var EXPORTED_SYMBOLS = ["Startup"];
+this.EXPORTED_SYMBOLS = ["Startup"];
 
 const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
 const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 const { XulApp } = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {});
 const { defer } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
 const appStartupSrv = Cc["@mozilla.org/toolkit/app-startup;1"]
                        .getService(Ci.nsIAppStartup);
 
 const NAME2TOPIC = {
   'Firefox': 'sessionstore-windows-restored',
   'Fennec': 'sessionstore-windows-restored',
   'SeaMonkey': 'sessionstore-windows-restored',
   'Thunderbird': 'mail-startup-done'
 };
 
-var Startup = {
+var exports = {
   initialized: !appStartupSrv.startingUp
 };
-var exports = Startup;
+this.Startup = exports;
 
 let gOnceInitializedDeferred = defer();
 exports.onceInitialized = gOnceInitializedDeferred.promise;
 
 // Set 'final-ui-startup' as default topic for unknown applications
 let appStartup = 'final-ui-startup';
 
 if (Startup.initialized) {
--- a/addon-sdk/source/modules/system/XulApp.js
+++ b/addon-sdk/source/modules/system/XulApp.js
@@ -1,22 +1,23 @@
 /* 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/. */
 "use strict";
 
-var EXPORTED_SYMBOLS = ["XulApp"];
+this.EXPORTED_SYMBOLS = ["XulApp"];
 
 var { classes: Cc, interfaces: Ci } = Components;
 
 var exports = {};
-var XulApp = exports;
+this.XulApp = exports;
 
-var appInfo = Cc["@mozilla.org/xre/app-info;1"]
-              .getService(Ci.nsIXULAppInfo);
+var appInfo = Cc["@mozilla.org/xre/app-info;1"].
+              getService(Ci.nsIXULAppInfo);
+
 var vc = Cc["@mozilla.org/xpcom/version-comparator;1"]
          .getService(Ci.nsIVersionComparator);
 
 var ID = exports.ID = appInfo.ID;
 var name = exports.name = appInfo.name;
 var version = exports.version = appInfo.version;
 var platformVersion = exports.platformVersion = appInfo.platformVersion;
 
@@ -178,8 +179,56 @@ function satisfiesVersion(version, versi
     let [, lowMod, lowVer, highMod, highVer] = matches;
 
     return compareVersion(version, lowMod, lowVer) && (highVer !== undefined
       ? compareVersion(version, highMod, highVer)
       : true);
   });
 }
 exports.satisfiesVersion = satisfiesVersion;
+
+/**
+ * Ensure the current application satisfied the requirements specified in the
+ * module given. If not, an exception related to the incompatibility is
+ * returned; `null` otherwise.
+ *
+ * @param {Object} module
+ *  The module to check
+ * @returns {Error}
+ */
+function incompatibility(module) {
+  let { metadata, id } = module;
+
+  // if metadata or engines are not specified we assume compatibility is not
+  // an issue.
+  if (!metadata || !("engines" in metadata))
+    return null;
+
+  let { engines } = metadata;
+
+  if (engines === null || typeof(engines) !== "object")
+    return new Error("Malformed engines' property in metadata");
+
+  let applications = Object.keys(engines);
+
+  let versionRange;
+  applications.forEach(function(name) {
+    if (is(name)) {
+      versionRange = engines[name];
+      // Continue iteration. We want to ensure the module doesn't
+      // contain a typo in the applications' name or some unknown
+      // application - `is` function throws an exception in that case.
+    }
+  });
+
+  if (typeof(versionRange) === "string") {
+    if (satisfiesVersion(versionRange))
+      return null;
+
+    return new Error("Unsupported Application version: The module " + id +
+            " currently supports only version " + versionRange + " of " +
+            name + ".");
+  }
+
+  return new Error("Unsupported Application: The module " + id +
+            " currently supports only " + applications.join(", ") + ".")
+}
+exports.incompatibility = incompatibility;
deleted file mode 100644
--- a/addon-sdk/source/modules/system/moz.build
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-EXTRA_JS_MODULES.sdk.system += [
-    'Startup.js',
-    'XulApp.js',
-]
--- a/addon-sdk/source/test/test-cuddlefish.js
+++ b/addon-sdk/source/test/test-cuddlefish.js
@@ -1,15 +1,15 @@
 /* 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/. */
-
 'use strict';
 
 const { Loader, Require, unload, override } = require('sdk/loader/cuddlefish');
+const app = require('sdk/system/xul-app');
 const packaging = require('@loader/options');
 
 exports['test loader'] = function(assert) {
   var prints = [];
   function print(message) {
     prints.push(message);
   }
 
@@ -39,9 +39,24 @@ exports['test loader'] = function(assert
   });
 
   unload(loader, 'test');
 
   assert.equal(unloadsCalled, 'ba',
                'loader.unload() must call listeners in LIFO order.');
 };
 
-require('test').run(exports);
+exports['test loader on unsupported modules'] = function(assert) {
+  let loader = Loader({});
+  let err = "";
+  assert.throws(() => {
+    if (!app.is('Firefox')) {
+      require('./fixtures/loader/unsupported/firefox');
+    }
+    else {
+      require('./fixtures/loader/unsupported/fennec');
+    }
+  }, /^Unsupported Application/, "throws Unsupported Application");
+
+  unload(loader);
+};
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-loader.js
+++ b/addon-sdk/source/test/test-loader.js
@@ -1,24 +1,24 @@
 /* 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/. */
-
 'use strict';
 
 let {
   Loader, main, unload, parseStack, generateMap, resolve, join
 } = require('toolkit/loader');
 let { readURI } = require('sdk/net/url');
 
 let root = module.uri.substr(0, module.uri.lastIndexOf('/'))
 
-
 // The following adds Debugger constructor to the global namespace.
 const { Cu } = require('chrome');
+const app = require('sdk/system/xul-app');
+
 const { addDebuggerToGlobal } = Cu.import('resource://gre/modules/jsdebugger.jsm', {});
 addDebuggerToGlobal(this);
 
 exports['test resolve'] = function (assert) {
   let cuddlefish_id = 'sdk/loader/cuddlefish';
   assert.equal(resolve('../index.js', './dir/c.js'), './index.js');
   assert.equal(resolve('./index.js', './dir/c.js'), './dir/index.js');
   assert.equal(resolve('./dir/c.js', './index.js'), './dir/c.js');
@@ -326,17 +326,17 @@ exports['test invisibleToDebugger: true'
     assert.ok(true, 'debugger did not add invisible value');
   }
 };
 
 exports['test console global by default'] = function (assert) {
   let uri = root + '/fixtures/loader/globals/';
   let loader = Loader({ paths: { '': uri }});
   let program = main(loader, 'main');
- 
+
   assert.ok(typeof program.console === 'object', 'global `console` exists');
   assert.ok(typeof program.console.log === 'function', 'global `console.log` exists');
 
   let loader2 = Loader({ paths: { '': uri }, globals: { console: fakeConsole }});
   let program2 = main(loader2, 'main');
 
   assert.equal(program2.console, fakeConsole,
     'global console can be overridden with Loader options');
@@ -369,9 +369,24 @@ exports['test shared globals'] = functio
 exports["test require#resolve"] = function(assert) {
   let root = require.resolve("sdk/tabs").replace(/commonjs\.path\/(.*)$/, "") + "commonjs.path/";
   assert.ok(/^resource:\/\/extensions\.modules\.[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}-at-jetpack\.commonjs\.path\/$/.test(root), "correct resolution root");
 
   assert.equal(root + "sdk/tabs.js", require.resolve("sdk/tabs"), "correct resolution of sdk module");
   assert.equal(root + "toolkit/loader.js", require.resolve("toolkit/loader"), "correct resolution of sdk module");
 };
 
-require('test').run(exports);
+exports['test loader on unsupported modules'] = function(assert) {
+  let loader = Loader({});
+  let err = "";
+  assert.throws(() => {
+    if (!app.is('Firefox')) {
+      require('./fixtures/loader/unsupported/firefox');
+    }
+    else {
+      require('./fixtures/loader/unsupported/fennec');
+    }
+  }, /^Unsupported Application/, "throws Unsupported Application");
+
+  unload(loader);
+};
+
+require('sdk/test').run(exports);
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -361,16 +361,19 @@ pref("browser.safebrowsing.provider.0.re
 
 // Name of the about: page contributed by safebrowsing to handle display of error
 // pages on phishing/malware hits.  (bug 399233)
 pref("urlclassifier.alternate_error_page", "blocked");
 
 // The number of random entries to send with a gethash request.
 pref("urlclassifier.gethashnoise", 4);
 
+// Gethash timeout for Safebrowsing.
+pref("urlclassifier.gethash.timeout_ms", 5000);
+
 // If an urlclassifier table has not been updated in this number of seconds,
 // a gethash request will be forced to check that the result is still in
 // the database.
 pref("urlclassifier.max-complete-age", 2700);
 
 // URL for checking the reason for a malware warning.
 pref("browser.safebrowsing.malware.reportURL", "https://safebrowsing.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
 #endif
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -320,16 +320,19 @@ setUpdateTrackingId();
   };
 
 })();
 
 // ================ Accessibility ============
 (function setupAccessibility() {
   let accessibilityScope = {};
   SettingsListener.observe("accessibility.screenreader", false, function(value) {
+    if (!value) {
+      return;
+    }
     if (!('AccessFu' in accessibilityScope)) {
       Cu.import('resource://gre/modules/accessibility/AccessFu.jsm',
                 accessibilityScope);
       accessibilityScope.AccessFu.attach(window);
     }
   });
 })();
 
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/>
   <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/>
@@ -122,22 +122,22 @@
   <project name="platform/system/security" path="system/security" revision="ee8068b9e7bfb2770635062fc9c2035be2142bd8"/>
   <project name="platform/system/vold" path="system/vold" revision="2e43efe1b30d0b98574d293059556aebd2f46454"/>
   <!--original fetch url was http://sprdsource.spreadtrum.com:8085/b2g/android-->
   <remote fetch="https://git.mozilla.org/external/sprd-aosp" name="sprd-aosp"/>
   <default remote="sprd-aosp" revision="sprdb2g_gonk4.4" sync-j="4"/>
   <!-- Stock Android things -->
   <project name="platform/external/icu4c" path="external/icu4c" revision="2bb01561780583cc37bc667f0ea79f48a122d8a2"/>
   <!-- dolphin specific things -->
-  <project name="device/sprd" path="device/sprd" revision="ebb1ce6af72efe15c6919e2ceb9ee805ce2e5960"/>
+  <project name="device/sprd" path="device/sprd" revision="0351ccd65808a2486e0fefb99674ca7a64c2c6dc"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="4e58336019b5cbcfd134caf55b142236cf986618"/>
   <project name="platform/frameworks/av" path="frameworks/av" revision="facca8d3e35431b66f85a4eb42bc6c5b24bd04da"/>
   <project name="platform/hardware/akm" path="hardware/akm" revision="6d3be412647b0eab0adff8a2768736cf4eb68039"/>
   <project groups="invensense" name="platform/hardware/invensense" path="hardware/invensense" revision="e6d9ab28b4f4e7684f6c07874ee819c9ea0002a2"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="865ce3b4a2ba0b3a31421ca671f4d6c5595f8690"/>
   <project name="kernel/common" path="kernel" revision="28aab3bd1139b6beea545f50dee8903c0634de84"/>
   <project name="platform/system/core" path="system/core" revision="53d584d4a4b4316e4de9ee5f210d662f89b44e7e"/>
   <project name="u-boot" path="u-boot" revision="2d7a801a3e002078f885e8085fad374a564682e5"/>
   <project name="vendor/sprd/gps" path="vendor/sprd/gps" revision="7feb3df0e150053e0143ef525f6e082bda320aea"/>
-  <project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="cbc0a8e207a21bfaa96e07971ac1f380d9a677cf"/>
+  <project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="69c8c336794666b010e34b2f501d89118513c546"/>
   <project name="vendor/sprd/partner" path="vendor/sprd/partner" revision="8649c7145972251af11b0639997edfecabfc7c2e"/>
   <project name="vendor/sprd/proprietories" path="vendor/sprd/proprietories" revision="d2466593022f7078aaaf69026adf3367c2adb7bb"/>
 </manifest>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
@@ -118,17 +118,17 @@
   <project name="platform/system/vold" path="system/vold" revision="153df4d067a4149c7d78f1c92fed2ce2bd6a272e"/>
   <!--original fetch url was git://github.com/t2m-foxfone/-->
   <remote fetch="https://git.mozilla.org/external/t2m-foxfone" name="t2m"/>
   <default remote="caf" revision="jb_3.2" sync-j="4"/>
   <!-- Flame specific things -->
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="e8a318f7690092e639ba88891606f4183e846d3f"/>
   <project name="device/qcom/common" path="device/qcom/common" revision="878804e0becfe5635bb8ccbf2671333d546c6fb6"/>
   <project name="device-flame" path="device/t2m/flame" remote="b2g" revision="55ba09d8edffe7daffd954986b913319fd97890f"/>
-  <project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="ebb14165369f5edc3f335d5bde6eef8439073589"/>
+  <project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="49417cfc622074daa3c76b345a199f6731375800"/>
   <project name="kernel_lk" path="bootable/bootloader/lk" remote="b2g" revision="9eb619d2efdf4bd121587d8296f5c10481f750b8"/>
   <project name="platform_bootable_recovery" path="bootable/recovery" remote="b2g" revision="e81502511cda303c803e63f049574634bc96f9f2"/>
   <project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="81c4a859d75d413ad688067829d21b7ba9205f81"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="f0689ac1914cdbc59e53bdc9edd9013dc157c299"/>
   <project name="platform/external/bluetooth/glib" path="external/bluetooth/glib" revision="dd925f76e4f149c3d5571b80e12f7e24bbe89c59"/>
   <project name="platform/external/dbus" path="external/dbus" revision="ea87119c843116340f5df1d94eaf8275e1055ae8"/>
   <project name="platform_external_libnfc-nci" path="external/libnfc-nci" remote="t2m" revision="4186bdecb4dae911b39a8202252cc2310d91b0be"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="320b05a5761eb2a4816f7529c91ea49422979b55"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "http://github.com/qdot/gaia", 
         "branch": "bug_1060642"
     }, 
-    "revision": "51bb0dde2b9800784dc6b4688eb8108aa18de765", 
+    "revision": "90c5e3b6bc763bd6a40aa5671801ff6852ad951d", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="4d1e85908d792d9468c4da7040acd191fbb51b40">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
--- a/b2g/dev/app/moz.build
+++ b/b2g/dev/app/moz.build
@@ -1,3 +1,6 @@
 # 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/.
+
+DIST_SUBDIR = 'browser'
+export('DIST_SUBDIR')
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -646,17 +646,17 @@
 @BINPATH@/icons/*.xpm
 @BINPATH@/icons/*.png
 #endif
 #endif
 
 ; [Default Preferences]
 ; All the pref files must be part of base to prevent migration bugs
 #ifdef MOZ_MULET
-@BINPATH@/defaults/pref/b2g.js
+@BINPATH@/browser/@PREF_DIR@/b2g.js
 #else
 @BINPATH@/@PREF_DIR@/b2g.js
 #endif
 @BINPATH@/@PREF_DIR@/channel-prefs.js
 @BINPATH@/greprefs.js
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1000,16 +1000,19 @@ pref("browser.safebrowsing.id", "navclie
 
 // Name of the about: page contributed by safebrowsing to handle display of error
 // pages on phishing/malware hits.  (bug 399233)
 pref("urlclassifier.alternate_error_page", "blocked");
 
 // The number of random entries to send with a gethash request.
 pref("urlclassifier.gethashnoise", 4);
 
+// Gethash timeout for Safebrowsing.
+pref("urlclassifier.gethash.timeout_ms", 5000);
+
 // If an urlclassifier table has not been updated in this number of seconds,
 // a gethash request will be forced to check that the result is still in
 // the database.
 pref("urlclassifier.max-complete-age", 2700);
 // Tables for application reputation.
 pref("urlclassifier.downloadBlockTable", "goog-badbinurl-shavar");
 #ifdef XP_WIN
 // Only download the whitelist on Windows, since the whitelist is
@@ -1474,20 +1477,16 @@ pref("devtools.browserconsole.filter.war
 pref("devtools.browserconsole.filter.info", true);
 pref("devtools.browserconsole.filter.log", true);
 pref("devtools.browserconsole.filter.secerror", true);
 pref("devtools.browserconsole.filter.secwarn", true);
 
 // Text size in the Web Console. Use 0 for the system default size.
 pref("devtools.webconsole.fontSize", 0);
 
-// Number of usages of the web console or scratchpad.
-// If this is less than 5, then pasting code into the web console or scratchpad is disabled
-pref("devtools.selfxss.count", 0);
-
 // Persistent logging: |true| if you want the Web Console to keep all of the
 // logged messages after reloading the page, |false| if you want the output to
 // be cleared each time page navigation happens.
 pref("devtools.webconsole.persistlog", false);
 
 // Web Console timestamp: |true| if you want the logs and instructions
 // in the Web Console to display a timestamp, or |false| to not display
 // any timestamps.
@@ -1608,16 +1607,17 @@ pref("loop.legal.ToS_url", "https://acco
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.websocket", false);
+pref("loop.debug.sdk", false);
 
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
 pref("dom.identity.enabled", false);
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1092,16 +1092,18 @@ var gBrowserInit = {
     PanelUI.init();
     LightweightThemeListener.init();
 
 #ifdef MOZ_CRASHREPORTER
     if (gMultiProcessBrowser)
       TabCrashReporter.init();
 #endif
 
+    Services.telemetry.getHistogramById("E10S_WINDOW").add(gMultiProcessBrowser);
+
     if (mustLoadSidebar) {
       let sidebar = document.getElementById("sidebar");
       let sidebarBox = document.getElementById("sidebar-box");
       sidebar.setAttribute("src", sidebarBox.getAttribute("src"));
     }
 
     UpdateUrlbarSearchSplitterState();
 
@@ -4127,17 +4129,17 @@ var TabsProgressListener = {
   }
 }
 
 function nsBrowserAccess() { }
 
 nsBrowserAccess.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
 
-  _openURIInNewTab: function(aURI, aOpener, aIsExternal) {
+  _openURIInNewTab: function(aURI, aOpener, aIsExternal, aEnsureNonRemote=false) {
     let win, needToFocusWin;
 
     // try the current window.  if we're in a popup, fall back on the most recent browser window
     if (window.toolbar.visible)
       win = window;
     else {
       let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aOpener || window);
       win = RecentWindow.getMostRecentBrowserWindow({private: isPrivate});
@@ -4159,23 +4161,40 @@ nsBrowserAccess.prototype = {
     let referrer = aOpener ? makeURI(aOpener.location.href) : null;
 
     let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", {
                                       referrerURI: referrer,
                                       fromExternal: aIsExternal,
                                       inBackground: loadInBackground});
     let browser = win.gBrowser.getBrowserForTab(tab);
 
+    // It's possible that we've been asked to open a new non-remote
+    // browser in a window that defaults to having remote browsers -
+    // this can happen if we're opening the new tab due to a window.open
+    // or _blank anchor in a non-remote browser. If so, we have to force
+    // the newly opened browser to also not be remote.
+    if (win.gMultiProcessBrowser && aEnsureNonRemote) {
+      win.gBrowser.updateBrowserRemoteness(browser, false);
+    }
+
     if (needToFocusWin || (!loadInBackground && aIsExternal))
       win.focus();
 
     return browser;
   },
 
   openURI: function (aURI, aOpener, aWhere, aContext) {
+    // This function should only ever be called if we're opening a URI
+    // from a non-remote browser window (via nsContentTreeOwner).
+    if (aOpener && Cu.isCrossProcessWrapper(aOpener)) {
+      Cu.reportError("nsBrowserAccess.openURI was passed a CPOW for aOpener. " +
+                     "openURI should only ever be called from non-remote browsers.");
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
     var newWindow = null;
     var isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
 
     if (isExternal && aURI && aURI.schemeIs("chrome")) {
       dump("use -chrome command-line option to load external chrome urls\n");
       return null;
     }
 
@@ -4191,17 +4210,17 @@ nsBrowserAccess.prototype = {
         // FIXME: Bug 408379. So how come this doesn't send the
         // referrer like the other loads do?
         var url = aURI ? aURI.spec : "about:blank";
         // Pass all params to openDialog to ensure that "url" isn't passed through
         // loadOneOrMoreURIs, which splits based on "|"
         newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null);
         break;
       case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB :
-        let browser = this._openURIInNewTab(aURI, aOpener, isExternal);
+        let browser = this._openURIInNewTab(aURI, aOpener, isExternal, true);
         if (browser)
           newWindow = browser.contentWindow;
         break;
       default : // OPEN_CURRENTWINDOW or an illegal value
         newWindow = content;
         if (aURI) {
           let referrer = aOpener ? makeURI(aOpener.location.href) : null;
           let loadflags = isExternal ?
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -16,17 +16,18 @@ nsContextMenu.prototype = {
   initMenu: function CM_initMenu(aXulMenu, aIsShift) {
     // Get contextual info.
     this.setTarget(document.popupNode, document.popupRangeParent,
                    document.popupRangeOffset);
     if (!this.shouldDisplay)
       return;
 
     this.hasPageMenu = false;
-    if (!aIsShift) {
+    // FIXME (bug 1047751) - The page menu is disabled in e10s.
+    if (!aIsShift && !this.isRemote) {
       this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(this.target,
                                                           aXulMenu);
     }
 
     this.isFrameImage = document.getElementById("isFrameImage");
     this.ellipsis = "\u2026";
     try {
       this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -352,17 +352,17 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_pageInfo.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_page_style_menu.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content
 
 [browser_parsable_css.js]
 skip-if = e10s
 [browser_parsable_script.js]
-skip-if = debug || asan # Times out on debug/asan, and we are less picky about our JS there
+skip-if = asan # Disabled because it takes a long time (see test for more information)
 
 [browser_pinnedTabs.js]
 [browser_plainTextLinks.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (creates and fetches elements directly from content document)
 [browser_popupUI.js]
 skip-if = buildapp == 'mulet' || e10s # Bug ?????? - test directly manipulates content (tries to get a popup element directly from content)
 [browser_printpreview.js]
 skip-if = buildapp == 'mulet' || e10s # Bug ?????? - timeout after logging "Error: Channel closing: too late to send/recv, messages will be lost"
@@ -478,8 +478,10 @@ skip-if = e10s # Bug 516755 - SessionSto
 skip-if = e10s
 [browser_bug1024133-switchtab-override-keynav.js]
 skip-if = e10s
 [browser_bug1025195_switchToTabHavingURI_ignoreFragment.js]
 [browser_addCertException.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (content.document.getElementById)
 [browser_bug1045809.js]
 skip-if = e10s
+[browser_bug1047603.js]
+skip-if = os == "linux" # Bug 1066856 - waiting for OMTC to be enabled by default on Linux.
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1047603.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const OPEN_LOCATION_PREF = "browser.link.open_newwindow";
+const NON_REMOTE_PAGE = "about:crashes";
+
+const SIMPLE_PAGE_HTML = `
+<a href="about:home" target="_blank" id="testAnchor">Open a window</a>
+`;
+
+function frame_script() {
+  addMessageListener("test:click", (message) => {
+    let element = content.document.getElementById("testAnchor");
+    element.click();
+  });
+  sendAsyncMessage("test:ready");
+}
+
+/**
+ * Returns a Promise that resolves once the frame_script is loaded
+ * in the browser, and has seen the DOMContentLoaded event.
+ */
+function waitForFrameScriptReady(mm) {
+  return new Promise((resolve, reject) => {
+    mm.addMessageListener("test:ready", function onTestReady() {
+      mm.removeMessageListener("test:ready", onTestReady);
+      resolve();
+    });
+  });
+}
+
+/**
+ * Takes some browser in some window, and forces that browser
+ * to become non-remote, and then navigates it to a page that
+ * we're not supposed to be displaying remotely. Returns a
+ * Promise that resolves when the browser is no longer remote.
+ */
+function prepareNonRemoteBrowser(aWindow, browser) {
+  aWindow.gBrowser.updateBrowserRemoteness(browser, false);
+  browser.loadURI(NON_REMOTE_PAGE);
+  return new Promise((resolve, reject) => {
+    waitForCondition(() => !browser.isRemoteBrowser, () => {
+      resolve();
+    }, "Waiting for browser to become non-remote");
+  })
+}
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
+});
+
+/**
+ * Test that if we open a new tab from a link in a non-remote
+ * browser in an e10s window, that the new tab's browser is also
+ * not remote. Also tests with a private browsing window.
+ */
+add_task(function* test_new_tab() {
+  let normalWindow = yield promiseOpenAndLoadWindow({
+    remote: true
+  }, true);
+  let privateWindow = yield promiseOpenAndLoadWindow({
+    remote: true,
+    private: true,
+  }, true);
+
+  for (let testWindow of [normalWindow, privateWindow]) {
+    let testBrowser = testWindow.gBrowser.selectedBrowser;
+    yield prepareNonRemoteBrowser(testWindow, testBrowser);
+
+    // Get our framescript ready
+    let mm = testBrowser.messageManager;
+    mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+    let readyPromise = waitForFrameScriptReady(mm);
+    yield readyPromise;
+
+    // Inject our test HTML into our non-remote tab.
+    testBrowser.contentDocument.body.innerHTML = SIMPLE_PAGE_HTML;
+
+    // Click on the link in the browser, and wait for the new tab.
+    mm.sendAsyncMessage("test:click");
+    let tabOpenEvent = yield waitForNewTab(testWindow.gBrowser);
+    let newTab = tabOpenEvent.target;
+    ok(!newTab.linkedBrowser.isRemoteBrowser,
+       "The opened browser should not be remote.");
+
+    testWindow.gBrowser.removeTab(newTab);
+  }
+
+  normalWindow.close();
+  privateWindow.close();
+});
+
+/**
+ * Test that if we open a new window from a link in a non-remote
+ * browser in an e10s window, that the new window is not an e10s
+ * window. Also tests with a private browsing window.
+ */
+add_task(function* test_new_window() {
+  let normalWindow = yield promiseOpenAndLoadWindow({
+    remote: true
+  }, true);
+  let privateWindow = yield promiseOpenAndLoadWindow({
+    remote: true,
+    private: true,
+  }, true);
+
+  // Fiddle with the prefs so that we open target="_blank" links
+  // in new windows instead of new tabs.
+  Services.prefs.setIntPref(OPEN_LOCATION_PREF,
+                            Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW);
+
+  for (let testWindow of [normalWindow, privateWindow]) {
+    let testBrowser = testWindow.gBrowser.selectedBrowser;
+    yield prepareNonRemoteBrowser(testWindow, testBrowser);
+
+    // Get our framescript ready
+    let mm = testBrowser.messageManager;
+    mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+    let readyPromise = waitForFrameScriptReady(mm);
+    yield readyPromise;
+
+    // Inject our test HTML into our non-remote window.
+    testBrowser.contentDocument.body.innerHTML = SIMPLE_PAGE_HTML;
+
+    // Click on the link in the browser, and wait for the new window.
+    let windowOpenPromise = promiseTopicObserved("browser-delayed-startup-finished");
+    mm.sendAsyncMessage("test:click");
+    let [newWindow] = yield windowOpenPromise;
+    ok(!newWindow.gMultiProcessBrowser,
+       "The opened window should not be an e10s window.");
+    newWindow.close();
+  }
+
+  normalWindow.close();
+  privateWindow.close();
+
+  Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
+});
--- a/browser/base/content/test/general/browser_parsable_script.js
+++ b/browser/base/content/test/general/browser_parsable_script.js
@@ -55,28 +55,68 @@ function parsePromise(uri) {
     };
     xhr.overrideMimeType("application/javascript");
     xhr.send(null);
   });
   return promise;
 }
 
 add_task(function* checkAllTheJS() {
-  let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
-  // This asynchronously produces a list of URLs (sadly, mostly sync on our
-  // test infrastructure because it runs against jarfiles there, and
-  // our zipreader APIs are all sync)
-  let uris = yield generateURIsFromDirTree(appDir, [".js", ".jsm"]);
+  // In debug builds, even on a fast machine, collecting the file list may take
+  // more than 30 seconds, and parsing all files may take four more minutes.
+  // For this reason, this test must be explictly requested in debug builds by
+  // using the "--setpref parse=<filter>" argument to mach.  You can specify:
+  //  - A case-sensitive substring of the file name to test (slow).
+  //  - A single absolute URI printed out by a previous run (fast).
+  //  - An empty string to run the test on all files (slowest).
+  let parseRequested = Services.prefs.prefHasUserValue("parse");
+  let parseValue = parseRequested && Services.prefs.getCharPref("parse");
+  if (SpecialPowers.isDebugBuild) {
+    if (!parseRequested) {
+      ok(true, "Test disabled on debug build. To run, execute: ./mach" +
+               " mochitest-browser --setpref parse=<case_sensitive_filter>" +
+               " browser/base/content/test/general/browser_parsable_script.js");
+      return;
+    }
+    // Request a 10 minutes timeout (30 seconds * 20) for debug builds.
+    requestLongerTimeout(20);
+  }
+
+  let uris;
+  // If an absolute URI is specified on the command line, use it immediately.
+  if (parseValue && parseValue.contains(":")) {
+    uris = [NetUtil.newURI(parseValue)];
+  } else {
+    let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+    // This asynchronously produces a list of URLs (sadly, mostly sync on our
+    // test infrastructure because it runs against jarfiles there, and
+    // our zipreader APIs are all sync)
+    let startTimeMs = Date.now();
+    info("Collecting URIs");
+    uris = yield generateURIsFromDirTree(appDir, [".js", ".jsm"]);
+    info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
+
+    // Apply the filter specified on the command line, if any.
+    if (parseValue) {
+      uris = uris.filter(uri => {
+        if (uri.spec.contains(parseValue)) {
+          return true;
+        }
+        info("Not checking filtered out " + uri.spec);
+        return false;
+      });
+    }
+  }
 
   // We create an array of promises so we can parallelize all our parsing
   // and file loading activity:
   let allPromises = [];
   for (let uri of uris) {
     if (uriIsWhiteListed(uri)) {
-      info("Not checking " + uri.spec);
+      info("Not checking whitelisted " + uri.spec);
       continue;
     }
     allPromises.push(parsePromise(uri.spec));
   }
 
   let promiseResults = yield Promise.all(allPromises);
   is(promiseResults.filter((x) => !x).length, 0, "There should be 0 parsing errors");
 });
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -660,8 +660,12 @@ function assertWebRTCIndicatorStatus(exp
         is(docElt.getAttribute("sharing" + item), expectedValue,
            item + " global indicator attribute as expected");
       }
 
       ok(!indicator.hasMoreElements(), "only one global indicator window");
     }
   }
 }
+
+function waitForNewTab(aTabBrowser) {
+  return promiseWaitForEvent(aTabBrowser.tabContainer, "TabOpen");
+}
--- a/browser/branding/aurora/pref/firefox-branding.js
+++ b/browser/branding/aurora/pref/firefox-branding.js
@@ -28,8 +28,12 @@ pref("app.update.url.details", "https://
 pref("app.update.checkInstallTime.days", 2);
 
 // code usage depends on contracts, please contact the Firefox module owner if you have questions
 pref("browser.search.param.yahoo-fr", "moz35");
 pref("browser.search.param.yahoo-fr-ja", "mozff");
 #ifdef MOZ_METRO
 pref("browser.search.param.yahoo-fr-metro", "");
 #endif
+
+// Number of usages of the web console or scratchpad.
+// If this is less than 5, then pasting code into the web console or scratchpad is disabled
+pref("devtools.selfxss.count", 5);
\ No newline at end of file
--- a/browser/branding/nightly/pref/firefox-branding.js
+++ b/browser/branding/nightly/pref/firefox-branding.js
@@ -25,8 +25,12 @@ pref("app.update.url.details", "https://
 pref("app.update.checkInstallTime.days", 2);
 
 // code usage depends on contracts, please contact the Firefox module owner if you have questions
 pref("browser.search.param.yahoo-fr", "moz35");
 pref("browser.search.param.yahoo-fr-ja", "mozff");
 #ifdef MOZ_METRO
 pref("browser.search.param.yahoo-fr-metro", "");
 #endif
+
+// Number of usages of the web console or scratchpad.
+// If this is less than 5, then pasting code into the web console or scratchpad is disabled
+pref("devtools.selfxss.count", 5);
\ No newline at end of file
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -25,8 +25,12 @@ pref("app.update.checkInstallTime.days",
 
 // code usage depends on contracts, please contact the Firefox module owner if you have questions
 pref("browser.search.param.yahoo-fr", "moz35");
 pref("browser.search.param.yahoo-fr-ja", "mozff");
 #ifdef MOZ_METRO
 pref("browser.search.param.ms-pc-metro", "MOZW");
 pref("browser.search.param.yahoo-fr-metro", "mozilla_metro_search");
 #endif
+
+// Number of usages of the web console or scratchpad.
+// If this is less than 5, then pasting code into the web console or scratchpad is disabled
+pref("devtools.selfxss.count", 0);
\ No newline at end of file
--- a/browser/branding/unofficial/pref/firefox-branding.js
+++ b/browser/branding/unofficial/pref/firefox-branding.js
@@ -24,8 +24,12 @@ pref("app.update.url.details", "https://
 pref("app.update.checkInstallTime.days", 2);
 
 // code usage depends on contracts, please contact the Firefox module owner if you have questions
 pref("browser.search.param.yahoo-fr", "moz35");
 pref("browser.search.param.yahoo-fr-ja", "mozff");
 #ifdef MOZ_METRO
 pref("browser.search.param.yahoo-fr-metro", "");
 #endif
+
+// Number of usages of the web console or scratchpad.
+// If this is less than 5, then pasting code into the web console or scratchpad is disabled
+pref("devtools.selfxss.count", 0);
\ No newline at end of file
--- a/browser/components/customizableui/test/browser_967000_button_sync.js
+++ b/browser/components/customizableui/test/browser_967000_button_sync.js
@@ -25,21 +25,26 @@ function openAboutAccountsFromMenuPanel(
     UITour.originTabs.set(window, new Set());
     UITour.originTabs.get(window).add(gBrowser.selectedTab);
   }
 
   let syncButton = document.getElementById("sync-button");
   ok(syncButton, "The Sync button was added to the Panel Menu");
 
   let deferred = Promise.defer();
-  let handler = () => {
-    gBrowser.selectedTab.removeEventListener("load", handler, true);
+  let handler = (e) => {
+    if (e.originalTarget != gBrowser.selectedTab.linkedBrowser.contentDocument ||
+        e.target.location.href == "about:blank") {
+      info("Skipping spurious 'load' event for " + e.target.location.href);
+      return;
+    }
+    gBrowser.selectedTab.linkedBrowser.removeEventListener("load", handler, true);
     deferred.resolve();
   }
-  gBrowser.selectedTab.addEventListener("load", handler, true);
+  gBrowser.selectedTab.linkedBrowser.addEventListener("load", handler, true);
 
   syncButton.click();
   yield deferred.promise;
   newTab = gBrowser.selectedTab;
 
   is(gBrowser.currentURI.spec, "about:accounts?entrypoint=" + entryPoint,
     "Firefox Sync page opened with `menupanel` entrypoint");
   ok(!isPanelUIOpen(), "The panel closed");
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -37,41 +37,53 @@ const cloneErrorObject = function(error,
   let obj = new targetWindow.Error();
   for (let prop of Object.getOwnPropertyNames(error)) {
     obj[prop] = String(error[prop]);
   }
   return obj;
 };
 
 /**
+ * Makes an object or value available to an unprivileged target window.
+ *
+ * Primitives are returned as they are, while objects are cloned into the
+ * specified target.  Error objects are also handled correctly.
+ *
+ * @param {any}          value        Value or object to copy
+ * @param {nsIDOMWindow} targetWindow The content window to copy to
+ */
+const cloneValueInto = function(value, targetWindow) {
+  if (!value || typeof value != "object") {
+    return value;
+  }
+
+  // Inspect for an error this way, because the Error object is special.
+  if (value.constructor.name == "Error") {
+    return cloneErrorObject(value, targetWindow);
+  }
+
+  return Cu.cloneInto(value, targetWindow);
+};
+
+/**
  * Inject any API containing _only_ function properties into the given window.
  *
  * @param {Object}       api          Object containing functions that need to
  *                                    be exposed to content
  * @param {nsIDOMWindow} targetWindow The content window to attach the API
  */
 const injectObjectAPI = function(api, targetWindow) {
   let injectedAPI = {};
   // Wrap all the methods in `api` to help results passed to callbacks get
   // through the priv => unpriv barrier with `Cu.cloneInto()`.
   Object.keys(api).forEach(func => {
     injectedAPI[func] = function(...params) {
       let callback = params.pop();
       api[func](...params, function(...results) {
-        results = results.map(result => {
-          if (result && typeof result == "object") {
-            // Inspect for an error this way, because the Error object is special.
-            if (result.constructor.name == "Error") {
-              return cloneErrorObject(result.message)
-            }
-            return Cu.cloneInto(result, targetWindow);
-          }
-          return result;
-        });
-        callback(...results);
+        callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
       });
     };
   });
 
   let contentObj = Cu.cloneInto(injectedAPI, targetWindow, {cloneFunctions: true});
   // Since we deny preventExtensions on XrayWrappers, because Xray semantics make
   // it difficult to act like an object has actually been frozen, we try to seal
   // the `contentObj` without Xrays.
@@ -198,21 +210,21 @@ function injectLoopAPI(targetWindow) {
      *                            happened.
      */
     ensureRegistered: {
       enumerable: true,
       writable: true,
       value: function(callback) {
         // We translate from a promise to a callback, as we can't pass promises from
         // Promise.jsm across the priv versus unpriv boundary.
-        return MozLoopService.register().then(() => {
+        MozLoopService.register().then(() => {
           callback(null);
         }, err => {
-          callback(err);
-        });
+          callback(cloneValueInto(err, targetWindow));
+        }).catch(Cu.reportError);
       }
     },
 
     /**
      * Used to note a call url expiry time. If the time is later than the current
      * latest expiry time, then the stored expiry time is increased. For times
      * sooner, this function is a no-op; this ensures we always have the latest
      * expiry time for a url.
@@ -352,21 +364,30 @@ function injectLoopAPI(targetWindow) {
      * @param {Function} callback Called when the request completes.
      */
     hawkRequest: {
       enumerable: true,
       writable: true,
       value: function(path, method, payloadObj, callback) {
         // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST
         // XXX Should really return a DOM promise here.
-        return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
+        MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
           callback(null, response.body);
-        }, (error) => {
-          callback(Cu.cloneInto(error, targetWindow));
-        });
+        }, hawkError => {
+          // The hawkError.error property, while usually a string representing
+          // an HTTP response status message, may also incorrectly be a native
+          // error object that will cause the cloning function to fail.
+          callback(Cu.cloneInto({
+            error: (hawkError.error && typeof hawkError.error == "string")
+                   ? hawkError.error : "Unexpected exception",
+            message: hawkError.message,
+            code: hawkError.code,
+            errno: hawkError.errno,
+          }, targetWindow));
+        }).catch(Cu.reportError);
       }
     },
 
     LOOP_SESSION_TYPE: {
       enumerable: true,
       writable: false,
       value: function() {
         return LOOP_SESSION_TYPE;
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -6,34 +6,36 @@
 
 /* jshint newcap:false, esnext:true */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversation = (function(OT, mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views,
-      // aliasing translation function as __ for concision
-      __ = mozL10n.get;
+  var sharedViews = loop.shared.views;
 
   /**
    * App router.
    * @type {loop.desktopRouter.DesktopConversationRouter}
    */
   var router;
 
   var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
 
     propTypes: {
-      model: React.PropTypes.object.isRequired
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
     },
 
-    getInitialProps: function() {
-      return {showDeclineMenu: false};
+    getDefaultProps: function() {
+      return {
+        showDeclineMenu: false,
+        video: true
+      };
     },
 
     getInitialState: function() {
       return {showDeclineMenu: this.props.showDeclineMenu};
     },
 
     componentDidMount: function() {
       window.addEventListener("click", this.clickHandler);
@@ -74,84 +76,135 @@ loop.conversation = (function(OT, mozL10
       var currentState = this.state.showDeclineMenu;
       this.setState({showDeclineMenu: !currentState});
     },
 
     _hideDeclineMenu: function() {
       this.setState({showDeclineMenu: false});
     },
 
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
     render: function() {
       /* jshint ignore:start */
       var btnClassAccept = "btn btn-accept";
       var btnClassDecline = "btn btn-error btn-decline";
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         React.DOM.div({className: conversationPanelClass}, 
-          React.DOM.h2(null, __("incoming_call_title2")), 
+          React.DOM.h2(null, mozL10n.get("incoming_call_title2")), 
           React.DOM.div({className: "btn-group incoming-call-action-group"}, 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
             React.DOM.div({className: "btn-chevron-menu-group"}, 
               React.DOM.div({className: "btn-group-chevron"}, 
                 React.DOM.div({className: "btn-group"}, 
 
                   React.DOM.button({className: btnClassDecline, 
                           onClick: this._handleDecline}, 
-                    __("incoming_call_cancel_button")
+                    mozL10n.get("incoming_call_cancel_button")
                   ), 
                   React.DOM.div({className: "btn-chevron", 
                        onClick: this._toggleDeclineMenu}
                   )
                 ), 
 
                 React.DOM.ul({className: dropdownMenuClassesDecline}, 
                   React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, 
-                    __("incoming_call_cancel_and_block_button")
+                    mozL10n.get("incoming_call_cancel_and_block_button")
                   )
                 )
 
               )
             ), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
-            React.DOM.div({className: "btn-chevron-menu-group"}, 
-              React.DOM.div({className: "btn-group"}, 
-                React.DOM.button({className: btnClassAccept, 
-                        onClick: this._handleAccept("audio-video")}, 
-                  React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
-                    __("incoming_call_accept_button")
-                  ), 
-                  React.DOM.span({className: "fx-embedded-btn-icon-video"}
-                  )
-                ), 
-                React.DOM.div({className: "call-audio-only", 
-                     onClick: this._handleAccept("audio"), 
-                     title: __("incoming_call_accept_audio_only_tooltip")}
-                )
-              )
-            ), 
+            AcceptCallButton({mode: this._answerModeProps()}), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"})
 
           )
         )
       );
       /* jshint ignore:end */
     }
   });
 
   /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        React.DOM.div({className: "btn-chevron-menu-group"}, 
+          React.DOM.div({className: "btn-group"}, 
+            React.DOM.button({className: "btn btn-accept", 
+                    onClick: mode.primary.handler, 
+                    title: mozL10n.get(mode.primary.tooltip)}, 
+              React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
+                mozL10n.get("incoming_call_accept_button")
+              ), 
+              React.DOM.span({className: mode.primary.className})
+            ), 
+            React.DOM.div({className: mode.secondary.className, 
+                 onClick: mode.secondary.handler, 
+                 title: mozL10n.get(mode.secondary.tooltip)}
+            )
+          )
+        )
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
    * Conversation router.
    *
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
@@ -220,17 +273,17 @@ loop.conversation = (function(OT, mozL10
       this._websocket = new loop.CallConnectionWebSocket({
         url: this._conversation.get("progressURL"),
         websocketToken: this._conversation.get("websocketToken"),
         callId: this._conversation.get("callId"),
       });
       this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
-          video: {enabled: this._conversation.hasVideoStream("incoming")}
+          video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -6,34 +6,36 @@
 
 /* jshint newcap:false, esnext:true */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversation = (function(OT, mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views,
-      // aliasing translation function as __ for concision
-      __ = mozL10n.get;
+  var sharedViews = loop.shared.views;
 
   /**
    * App router.
    * @type {loop.desktopRouter.DesktopConversationRouter}
    */
   var router;
 
   var IncomingCallView = React.createClass({
 
     propTypes: {
-      model: React.PropTypes.object.isRequired
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
     },
 
-    getInitialProps: function() {
-      return {showDeclineMenu: false};
+    getDefaultProps: function() {
+      return {
+        showDeclineMenu: false,
+        video: true
+      };
     },
 
     getInitialState: function() {
       return {showDeclineMenu: this.props.showDeclineMenu};
     },
 
     componentDidMount: function() {
       window.addEventListener("click", this.clickHandler);
@@ -74,84 +76,135 @@ loop.conversation = (function(OT, mozL10
       var currentState = this.state.showDeclineMenu;
       this.setState({showDeclineMenu: !currentState});
     },
 
     _hideDeclineMenu: function() {
       this.setState({showDeclineMenu: false});
     },
 
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
     render: function() {
       /* jshint ignore:start */
       var btnClassAccept = "btn btn-accept";
       var btnClassDecline = "btn btn-error btn-decline";
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         <div className={conversationPanelClass}>
-          <h2>{__("incoming_call_title2")}</h2>
+          <h2>{mozL10n.get("incoming_call_title2")}</h2>
           <div className="btn-group incoming-call-action-group">
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
             <div className="btn-chevron-menu-group">
               <div className="btn-group-chevron">
                 <div className="btn-group">
 
                   <button className={btnClassDecline}
                           onClick={this._handleDecline}>
-                    {__("incoming_call_cancel_button")}
+                    {mozL10n.get("incoming_call_cancel_button")}
                   </button>
                   <div className="btn-chevron"
                        onClick={this._toggleDeclineMenu}>
                   </div>
                 </div>
 
                 <ul className={dropdownMenuClassesDecline}>
                   <li className="btn-block" onClick={this._handleDeclineBlock}>
-                    {__("incoming_call_cancel_and_block_button")}
+                    {mozL10n.get("incoming_call_cancel_and_block_button")}
                   </li>
                 </ul>
 
               </div>
             </div>
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
-            <div className="btn-chevron-menu-group">
-              <div className="btn-group">
-                <button className={btnClassAccept}
-                        onClick={this._handleAccept("audio-video")}>
-                  <span className="fx-embedded-answer-btn-text">
-                    {__("incoming_call_accept_button")}
-                  </span>
-                  <span className="fx-embedded-btn-icon-video">
-                  </span>
-                </button>
-                <div className="call-audio-only"
-                     onClick={this._handleAccept("audio")}
-                     title={__("incoming_call_accept_audio_only_tooltip")} >
-                </div>
-              </div>
-            </div>
+            <AcceptCallButton mode={this._answerModeProps()} />
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
           </div>
         </div>
       );
       /* jshint ignore:end */
     }
   });
 
   /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        <div className="btn-chevron-menu-group">
+          <div className="btn-group">
+            <button className="btn btn-accept"
+                    onClick={mode.primary.handler}
+                    title={mozL10n.get(mode.primary.tooltip)}>
+              <span className="fx-embedded-answer-btn-text">
+                {mozL10n.get("incoming_call_accept_button")}
+              </span>
+              <span className={mode.primary.className}></span>
+            </button>
+            <div className={mode.secondary.className}
+                 onClick={mode.secondary.handler}
+                 title={mozL10n.get(mode.secondary.tooltip)}>
+            </div>
+          </div>
+        </div>
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
    * Conversation router.
    *
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
@@ -220,17 +273,17 @@ loop.conversation = (function(OT, mozL10
       this._websocket = new loop.CallConnectionWebSocket({
         url: this._conversation.get("progressURL"),
         websocketToken: this._conversation.get("websocketToken"),
         callId: this._conversation.get("callId"),
       });
       this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
-          video: {enabled: this._conversation.hasVideoStream("incoming")}
+          video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -130,30 +130,33 @@ p {
     background-color: #64a43a;
     border: 1px solid #64a43a;
   }
 
 .btn-warning {
   background-color: #f0ad4e;
 }
 
+.btn-cancel,
 .btn-error,
 .btn-hangup,
 .btn-error + .btn-chevron {
   background-color: #d74345;
   border: 1px solid #d74345;
 }
 
+  .btn-cancel:hover,
   .btn-error:hover,
   .btn-hangup:hover,
   .btn-error + .btn-chevron:hover {
     background-color: #c53436;
     border: 1px solid #c53436;
   }
 
+  .btn-cancel:active,
   .btn-error:active,
   .btn-hangup:active,
   .btn-error + .btn-chevron:active {
     background-color: #ae2325;
     border: 1px solid #ae2325;
   }
 
 .btn-chevron {
@@ -217,37 +220,32 @@ p {
   border-radius: 2px;
   border-bottom-right-radius: 0;
   border-top-right-radius: 0;
 }
 
 /* Alerts */
 .alert {
   background: #eee;
-  padding: .2em 1em;
+  padding: .4em 1em;
   margin-bottom: 1em;
   border-bottom: 2px solid #E9E9E9;
 }
 
 .alert p.message {
   padding: 0;
   margin: 0;
 }
 
-.alert.alert-error {
-  display: flex;
-  align-content: center;
-  padding: 5px;
-  font-size: 10px;
-  justify-content: center;
-  color: #FFF;
+.alert-error {
   background: repeating-linear-gradient(-45deg, #D74345, #D74345 10px, #D94B4D 10px, #D94B4D 20px) repeat scroll 0% 0% transparent;
+  color: #fff;
 }
 
-.alert.alert-warning {
+.alert-warning {
   background: #fcf8e3;
   border: 1px solid #fbeed5;
 }
 
 .alert .close {
   position: relative;
   top: -.1rem;
   right: -1rem;
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -78,26 +78,64 @@
   }
 
 .fx-embedded-answer-btn-text {
   vertical-align: bottom;
   /* don't stretch the button if the localized text is too big */
   max-width: 80%;
 }
 
-.fx-embedded-btn-icon-video {
+.fx-embedded-btn-icon-video,
+.fx-embedded-btn-icon-audio {
   display: inline-block;
   vertical-align: top;
   width: .8rem;
   height: .8rem;
-  background-image: url("../img/video-inverse-14x14.png");
   background-repeat: no-repeat;
   cursor: pointer;
 }
 
+.fx-embedded-btn-icon-video,
+.fx-embedded-btn-video-small {
+  background-image: url("../img/video-inverse-14x14.png");
+}
+
+.fx-embedded-btn-icon-audio,
+.fx-embedded-btn-audio-small {
+  background-image: url("../img/audio-inverse-14x14.png");
+}
+
+.fx-embedded-btn-audio-small,
+.fx-embedded-btn-video-small {
+  width: 26px;
+  height: 26px;
+  border-left: 1px solid rgba(255,255,255,.4);
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+  background-color: #74BF43;
+  background-position: center;
+  background-size: 1rem;
+  background-repeat: no-repeat;
+  cursor: pointer;
+}
+
+  .fx-embedded-btn-video-small:hover,
+  .fx-embedded-btn-audio-small:hover {
+    background-color: #6cb23e;
+  }
+
+@media (min-resolution: 2dppx) {
+  .fx-embedded-btn-audio-small {
+    background-image: url("../img/audio-inverse-14x14@2x.png");
+  }
+  .fx-embedded-btn-video-small {
+    background-image: url("../img/video-inverse-14x14@2x.png");
+  }
+}
+
 .standalone .btn-hangup {
   width: auto;
   font-size: 12px;
   border-radius: 2px;
   padding: 0 20px;
 }
 
 .fx-embedded .conversation-toolbar .btn-hangup {
@@ -228,40 +266,16 @@
   margin: 0.83em 0;
 }
 
 .fx-embedded-incoming-call-button-spacer {
   display: flex;
   flex: 1;
 }
 
-.call-audio-only {
-  width: 26px;
-  height: 26px;
-  border-left: 1px solid rgba(255,255,255,.4);
-  border-top-right-radius: 2px;
-  border-bottom-right-radius: 2px;
-  background-color: #74BF43;
-  background-image: url("../img/audio-inverse-14x14.png");
-  background-size: 1rem;
-  background-position: center;
-  background-repeat: no-repeat;
-  cursor: pointer;
-}
-
-  .call-audio-only:hover {
-    background-color: #6cb23e;
-  }
-
-@media (min-resolution: 2dppx) {
-  .call-audio-only {
-    background-image: url("../img/audio-inverse-14x14@2x.png");
-  }
-}
-
 /* Expired call url page */
 
 .expired-url-info {
   width: 400px;
   margin: 0 auto;
 }
 
 .promote-firefox {
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -47,52 +47,38 @@ loop.shared.models = (function(l10n) {
 
     /**
      * SDK session object.
      * @type {XXX}
      */
     session: undefined,
 
     /**
-     * Pending call timeout value.
-     * @type {Number}
-     */
-    pendingCallTimeout: undefined,
-
-    /**
-     * Pending call timer.
-     * @type {Number}
-     */
-    _pendingCallTimer: undefined,
-
-    /**
      * Constructor.
      *
      * Options:
      *
      * Required:
      * - {OT} sdk: OT SDK object.
      *
-     * Optional:
-     * - {Number} pendingCallTimeout: Pending call timeout in milliseconds
-     *                                (default: 20000).
-     *
      * @param  {Object} attributes Attributes object.
      * @param  {Object} options    Options object.
      */
     initialize: function(attributes, options) {
       options = options || {};
       if (!options.sdk) {
         throw new Error("missing required sdk");
       }
       this.sdk = options.sdk;
-      this.pendingCallTimeout = options.pendingCallTimeout || 20000;
 
-      // Ensure that any pending call timer is cleared on disconnect/error
-      this.on("session:ended session:error", this._clearPendingCallTimer, this);
+      // Set loop.debug.sdk to true in the browser, or standalone:
+      // localStorage.setItem("debug.sdk", true);
+      if (loop.shared.utils.getBoolPreference("debug.sdk")) {
+        this.sdk.setLogLevel(this.sdk.DEBUG);
+      }
     },
 
     /**
      * Starts an incoming conversation.
      */
     incoming: function() {
       this.trigger("call:incoming");
     },
@@ -107,30 +93,16 @@ loop.shared.models = (function(l10n) {
 
     /**
      * Starts an outgoing conversation.
      *
      * @param {Object} sessionData The session data received from the
      *                             server for the outgoing call.
      */
     outgoing: function(sessionData) {
-      this._clearPendingCallTimer();
-
-      // Outgoing call has never reached destination, closing - see bug 1020448
-      function handleOutgoingCallTimeout() {
-        /*jshint validthis:true */
-        if (!this.get("ongoing")) {
-          this.trigger("timeout").endSession();
-        }
-      }
-
-      // Setup pending call timeout.
-      this._pendingCallTimer = setTimeout(
-        handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
-
       this.setOutgoingSessionData(sessionData);
       this.trigger("call:outgoing");
     },
 
     /**
      * Checks that the session is ready.
      *
      * @return {Boolean}
@@ -274,25 +246,16 @@ loop.shared.models = (function(l10n) {
           break;
         default:
           this.trigger("session:error", err);
           break;
       }
     },
 
     /**
-     * Clears current pending call timer, if any.
-     */
-    _clearPendingCallTimer: function() {
-      if (this._pendingCallTimer) {
-        clearTimeout(this._pendingCallTimer);
-      }
-    },
-
-    /**
      * Manages connection status
      * triggers apropriate event for connection error/success
      * http://tokbox.com/opentok/tutorials/connect-session/js/
      * http://tokbox.com/opentok/tutorials/hello-world/js/
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param {error|null} error
      */
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -24,12 +24,30 @@ loop.shared.utils = (function() {
     }
     if (navigator.platform.indexOf("Linux") !== -1) {
       platform = "linux";
     }
 
     return platform;
   }
 
+  /**
+   * Used for getting a boolean preference. It will either use the browser preferences
+   * (if navigator.mozLoop is defined) or try to get them from localStorage.
+   *
+   * @param {String} prefName The name of the preference. Note that mozLoop adds
+   *                          'loop.' to the start of the string.
+   *
+   * @return The value of the preference, or false if not available.
+   */
+  function getBoolPreference(prefName) {
+    if (navigator.mozLoop) {
+      return !!navigator.mozLoop.getLoopBoolPref(prefName);
+    }
+
+    return !!localStorage.getItem(prefName);
+  }
+
   return {
-    getTargetPlatform: getTargetPlatform
+    getTargetPlatform: getTargetPlatform,
+    getBoolPreference: getBoolPreference
   };
 })();
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -31,21 +31,20 @@ loop.CallConnectionWebSocket = (function
     }
     if (!this.options.callId) {
       throw new Error("No callId in options");
     }
     if (!this.options.websocketToken) {
       throw new Error("No websocketToken in options");
     }
 
-    // Save the debug pref now, to avoid getting it each time.
-    if (navigator.mozLoop) {
-      this._debugWebSocket =
-        navigator.mozLoop.getLoopBoolPref("debug.websocket");
-    }
+    // Set loop.debug.sdk to true in the browser, or standalone:
+    // localStorage.setItem("debug.websocket", true);
+    this._debugWebSocket =
+      loop.shared.utils.getBoolPreference("debug.websocket");
 
     _.extend(this, Backbone.Events);
   };
 
   CallConnectionWebSocket.prototype = {
     /**
      * Start the connection to the websocket.
      *
@@ -144,16 +143,28 @@ loop.CallConnectionWebSocket = (function
     mediaUp: function() {
       this._send({
         messageType: "action",
         event: "media-up"
       });
     },
 
     /**
+     * Notifies the server that the outgoing call is cancelled by the
+     * user.
+     */
+    cancel: function() {
+      this._send({
+        messageType: "action",
+        event: "terminate",
+        reason: "cancel"
+      });
+    },
+
+    /**
      * Sends data on the websocket.
      *
      * @param {Object} data The data to send.
      */
     _send: function(data) {
       this._log("WS Sending", data);
 
       this.socket.send(JSON.stringify(data));
@@ -201,16 +212,17 @@ loop.CallConnectionWebSocket = (function
 
       this._lastServerState = msg.state;
 
       switch(msg.messageType) {
         case "hello":
           this._completeConnection();
           break;
         case "progress":
+          this.trigger("progress:" + msg.state);
           this.trigger("progress", msg);
           break;
       }
     },
 
     /**
      * Called when there is an error on the websocket.
      *
--- a/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
+++ b/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
@@ -218,21 +218,23 @@
   display: inline-block;
   margin-top: 20px;
   width: 227px;
   height: 94px;
   background-image: url(../images/rtc/access-prompt-chrome.png);
 }
 
 .OT_closeButton {
-  top: 15px;
-  right: 15px;
+  color: #999999;
+  cursor: pointer;
+  font-size: 32px;
+  line-height: 30px;
   position: absolute;
-  font-size: 18px;
-  cursor: pointer;
+  right: 15px;
+  top: 0;
 }
 
 .OT_dialog-messages {
   position: absolute;
   top: 32px;
   left: 32px;
   right: 32px;
   text-align: center;
@@ -261,25 +263,45 @@
   margin-top: 4px;
 }
 
 .OT_dialog-messages-minor strong {
   font-weight: 300;
   color: #ffffff;
 }
 
+.OT_dialog-hidden {
+  display: none;
+}
+
 .OT_dialog-single-button {
   position: absolute;
   bottom: 41px;
   left: 50%;
   margin-left: -97px;
   height: 47px;
   width: 193px;
 }
 
+
+.OT_dialog-single-button-wide {
+    bottom: 35px;
+    height: 140px;
+    left: 5px;
+    position: absolute;
+    right: 0;
+}
+  .OT_dialog-single-button-with-title {
+      margin: 0 auto;
+      padding-left: 30px;
+      padding-right: 30px;
+      width: 270px;
+  }
+
+
 .OT_dialog-button-pair {
   position: absolute;
   bottom: 45px;
   left: 5px;
   right: 0;
   height: 94px;
 }
 
@@ -296,50 +318,72 @@
   width: 1px;
   float: left;
 }
 
 .OT_dialog-button-title {
   font-weight: 300;
   text-align: center;
   margin-bottom: 15px;
-  font-size: 12px;
+  font-size: 14px;
   line-height: 150%;
-  color: #A4A4A4;
+  color: #999999;
 }
 
+.OT_dialog-button-title label {
+  color: #999999;
+}
+
+.OT_dialog-button-title a,
+.OT_dialog-button-title a:link,
+.OT_dialog-button-title a:active {
+  color: #02A1DE;
+}
 
 .OT_dialog-button-title strong {
   color: #ffffff;
   font-weight: 100;
   display: block;
 }
 
 .OT_dialog-button {
   font-weight: 100;
   display: block;
   line-height: 50px;
   height: 47px;
-  background-color: #29A4DA;
+  background-color: #1CA3DC;
   text-align: center;
   font-size: 16pt;
   cursor: pointer;
 }
 
 .OT_dialog-button.OT_dialog-button-disabled {
-  background-color: #444444;
-  color: #999999;
   cursor: not-allowed;
+
+  /* IE 8 */
+  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+
+  opacity: 0.5;
 }
 
 .OT_dialog-button.OT_dialog-button-large {
   line-height: 60px;
   height: 58px;
 }
 
+.OT_dialog-button.OT_dialog-button-small {
+  background-color: #444444;
+  color: #999999;
+  font-size: 12pt;
+  height: 40px;
+  line-height: 40px;
+  margin: 20px auto 0 auto;
+  width: 86px;
+}
+
 .OT_dialog-progress-bar {
   border: 1px solid #4E4E4E;
   height: 8px;
 }
 
 .OT_dialog-progress-bar-fill {
   background-color: #29A4DA;
   height: 10px;
@@ -513,16 +557,21 @@
     position: absolute;
 }
 
 .OT_publisher .OT_bar,
 .OT_subscriber .OT_bar {
     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 */
+}
+
 /* 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;
@@ -895,16 +944,18 @@
 .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;
 }
 
 .OT_publisher .OT_edge-bar-item.OT_mode-on,
 .OT_subscriber .OT_edge-bar-item.OT_mode-on,
+.OT_publisher .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
+.OT_subscriber .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
 .OT_publisher:hover .OT_edge-bar-item.OT_mode-auto,
 .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;
 }
 
@@ -927,18 +978,20 @@
 .OT_publisher .OT_opentok.OT_mode-off,
 .OT_publisher .OT_opentok.OT_mode-auto,
 .OT_subscriber .OT_opentok.OT_mode-off,
 .OT_subscriber .OT_opentok.OT_mode-auto  {
     top: -17px;
 }
 
 .OT_publisher .OT_opentok.OT_mode-on,
+.OT_publisher .OT_opentok.OT_mode-auto.OT_mode-on-hold,
 .OT_publisher:hover .OT_opentok.OT_mode-auto,
 .OT_subscriber .OT_opentok.OT_mode-on,
+.OT_subscriber .OT_opentok.OT_mode-auto.OT_mode-on-hold,
 .OT_subscriber:hover .OT_opentok.OT_mode-auto {
     top: 8px;
 }
 
 
 /* Contains the video element, used to fix video letter-boxing */
 .OT_video-container {
     position: absolute;
@@ -976,20 +1029,103 @@
 .OT_subscriber.OT_loading object {
     display: none;
 }
 
 
 .OT_video-poster {
     width: 100%;
     height: 100%;
-    background-position: 50% 50%;
+    display: none;
+
+    opacity: .25;
+    background-size: auto 76%;
     background-repeat: no-repeat;
+    background-position: center bottom;
+    background-image: url(../images/rtc/audioonly-silhouette.svg);
+}
+
+.OT_audio-level-meter {
+    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%;
+}
+
+.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%;
+
+    background-color: rgba(0, 0, 0, .8);
+}
+
+.OT_audio-level-meter__audio-only-img {
+    position: absolute;
+    top: 22%;
+    right: 15%;
+    width: 40%;
+
+    opacity: .7;
+
+    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%;
+}
+
+.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%);
+}
+
+.OT_audio-level-meter {
     display: none;
 }
 
-.OT_publisher .OT_video-poster {
-    background-image: url(../images/rtc/audioonly-publisher.png);
+.OT_audio-level-meter.OT_mode-on,
+.OT_audio-only .OT_audio-level-meter.OT_mode-auto {
+    display: block;
 }
 
-.OT_subscriber .OT_video-poster  {
-    background-image: url(../images/rtc/audioonly-subscriber.png);
+.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;
 }
+
+.OT_video-disabled {
+    background-image: url(../images/rtc/video-disabled.png);
+}
+
+.OT_video-disabled-warning {
+    background-image: url(../images/rtc/video-disabled-warning.png);
+}
+
+.OT_video-disabled-indicator.OT_active {
+    display: block;
+}
old mode 100644
new mode 100755
--- a/browser/components/loop/content/shared/libs/sdk.js
+++ b/browser/components/loop/content/shared/libs/sdk.js
@@ -1,82 +1,80 @@
 /**
- * @license  OpenTok JavaScript Library v2.2.7.2
+ * @license  OpenTok JavaScript Library v2.2.9.1
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: August 05 08:56:17 2014
+ * Date: September 08 10:17:05 2014
  */
 
 (function(window) {
   if (!window.OT) window.OT = {};
 
   OT.properties = {
-    version: 'v2.2.7.2',         // The current version (eg. v2.0.4) (This is replaced by gradle)
-    build: '9425efe',    // The current build hash (This is replaced by gradle)
+    version: 'v2.2.9.1',         // The current version (eg. v2.0.4) (This is replaced by gradle)
+    build: '72b534e',    // The current build hash (This is replaced by gradle)
 
     // Whether or not to turn on debug logging by default
     debug: 'false',
     // The URL of the tokbox website
     websiteURL: 'http://www.tokbox.com',
 
     // The URL of the CDN
     cdnURL: 'http://static.opentok.com',
     // The URL to use for logging
-    loggingURL: 'https://hlg.tokbox.com/prod',
+    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('26'),
-      chrome: parseFloat('32')
+      firefox: parseFloat('29'),
+      chrome: parseFloat('34')
     }
   };
 
 })(window);
 /**
- * @license  Common JS Helpers on OpenTok 0.2.0 5c6f145 vib-2.2-node-fixes
+ * @license  Common JS Helpers on OpenTok 0.2.0 3fa583f master
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: July 28 08:28:31 2014
+ * Date: August 08 12:31:42 2014
  *
  */
 
 // OT Helper Methods
 //
 // helpers.js                           <- the root file
 // helpers/lib/{helper topic}.js        <- specialised helpers for specific tasks/topics
 //                                          (i.e. video, dom, etc)
 //
 // @example Getting a DOM element by it's id
 //  var element = OTHelpers('domId');
 //
-// @example Testing for web socket support
-//  if (OT.supportsWebSockets()) {
-//      // do some stuff with websockets
-//  }
 //
 
 /*jshint browser:true, smarttabs:true*/
 
 !(function(window, undefined) {
 
 
   var OTHelpers = function(domId) {
@@ -291,20 +289,16 @@
     return OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj);
   };
 
 
 
 // Handy do nothing function
   OTHelpers.noop = function() {};
 
-// Returns true if the client supports WebSockets, false otherwise.
-  OTHelpers.supportsWebSockets = function() {
-    return 'WebSocket' in window;
-  };
 
 // Returns the number of millisceonds since the the UNIX epoch, this is functionally
 // equivalent to executing new Date().getTime().
 //
 // Where available, we use 'performance.now' which is more accurate and reliable,
 // otherwise we default to new Date().getTime().
   OTHelpers.now = (function() {
     var performance = window.performance || {},
@@ -760,17 +754,17 @@
       return _rndBytes;
     };
   }
 
   // Select RNG with best quality
   var _rng = whatwgRNG || mathRNG;
 
   // Buffer class to use
-  var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array;
+  var BufferClass = typeof(Buffer) === 'function' ? Buffer : Array;
 
   // Maps for number <-> hex string conversion
   var _byteToHex = [];
   var _hexToByte = {};
   for (var i = 0; i < 256; i++) {
     _byteToHex[i] = (i + 0x100).toString(16).substr(1);
     _hexToByte[_byteToHex[i]] = i;
   }
@@ -809,18 +803,18 @@
 
   // **`v4()` - Generate random UUID**
 
   // See https://github.com/broofa/node-uuid for API details
   function v4(options, buf, offset) {
     // Deprecated - 'format' argument, as supported in v1.2
     var i = buf && offset || 0;
 
-    if (typeof(options) == 'string') {
-      buf = options == 'binary' ? new BufferClass(16) : null;
+    if (typeof(options) === 'string') {
+      buf = options === 'binary' ? new BufferClass(16) : null;
       options = null;
     }
     options = options || {};
 
     var rnds = options.random || (options.rng || _rng)();
 
     // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
     rnds[6] = (rnds[6] & 0x0f) | 0x40;
@@ -851,241 +845,241 @@
 
 }(window, window.OTHelpers));
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
 (function(window, OTHelpers, undefined) {
 
-OTHelpers.useLogHelpers = function(on){
+  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);
+      Function.prototype.bind.call(window.console.log, window.console);
     } catch (err) {
-        _canApplyConsole = false;
+      _canApplyConsole = false;
     }
 
     // Some objects can't be logged in the console, mostly these are certain
     // types of native objects that are exposed to JS. This is only really a
     // problem with IE, hence only the IE version does anything.
     var makeLogArgumentsSafe = function(args) { return args; };
 
     if (OTHelpers.browser() === 'IE') {
-        makeLogArgumentsSafe = function(args) {
-            return [toDebugString(Array.prototype.slice.apply(args))];
-        };
+      makeLogArgumentsSafe = function(args) {
+        return [toDebugString(Array.prototype.slice.apply(args))];
+      };
     }
 
     // Generates a logging method for a particular method and log level.
     //
     // Attempts to handle the following cases:
     // * the desired log method doesn't exist, call fallback (if available) instead
     // * the console functionality isn't available because the developer tools (in IE)
     // aren't open, call fallback (if available)
     // * attempt to deal with weird IE hosted logging methods as best we can.
     //
     function generateLoggingMethod(method, level, fallback) {
-        return function() {
-            if (on.shouldLog(level)) {
-                var cons = window.console,
-                    args = makeLogArgumentsSafe(arguments);
-
-                // In IE, window.console may not exist if the developer tools aren't open
-                // This also means that cons and cons[method] can appear at any moment
-                // hence why we retest this every time.
-                if (cons && cons[method]) {
-                    // the desired console method isn't a real object, which means
-                    // that we can't use apply on it. We force it to be a real object
-                    // using Function.bind, assuming that's available.
-                    if (cons[method].apply || _canApplyConsole) {
-                        if (!cons[method].apply) {
-                            cons[method] = Function.prototype.bind.call(cons[method], cons);
-                        }
-
-                        cons[method].apply(cons, args);
-                    }
-                    else {
-                        // This isn't the same result as the above, but it's better
-                        // than nothing.
-                        cons[method](args);
-                    }
-                }
-                else if (fallback) {
-                    fallback.apply(on, args);
-
-                    // Skip appendToLogs, we delegate entirely to the fallback
-                    return;
-                }
-
-                appendToLogs(method, makeLogArgumentsSafe(arguments));
-            }
-        };
+      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;
+      _logLevel = typeof(level) === 'number' ? level : 0;
+      on.debug('TB.setLogLevel(' + _logLevel + ')');
+      return _logLevel;
     };
 
     on.getLogs = function() {
-        return _logs;
+      return _logs;
     };
 
     // Determine if the level is visible given the current logLevel.
     on.shouldLog = function(level) {
-        return _logLevel >= 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();
+      var now = new Date();
+      return now.toLocaleTimeString() + now.getMilliseconds();
     }
 
     function toJson(object) {
-        try {
-            return JSON.stringify(object);
-        } catch(e) {
-            return object.toString();
-        }
+      try {
+        return JSON.stringify(object);
+      } catch(e) {
+        return object.toString();
+      }
     }
 
     function toDebugString(object) {
-        var components = [];
-
-        if (typeof(object) === 'undefined') {
-            // noop
-        }
-        else if (object === null) {
-            components.push('NULL');
-        }
-        else if (OTHelpers.isArray(object)) {
-            for (var i=0; i<object.length; ++i) {
-                components.push(toJson(object[i]));
-            }
-        }
-        else if (OTHelpers.isObject(object)) {
-            for (var key in object) {
-                var stringValue;
-
-                if (!OTHelpers.isFunction(object[key])) {
-                    stringValue = toJson(object[key]);
-                }
-                else if (object.hasOwnProperty(key)) {
-                    stringValue = 'function ' + key + '()';
-                }
-
-                components.push(key + ': ' + stringValue);
-            }
-        }
-        else if (OTHelpers.isFunction(object)) {
-            try {
-                components.push(object.toString());
-            } catch(e) {
-                components.push('function()');
-            }
-        }
-        else  {
-            components.push(object.toString());
-        }
-
-        return components.join(", ");
+      var components = [];
+
+      if (typeof(object) === 'undefined') {
+        // noop
+      }
+      else if (object === null) {
+        components.push('NULL');
+      }
+      else if (OTHelpers.isArray(object)) {
+        for (var i=0; i<object.length; ++i) {
+          components.push(toJson(object[i]));
+        }
+      }
+      else if (OTHelpers.isObject(object)) {
+        for (var key in object) {
+          var stringValue;
+
+          if (!OTHelpers.isFunction(object[key])) {
+            stringValue = toJson(object[key]);
+          }
+          else if (object.hasOwnProperty(key)) {
+            stringValue = 'function ' + key + '()';
+          }
+
+          components.push(key + ': ' + stringValue);
+        }
+      }
+      else if (OTHelpers.isFunction(object)) {
+        try {
+          components.push(object.toString());
+        } catch(e) {
+          components.push('function()');
+        }
+      }
+      else  {
+        components.push(object.toString());
+      }
+
+      return components.join(', ');
     }
 
     // Append +args+ to logs, along with the current log level and the a date stamp.
     function appendToLogs(level, args) {
-        if (!args) return;
-
-        var message = toDebugString(args);
-        if (message.length <= 2) return;
-
-        _logs.push(
-            [level, formatDateStamp(), message]
-        );
-    }
-};
-
-OTHelpers.useLogHelpers(OTHelpers);
-OTHelpers.setLogLevel(OTHelpers.ERROR);
+      if (!args) return;
+
+      var message = toDebugString(args);
+      if (message.length <= 2) return;
+
+      _logs.push(
+        [level, formatDateStamp(), message]
+      );
+    }
+  };
+
+  OTHelpers.useLogHelpers(OTHelpers);
+  OTHelpers.setLogLevel(OTHelpers.ERROR);
 
 })(window, window.OTHelpers);
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
 // DOM helpers
 (function(window, OTHelpers, undefined) {
 
-    // Helper function for adding event listeners to dom elements.
-    // WARNING: This doesn't preserve event types, your handler could
-    // be getting all kinds of different parameters depending on the browser.
-    // You also may have different scopes depending on the browser and bubbling
-    // and cancelable are not supported.
-    OTHelpers.on = function(element, eventName,  handler) {
-        if (element.addEventListener) {
-            element.addEventListener(eventName, handler, false);
-        } else if (element.attachEvent) {
-            element.attachEvent("on" + eventName, handler);
-        } else {
-            var oldHandler = element["on"+eventName];
-            element["on"+eventName] = function() {
-              handler.apply(this, arguments);
-              if (oldHandler) oldHandler.apply(this, arguments);
-            };
-        }
-        return element;
-    };
-
-    // Helper function for removing event listeners from dom elements.
-    OTHelpers.off = function(element, eventName, handler) {
-        if (element.removeEventListener) {
-            element.removeEventListener (eventName, handler,false);
-        }
-        else if (element.detachEvent) {
-            element.detachEvent("on" + eventName, handler);
-        }
-    };
+  // Helper function for adding event listeners to dom elements.
+  // WARNING: This doesn't preserve event types, your handler could
+  // be getting all kinds of different parameters depending on the browser.
+  // You also may have different scopes depending on the browser and bubbling
+  // and cancelable are not supported.
+  OTHelpers.on = function(element, eventName,  handler) {
+    if (element.addEventListener) {
+      element.addEventListener(eventName, handler, false);
+    } else if (element.attachEvent) {
+      element.attachEvent('on' + eventName, handler);
+    } else {
+      var oldHandler = element['on'+eventName];
+      element['on'+eventName] = function() {
+        handler.apply(this, arguments);
+        if (oldHandler) oldHandler.apply(this, arguments);
+      };
+    }
+    return element;
+  };
+
+  // Helper function for removing event listeners from dom elements.
+  OTHelpers.off = function(element, eventName, handler) {
+    if (element.removeEventListener) {
+      element.removeEventListener (eventName, handler,false);
+    }
+    else if (element.detachEvent) {
+      element.detachEvent('on' + eventName, handler);
+    }
+  };
 
 })(window, window.OTHelpers);
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./dom_events.js')
 
 (function(window, OTHelpers, undefined) {
 
-  var _domReady = typeof document === 'undefined' ||
-                  document.readyState === 'complete' ||
-                 (document.readyState === 'interactive' && document.body),
+  var _domReady = typeof(document) === 'undefined' ||
+                    document.readyState === 'complete' ||
+                   (document.readyState === 'interactive' && document.body),
 
       _loadCallbacks = [],
       _unloadCallbacks = [],
       _domUnloaded = false,
 
       onDomReady = function() {
         _domReady = true;
 
@@ -1135,17 +1129,17 @@ OTHelpers.setLogLevel(OTHelpers.ERROR);
 
   OTHelpers.isDOMUnloaded = function() {
     return _domUnloaded;
   };
 
 
   if (_domReady) {
     onDomReady();
-  } else if(typeof document !== 'undefined') {
+  } else if(typeof(document) !== 'undefined') {
     if (document.addEventListener) {
       document.addEventListener('DOMContentLoaded', onDomReady, false);
     } else if (document.attachEvent) {
       // This is so onLoad works in IE, primarily so we can show the upgrade to Chrome popup
       document.attachEvent('onreadystatechange', function() {
         if (document.readyState === 'complete') onDomReady();
       });
     }
@@ -1154,16 +1148,32 @@ OTHelpers.setLogLevel(OTHelpers.ERROR);
 })(window, window.OTHelpers);
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
 (function(window, OTHelpers, undefined) {
 
+  OTHelpers.castToBoolean = function(value, defaultValue) {
+    if (value === undefined) return defaultValue;
+    return value === 'true' || value === true;
+  };
+
+  OTHelpers.roundFloat = function(value, places) {
+    return Number(value.toFixed(places));
+  };
+
+})(window, window.OTHelpers);
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+(function(window, OTHelpers, undefined) {
+
   var capabilities = {};
 
   // Registers a new capability type and a function that will indicate
   // whether this client has that capability.
   //
   //   OTHelpers.registerCapability('bundle', function() {
   //     return OTHelpers.hasCapabilities('webrtc') &&
   //                (OTHelpers.browser() === 'Chrome' || TBPlugin.isInstalled());
@@ -1181,71 +1191,75 @@ OTHelpers.setLogLevel(OTHelpers.ERROR);
       OTHelpers.error('Attempted to register', name,
                               'capability with a callback that isn\' a function');
       return;
     }
 
     memoriseCapabilityTest(_name, callback);
   };
 
+
+  // Wrap up a capability test in a function that memorises the
+  // result.
+  var memoriseCapabilityTest = function (name, callback) {
+    capabilities[name] = function() {
+      var result = callback();
+      capabilities[name] = function() {
+        return result;
+      };
+
+      return result;
+    };
+  };
+
+  var testCapability = function (name) {
+    return capabilities[name]();
+  };
+
+
   // Returns true if all of the capability names passed in
   // exist and are met.
   //
   //  OTHelpers.hasCapabilities('bundle', 'rtcpMux')
   //
   OTHelpers.hasCapabilities = function(/* capability1, capability2, ..., capabilityN  */) {
     var capNames = Array.prototype.slice.call(arguments),
         name;
 
     for (var i=0; i<capNames.length; ++i) {
       name = capNames[i].toLowerCase();
 
       if (!capabilities.hasOwnProperty(name)) {
         OTHelpers.error('hasCapabilities was called with an unknown capability: ' + name);
         return false;
       }
-      else if (capabilities[name]() === false) {
+      else if (testCapability(name) === false) {
         return false;
       }
     }
 
     return true;
   };
 
-
-  // Wrap up a capability test in a function that memorises the
-  // result.
-  var memoriseCapabilityTest = function memoriseCapabilityTest(name, callback) {
-    capabilities[name] = function() {
-      var result = callback();
-      capabilities[name] = function() {
-        return result;
-      };
-
-      return result;
-    };
-  };
-
 })(window, window.OTHelpers);
+
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
+// tb_require('./capabilities.js')
 
 (function(window, OTHelpers, undefined) {
 
-OTHelpers.castToBoolean = function(value, defaultValue) {
-    if (value === undefined) return defaultValue;
-    return value === 'true' || value === true;
-};
-
-OTHelpers.roundFloat = function(value, places) {
-    return Number(value.toFixed(places));
-};
+  // Indicates if the client supports WebSockets.
+  OTHelpers.registerCapability('websockets', function() {
+    return 'WebSocket' in window;
+  });
 
 })(window, window.OTHelpers);
+
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('../vendor/uuid.js')
 // tb_require('./dom_events.js')
 
 (function(window, OTHelpers, undefined) {
 
@@ -1259,19 +1273,19 @@ OTHelpers.roundFloat = function(value, p
   var supportsPostMessage = (function () {
     if (window.postMessage) {
       // Check to see if postMessage fires synchronously,
       // if it does, then the implementation of postMessage
       // is broken.
       var postMessageIsAsynchronous = true;
       var oldOnMessage = window.onmessage;
       window.onmessage = function() {
-          postMessageIsAsynchronous = false;
-      };
-      window.postMessage("", "*");
+        postMessageIsAsynchronous = false;
+      };
+      window.postMessage('', '*');
       window.onmessage = oldOnMessage;
       return postMessageIsAsynchronous;
     }
   })();
 
   if (supportsPostMessage) {
     var timeouts = [],
         messageName = 'OTHelpers.' + OTHelpers.uuid.v4() + '.zero-timeout';
@@ -1953,281 +1967,295 @@ OTHelpers.roundFloat = function(value, p
       };
 
       this.isDefaultPrevented = function() {
         return _defaultPrevented;
       };
     };
 
   };
-  
+
 })(window, window.OTHelpers);
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./callbacks.js')
 
 // DOM helpers
 (function(window, OTHelpers, undefined) {
 
-OTHelpers.isElementNode = function(node) {
-    return node && typeof node === 'object' && node.nodeType == 1;
-};
-
-// Returns true if the client supports element.classList
-OTHelpers.supportsClassList = function() {
-    var hasSupport = typeof(document !== "undefined") && ("classList" in document.createElement("a"));
+  OTHelpers.isElementNode = function(node) {
+    return node && typeof node === 'object' && node.nodeType === 1;
+  };
+
+  // Returns true if the client supports element.classList
+  OTHelpers.supportsClassList = function() {
+    var hasSupport = (typeof document !== 'undefined') &&
+            ('classList' in document.createElement('a'));
+
     OTHelpers.supportsClassList = function() { return hasSupport; };
 
     return hasSupport;
-};
-
-OTHelpers.removeElement = function(element) {
+  };
+
+  OTHelpers.removeElement = function(element) {
     if (element && element.parentNode) {
-        element.parentNode.removeChild(element);
-    }
-};
-
-OTHelpers.removeElementById = function(elementId) {
+      element.parentNode.removeChild(element);
+    }
+  };
+
+  OTHelpers.removeElementById = function(elementId) {
+    /*jshint newcap:false */
     this.removeElement(OTHelpers(elementId));
-};
-
-OTHelpers.removeElementsByType = function(parentElem, type) {
+  };
+
+  OTHelpers.removeElementsByType = function(parentElem, type) {
     if (!parentElem) return;
 
     var elements = parentElem.getElementsByTagName(type);
 
     // elements is a "live" NodesList collection. Meaning that the collection
     // itself will be mutated as we remove elements from the DOM. This means
     // that "while there are still elements" is safer than "iterate over each
     // element" as the collection length and the elements indices will be modified
     // with each iteration.
     while (elements.length) {
-        parentElem.removeChild(elements[0]);
-    }
-};
-
-OTHelpers.emptyElement = function(element) {
+      parentElem.removeChild(elements[0]);
+    }
+  };
+
+  OTHelpers.emptyElement = function(element) {
     while (element.firstChild) {
-        element.removeChild(element.firstChild);
+      element.removeChild(element.firstChild);
     }
     return element;
-};
-
-OTHelpers.createElement = function(nodeName, attributes, children, doc) {
+  };
+
+  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]);
-            }
-        }
+      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(typeof child === 'string') {
+        element.innerHTML = element.innerHTML + child;
+      } else {
+        element.appendChild(child);
+      }
     };
 
     if(OTHelpers.isArray(children)) {
-        OTHelpers.forEach(children, setChildren);
+      OTHelpers.forEach(children, setChildren);
     } else if(children) {
-        setChildren(children);
+      setChildren(children);
     }
 
     return element;
-};
-
-OTHelpers.createButton = function(innerHTML, attributes, events) {
+  };
+
+  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;
+      for (var name in events) {
+        if (events.hasOwnProperty(name)) {
+          OTHelpers.on(button, name, events[name]);
+        }
+      }
+
+      button._boundEvents = events;
     }
 
     return button;
-};
-
-
-// Detects when an element is not part of the document flow because it or one of it's ancesters has display:none.
-OTHelpers.isDisplayNone = function(element) {
-    if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return true;
-    if (element.parentNode && element.parentNode.style) return OTHelpers.isDisplayNone(element.parentNode);
+  };
+
+
+  // Detects when an element is not part of the document flow because
+  // it or one of it's ancesters has display:none.
+  OTHelpers.isDisplayNone = function(element) {
+    if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
+                OTHelpers.css(element, 'display') === 'none') return true;
+
+    if (element.parentNode && element.parentNode.style) {
+      return OTHelpers.isDisplayNone(element.parentNode);
+    }
+
     return false;
-};
-
-OTHelpers.findElementWithDisplayNone = function(element) {
-    if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return element;
-    if (element.parentNode && element.parentNode.style) return OTHelpers.findElementWithDisplayNone(element.parentNode);
+  };
+
+  OTHelpers.findElementWithDisplayNone = function(element) {
+    if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
+              OTHelpers.css(element, 'display') === 'none') return element;
+
+    if (element.parentNode && element.parentNode.style) {
+      return OTHelpers.findElementWithDisplayNone(element.parentNode);
+    }
+
     return null;
-};
-
-function objectHasProperties(obj) {
+  };
+
+  function objectHasProperties(obj) {
     for (var key in obj) {
-        if (obj.hasOwnProperty(key)) return true;
+      if (obj.hasOwnProperty(key)) return true;
     }
     return false;
-}
-
-
-// Allows an +onChange+ callback to be triggered when specific style properties
-// of +element+ are notified. The callback accepts a single parameter, which is
-// a hash where the keys are the style property that changed and the values are
-// an array containing the old and new values ([oldValue, newValue]).
-//
-// Width and Height changes while the element is display: none will not be
-// fired until such time as the element becomes visible again.
-//
-// This function returns the MutationObserver itself. Once you no longer wish
-// to observe the element you should call disconnect on the observer.
-//
-// Observing changes:
-//  // observe changings to the width and height of object
-//  dimensionsObserver = OTHelpers.observeStyleChanges(object, ['width', 'height'], function(changeSet) {
-//      OT.debug("The new width and height are " + changeSet.width[1] + ',' + changeSet.height[1]);
-//  });
-//
-// Cleaning up
-//  // stop observing changes
-//  dimensionsObserver.disconnect();
-//  dimensionsObserver = null;
-//
-OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
+  }
+
+
+  // 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);
-            }
-        };
+      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);
+      oldStyles[style] = getStyle(style);
     });
 
     var observer = new MutationObserver(function(mutations) {
-        var changeSet = {};
-
-        OTHelpers.forEach(mutations, function(mutation) {
-            if (mutation.attributeName !== 'style') return;
-
-            var isHidden = OTHelpers.isDisplayNone(element);
-
-            OTHelpers.forEach(stylesToObserve, function(style) {
-                if(isHidden && (style == 'width' || style == 'height')) return;
-
-                var newValue = getStyle(style);
-
-                if (newValue !== oldStyles[style]) {
-                    // OT.debug("CHANGED " + style + ": " + oldStyles[style] + " -> " + newValue);
-
-                    changeSet[style] = [oldStyles[style], newValue];
-                    oldStyles[style] = newValue;
-                }
-            });
+      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);
-            });
-        }
+      });
+
+      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
+      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) {
+  };
+
+
+  // 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));
-            }
+      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);
         });
-
-        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
+      attributes:false,
+      childList:true,
+      characterData:false,
+      subtree:true
     });
 
     return observer;
-};
+  };
 
 })(window, window.OTHelpers);
 
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
@@ -2269,49 +2297,78 @@ OTHelpers.observeNodeOrChildNodeRemoval 
       // but we just make the background of the iframe completely transparent.
       domElement.style.backgroundColor = 'transparent';
       domElement.setAttribute('allowTransparency', 'true');
     }
 
     domElement.scrolling = 'no';
     domElement.setAttribute('scrolling', 'no');
 
+    // This is necessary for IE, as it will not inherit it's doctype from
+    // the parent frame.
+    var frameContent = '<!DOCTYPE html><html><head>' +
+                      '<meta http-equiv="x-ua-compatible" content="IE=Edge">' +
+                      '<meta http-equiv="Content-type" content="text/html; charset=utf-8">' +
+                      '<title></title></head><body></body></html>';
+
     var wrappedCallback = function() {
       var doc = domElement.contentDocument || domElement.contentWindow.document;
-      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();
+        }
+      }
+
       callback(
         domElement.contentWindow,
         doc
       );
     };
 
     document.body.appendChild(domElement);
-    
+
     if(OTHelpers.browserVersion().iframeNeedsLoad) {
+      if (OTHelpers.browser() === 'IE') {
+        // This works around some issues with IE and document.write.
+        // Basically this works by slightly abusing the bookmarklet/scriptlet
+        // functionality that all browsers support.
+        domElement.contentWindow.contents = frameContent;
+        /*jshint scripturl:true*/
+        domElement.src = 'javascript:window["contents"]';
+        /*jshint scripturl:false*/
+      }
+
       OTHelpers.on(domElement, 'load', wrappedCallback);
     } else {
       setTimeout(wrappedCallback);
     }
 
     this.close = function() {
       OTHelpers.removeElement(domElement);
       this.trigger('closed');
       this.element = domElement = null;
       return this;
     };
 
     this.element = domElement;
 
   };
-  
+
 })(window, window.OTHelpers);
 
 /*
- * getComputedStyle from 
+ * getComputedStyle from
  * https://github.com/jonathantneal/Polyfills-for-IE8/blob/master/getComputedStyle.js
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
 
 /*jshint strict: false, eqnull: true, browser:true, smarttabs:true*/
 
 (function(window, OTHelpers, undefined) {
@@ -2413,151 +2470,151 @@ OTHelpers.observeNodeOrChildNodeRemoval 
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
 
 (function(window, OTHelpers, undefined) {
 
-OTHelpers.addClass = function(element, value) {
+  OTHelpers.addClass = function(element, value) {
     // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
     if (element.nodeType !== 1) {
-        return;
+      return;
     }
 
     var classNames = OTHelpers.trim(value).split(/\s+/),
         i, l;
 
     if (OTHelpers.supportsClassList()) {
-        for (i=0, l=classNames.length; i<l; ++i) {
-            element.classList.add(classNames[i]);
-        }
-
-        return;
+      for (i=0, l=classNames.length; i<l; ++i) {
+        element.classList.add(classNames[i]);
+      }
+
+      return;
     }
 
     // Here's our fallback to browsers that don't support element.classList
 
     if (!element.className && classNames.length === 1) {
-        element.className = value;
+      element.className = value;
     }
     else {
-        var setClass = " " + element.className + " ";
-
-        for (i=0, l=classNames.length; i<l; ++i) {
-            if ( !~setClass.indexOf( " " + classNames[i] + " ")) {
-                setClass += classNames[i] + " ";
-            }
-        }
-
-        element.className = OTHelpers.trim(setClass);
+      var setClass = ' ' + element.className + ' ';
+
+      for (i=0, l=classNames.length; i<l; ++i) {
+        if ( !~setClass.indexOf( ' ' + classNames[i] + ' ')) {
+          setClass += classNames[i] + ' ';
+        }
+      }
+
+      element.className = OTHelpers.trim(setClass);
     }
 
     return this;
-};
-
-OTHelpers.removeClass = function(element, value) {
+  };
+
+  OTHelpers.removeClass = function(element, value) {
     if (!value) return;
 
     // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
     if (element.nodeType !== 1) {
-        return;
+      return;
     }
 
     var newClasses = OTHelpers.trim(value).split(/\s+/),
         i, l;
 
     if (OTHelpers.supportsClassList()) {
-        for (i=0, l=newClasses.length; i<l; ++i) {
-            element.classList.remove(newClasses[i]);
-        }
-
-        return;
-    }
-
-    var className = (" " + element.className + " ").replace(/[\s+]/, ' ');
+      for (i=0, l=newClasses.length; i<l; ++i) {
+        element.classList.remove(newClasses[i]);
+      }
+
+      return;
+    }
+
+    var className = (' ' + element.className + ' ').replace(/[\s+]/, ' ');
 
     for (i=0,l=newClasses.length; i<l; ++i) {
-        className = className.replace(' ' + newClasses[i] + ' ', ' ');
+      className = className.replace(' ' + newClasses[i] + ' ', ' ');
     }
 
     element.className = OTHelpers.trim(className);
 
     return this;
-};
-
-
-/**
- * Methods to calculate element widths and heights.
- */
-
-var _width = function(element) {
+  };
+
+
+  /**
+   * Methods to calculate element widths and heights.
+   */
+
+  var _width = function(element) {
         if (element.offsetWidth > 0) {
-            return element.offsetWidth + 'px';
+          return element.offsetWidth + 'px';
         }
 
         return OTHelpers.css(element, 'width');
-    },
-
-    _height = function(element) {
+      },
+
+      _height = function(element) {
         if (element.offsetHeight > 0) {
-            return element.offsetHeight + 'px';
+          return element.offsetHeight + 'px';
         }
 
         return OTHelpers.css(element, 'height');
-    };
-
-OTHelpers.width = function(element, newWidth) {
+      };
+
+  OTHelpers.width = function(element, newWidth) {
     if (newWidth) {
-        OTHelpers.css(element, 'width', newWidth);
-        return this;
+      OTHelpers.css(element, 'width', newWidth);
+      return this;
     }
     else {
-        if (OTHelpers.isDisplayNone(element)) {
-            // We can't get the width, probably since the element is hidden.
-            return OTHelpers.makeVisibleAndYield(element, function() {
-                return _width(element);
-            });
-        }
-        else {
-            return _width(element);
-        }
-    }
-};
-
-OTHelpers.height = function(element, newHeight) {
+      if (OTHelpers.isDisplayNone(element)) {
+        // We can't get the width, probably since the element is hidden.
+        return OTHelpers.makeVisibleAndYield(element, function() {
+          return _width(element);
+        });
+      }
+      else {
+        return _width(element);
+      }
+    }
+  };
+
+  OTHelpers.height = function(element, newHeight) {
     if (newHeight) {
-        OTHelpers.css(element, 'height', newHeight);
-        return this;
+      OTHelpers.css(element, 'height', newHeight);
+      return this;
     }
     else {
-        if (OTHelpers.isDisplayNone(element)) {
-            // We can't get the height, probably since the element is hidden.
-            return OTHelpers.makeVisibleAndYield(element, function() {
-                return _height(element);
-            });
-        }
-        else {
-            return _height(element);
-        }
-    }
-};
-
-// Centers +element+ within the window. You can pass through the width and height
-// if you know it, if you don't they will be calculated for you.
-OTHelpers.centerElement = function(element, width, height) {
+      if (OTHelpers.isDisplayNone(element)) {
+        // We can't get the height, probably since the element is hidden.
+        return OTHelpers.makeVisibleAndYield(element, function() {
+          return _height(element);
+        });
+      }
+      else {
+        return _height(element);
+      }
+    }
+  };
+
+  // Centers +element+ within the window. You can pass through the width and height
+  // if you know it, if you don't they will be calculated for you.
+  OTHelpers.centerElement = function(element, width, height) {
     if (!width) width = parseInt(OTHelpers.width(element), 10);
     if (!height) height = parseInt(OTHelpers.height(element), 10);
 
-    var marginLeft = -0.5 * width + "px";
-    var marginTop = -0.5 * height + "px";
-    OTHelpers.css(element, "margin", marginTop + " 0 0 " + marginLeft);
-    OTHelpers.addClass(element, "OT_centered");
-};
+    var marginLeft = -0.5 * width + 'px';
+    var marginTop = -0.5 * height + 'px';
+    OTHelpers.css(element, 'margin', marginTop + ' 0 0 ' + marginLeft);
+    OTHelpers.addClass(element, 'OT_centered');
+  };
 
 })(window, window.OTHelpers);
 
 // CSS helpers helpers
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
@@ -2565,37 +2622,37 @@ OTHelpers.centerElement = function(eleme
 // tb_require('./getcomputedstyle.js')
 
 (function(window, OTHelpers, undefined) {
 
   var displayStateCache = {},
       defaultDisplays = {};
 
   var defaultDisplayValueForElement = function(element) {
-      if (defaultDisplays[element.ownerDocument] &&
-        defaultDisplays[element.ownerDocument][element.nodeName]) {
-        return defaultDisplays[element.ownerDocument][element.nodeName];
-      }
-
-      if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
-    
-      // We need to know what display value to use for this node. The easiest way
-      // is to actually create a node and read it out.
-      var testNode = element.ownerDocument.createElement(element.nodeName),
-          defaultDisplay;
-
-      element.ownerDocument.body.appendChild(testNode);
-      defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] =
-        OTHelpers.css(testNode, 'display');
-
-      OTHelpers.removeElement(testNode);
-      testNode = null;
-
-      return defaultDisplay;
-    };
+    if (defaultDisplays[element.ownerDocument] &&
+      defaultDisplays[element.ownerDocument][element.nodeName]) {
+      return defaultDisplays[element.ownerDocument][element.nodeName];
+    }
+
+    if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
+
+    // We need to know what display value to use for this node. The easiest way
+    // is to actually create a node and read it out.
+    var testNode = element.ownerDocument.createElement(element.nodeName),
+        defaultDisplay;
+
+    element.ownerDocument.body.appendChild(testNode);
+    defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] =
+      OTHelpers.css(testNode, 'display');
+
+    OTHelpers.removeElement(testNode);
+    testNode = null;
+
+    return defaultDisplay;
+  };
 
   var isHidden = function(element) {
     var computedStyle = OTHelpers.getComputedStyle(element);
     return computedStyle.getPropertyValue('display') === 'none';
   };
 
   OTHelpers.show = function(element) {
     var display = element.style.display;
@@ -2625,17 +2682,19 @@ OTHelpers.centerElement = function(eleme
     return this;
   };
 
   OTHelpers.css = function(element, nameOrHash, value) {
     if (typeof(nameOrHash) !== 'string') {
       var style = element.style;
 
       for (var cssName in nameOrHash) {
-        style[cssName] = nameOrHash[cssName];
+        if (nameOrHash.hasOwnProperty(cssName)) {
+          style[cssName] = nameOrHash[cssName];
+        }
       }
 
       return this;
 
     } else if (value !== undefined) {
       element.style[nameOrHash] = value;
       return this;
 
@@ -2682,17 +2741,17 @@ OTHelpers.centerElement = function(eleme
       if (styles.hasOwnProperty(name)) {
         OTHelpers.css(element, name, oldStyles[name] || '');
       }
     }
 
     return ret;
   };
 
-// Make +element+ visible while executing +callback+.
+  // Make +element+ visible while executing +callback+.
   OTHelpers.makeVisibleAndYield = function(element, callback) {
     // find whether it's the element or an ancester that's display none and
     // then apply to whichever it is
     var targetElement = OTHelpers.findElementWithDisplayNone(element);
     if (!targetElement) return;
 
     return OTHelpers.applyCSS(targetElement, {
       display: 'block',
@@ -2705,26 +2764,39 @@ OTHelpers.centerElement = function(eleme
 // AJAX helpers
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
 (function(window, OTHelpers, undefined) {
 
-  OTHelpers.requestAnimationFrame =
-    OTHelpers.bind(
-        window.requestAnimationFrame ||
-        window.mozRequestAnimationFrame ||
-        window.webkitRequestAnimationFrame ||
-        window.msRequestAnimationFrame ||
-        setTimeout, window);
-
+  var requestAnimationFrame = window.requestAnimationFrame ||
+                              window.mozRequestAnimationFrame ||
+                              window.webkitRequestAnimationFrame ||
+                              window.msRequestAnimationFrame;
+
+  if (requestAnimationFrame) {
+    requestAnimationFrame = OTHelpers.bind(requestAnimationFrame, window);
+  }
+  else {
+    var lastTime = 0;
+    var startTime = OTHelpers.now();
+
+    requestAnimationFrame = function(callback){
+      var currTime = OTHelpers.now();
+      var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+      var id = window.setTimeout(function() { callback(currTime - startTime); }, timeToCall);
+      lastTime = currTime + timeToCall;
+      return id;
+    };
+  }
+
+  OTHelpers.requestAnimationFrame = requestAnimationFrame;
 })(window, window.OTHelpers);
-
 // AJAX helpers
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
 (function(window, OTHelpers, undefined) {
 
@@ -2942,17 +3014,18 @@ OTHelpers.centerElement = function(eleme
       OT.debug('Known issues: ' + OT.properties.websiteURL +
         '/opentok/webrtc/docs/js/release-notes.html#knownIssues');
       _debugHeaderLogged = true;
     }
     OT.debug('OT.setLogLevel(' + retVal + ')');
     return retVal;
   };
 
-  OT.setLogLevel(OT.properties.debug ? OT.DEBUG : OT.ERROR);
+  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;
   };
 
   /**
@@ -3066,20 +3139,19 @@ OTHelpers.centerElement = function(eleme
     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() <= 8) {
+    if (OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 8) {
       // Fix for IE8 not triggering the change event
       checkbox.on('click', function() {
-        console.log('CLICK');
         checkbox.blur();
         checkbox.focus();
       });
     }
 
     checkbox.setAttribute('name', nameAndId);
     checkbox.setAttribute('id', nameAndId);
     checkbox.setAttribute('type', 'checkbox');
@@ -3098,21 +3170,22 @@ OTHelpers.centerElement = function(eleme
   OT.Dialogs.AllowDeny = {
     Chrome: {},
     Firefox: {}
   };
 
   OT.Dialogs.AllowDeny.Chrome.initialPrompt = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           close, root;
 
       close = el('OT_closeButton', '&times;')
         .on('click', function() {
+          modal.trigger('closeButtonClicked');
           modal.close();
         });
 
       root = el('OT_root OT_dialog OT_dialog-allow-deny-chrome-first', [
         close,
         el('OT_dialog-messages', [
           el('OT_dialog-messages-main', 'Allow camera and mic access'),
           el('OT_dialog-messages-minor', 'Click the Allow button in the upper-right corner ' +
@@ -3127,22 +3200,23 @@ OTHelpers.centerElement = function(eleme
 
     });
     return modal;
   };
 
   OT.Dialogs.AllowDeny.Chrome.previouslyDenied = function(website) {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           close,
           root;
 
       close = el('OT_closeButton', '&times;')
         .on('click', function() {
+          modal.trigger('closeButtonClicked');
           modal.close();
         });
 
       root = el('OT_root OT_dialog OT_dialog-allow-deny-chrome-pre-denied', [
         close,
         el('OT_dialog-messages', [
           el('OT_dialog-messages-main', 'Allow camera and mic access'),
           el('OT_dialog-messages-minor', [
@@ -3175,17 +3249,17 @@ OTHelpers.centerElement = function(eleme
 
     });
     return modal;
   };
 
   OT.Dialogs.AllowDeny.Chrome.deniedNow = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           root;
 
       root = el('OT_root OT_dialog-blackout',
         el('OT_dialog OT_dialog-allow-deny-chrome-now-denied', [
           el('OT_dialog-messages', [
             el('OT_dialog-messages-main ',
               el('OT_dialog-allow-camera-icon')
             ),
@@ -3202,22 +3276,23 @@ OTHelpers.centerElement = function(eleme
 
     });
     return modal;
   };
 
   OT.Dialogs.AllowDeny.Firefox.maybeDenied = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           close,
           root;
 
       close = el('OT_closeButton', '&times;')
         .on('click', function() {
+          modal.trigger('closeButtonClicked');
           modal.close();
         });
 
       root = el('OT_root OT_dialog OT_dialog-allow-deny-firefox-maybe-denied', [
         close,
         el('OT_dialog-messages', [
           el('OT_dialog-messages-main', 'Please allow camera & mic access'),
           el('OT_dialog-messages-minor', [
@@ -3249,18 +3324,18 @@ OTHelpers.centerElement = function(eleme
 
     });
     return modal;
   };
 
   OT.Dialogs.AllowDeny.Firefox.denied = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
-          btn = templateElement.bind(document, 'OT_dialog-button OT_dialog-button-large'),
+      var el = OT.$.bind(templateElement, document),
+          btn = OT.$.bind(templateElement, document, 'OT_dialog-button OT_dialog-button-large'),
           root,
           refreshButton;
 
       refreshButton = btn('Reload')
         .on('click', function() {
           modal.trigger('refresh');
         });
 
@@ -3286,97 +3361,126 @@ OTHelpers.centerElement = function(eleme
   };
 
   OT.Dialogs.Plugin = {};
 
   OT.Dialogs.Plugin.promptToInstall = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
       var el = OT.$.bind(templateElement, document),
-          btn = OT.$.bind(templateElement, document,
-                    'OT_dialog-button OT_dialog-button-large OT_dialog-button-disabled'),
-          downloadButton = btn('Download OpenTok'),
-          refreshButton = btn('Refresh page'),
+          btn = function(children, size) {
+            var classes = 'OT_dialog-button ' +
+                          (size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large'),
+                b = el(classes, children);
+
+            b.enable = function() {
+              OT.$.removeClass(this, 'OT_dialog-button-disabled');
+              return this;
+            };
+
+            b.disable = function() {
+              OT.$.addClass(this, 'OT_dialog-button-disabled');
+              return this;
+            };
+
+            return b;
+          },
+          downloadButton = btn('Download plugin'),
+          cancelButton = btn('cancel', 'small'),
+          refreshButton = btn('Refresh browser'),
           acceptEULA,
           checkbox,
           close,
           root;
 
       function onDownload() {
         modal.trigger('download');
+        setTimeout(function() {
+          root.querySelector('.OT_dialog-messages-main').innerHTML =
+                                              'Plugin installation successful';
+          var sections = root.querySelectorAll('.OT_dialog-single-button-wide');
+          OT.$.addClass(sections[0], 'OT_dialog-hidden');
+          OT.$.removeClass(sections[1], 'OT_dialog-hidden');
+        }, 3000);
       }
 
       function onRefresh() {
         modal.trigger('refresh');
       }
 
       function onToggleEULA() {
         if (checkbox.checked) {
           enableButtons();
         }
         else {
           disableButtons();
         }
       }
 
       function enableButtons() {
-        OT.$.removeClass(downloadButton, 'OT_dialog-button-disabled');
+        downloadButton.enable();
         downloadButton.on('click', onDownload);
 
-        OT.$.removeClass(refreshButton, 'OT_dialog-button-disabled');
+        refreshButton.enable();
         refreshButton.on('click', onRefresh);
       }
 
       function disableButtons() {
-        OT.$.addClass(downloadButton, 'OT_dialog-button-disabled');
+        downloadButton.disable();
         downloadButton.off('click', onDownload);
 
-        OT.$.addClass(refreshButton, 'OT_dialog-button-disabled');
+        refreshButton.disable();
         refreshButton.off('click', onRefresh);
       }
 
+      downloadButton.disable();
+      refreshButton.disable();
+
+      cancelButton.on('click', function() {
+        modal.trigger('cancelButtonClicked');
+        modal.close();
+      });
 
       close = el('OT_closeButton', '&times;')
         .on('click', function() {
+          modal.trigger('closeButtonClicked');
           modal.close();
         });
 
       acceptEULA = linkElement.call(document,
-                                    'End-user license agreement',
+                                    'end-user license agreement',
                                     'http://tokbox.com/support/ie-eula');
 
       checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA);
 
       root = el('OT_root OT_dialog OT_dialog-plugin-prompt', [
         close,
         el('OT_dialog-messages', [
-          el('OT_dialog-messages-main', 'This app requires real-time communication'),
-          el('OT_dialog-messages-minor', 'These 2 simple steps will ' +
-            'enable real-time communications in Internet Explorer:')
+          el('OT_dialog-messages-main', 'This app requires real-time communication')
         ]),
-        el('OT_dialog-button-pair', [
-          el('OT_dialog-button-with-title', [
+        el('OT_dialog-single-button-wide', [
+          el('OT_dialog-single-button-with-title', [
             el('OT_dialog-button-title', [
-              el('', 'Step 1', 'strong'),
               checkbox,
               (function() {
-                var x = el('', 'Accept', 'label');
+                var x = el('', 'accept', 'label');
                 x.setAttribute('for', checkbox.id);
                 x.style.margin = '0 5px';
                 return x;
               })(),
               acceptEULA
             ]),
-            downloadButton
-          ]),
-          el('OT_dialog-button-pair-seperator', ''),
-          el('OT_dialog-button-with-title', [
+            downloadButton,
+            cancelButton
+          ])
+        ]),
+        el('OT_dialog-single-button-wide OT_dialog-hidden', [
+          el('OT_dialog-single-button-with-title', [
             el('OT_dialog-button-title', [
-              el('', 'Step 2', 'strong'),
-              'Reload this page after installation'
+              'You can now enjoy webRTC enabled video via Internet Explorer.'
             ]),
             refreshButton
           ])
         ])
       ]);
 
       addDialogCSS(document, [], function() {
         document.body.appendChild(root);
@@ -3384,29 +3488,30 @@ OTHelpers.centerElement = function(eleme
 
     });
     return modal;
   };
 
   OT.Dialogs.Plugin.promptToReinstall = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           close,
           okayButton,
           root;
 
       close = el('OT_closeButton', '&times;');
       okayButton = el('OT_dialog-button', 'Okay');
 
       OT.$.on(okayButton, 'click', function() {
         modal.trigger('okay');
       });
 
       OT.$.on(close, 'click', function() {
+        modal.trigger('closeButtonClicked');
         modal.close();
       });
 
       root = el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [
         close,
         el('OT_dialog-messages', [
           el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'),
           el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin again to ' +
@@ -3427,17 +3532,17 @@ OTHelpers.centerElement = function(eleme
   OT.Dialogs.Plugin.updateInProgress = function() {
 
     var progressBar,
         progressText,
         progressValue = 0;
 
     var modal = new OT.$.Modal(function(window, document) {
 
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           root;
 
       progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong');
 
       progressBar = el('OT_dialog-progress-bar-fill');
 
       root = el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [
         el('OT_dialog-messages', [
@@ -3475,17 +3580,17 @@ OTHelpers.centerElement = function(eleme
       }
     };
 
     return modal;
   };
 
   OT.Dialogs.Plugin.updateComplete = function(error) {
     var modal = new OT.$.Modal(function(window, document) {
-      var el = templateElement.bind(document),
+      var el = OT.$.bind(templateElement, document),
           reloadButton,
           root;
 
       reloadButton = el('OT_dialog-button', 'Reload').on('click', function() {
         modal.trigger('reload');
       });
 
       var msgs;
@@ -3552,16 +3657,22 @@ OTHelpers.centerElement = function(eleme
     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() {
 
@@ -3695,24 +3806,24 @@ OTHelpers.centerElement = function(eleme
 
     OT.$.eventing(_this);
 
     return _this;
   })();
 
 })(window);
 /**
- * @license  TB Plugin 0.4.0.7 9425efe HEAD
+ * @license  TB Plugin 0.4.0.8 72b534e HEAD
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: August 05 08:56:57 2014
+ * Date: September 08 10:17:49 2014
  *
  */
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: false,
           trailing: true, browser: true, smarttabs:true */
 /* global scope:true, OT:true */
 /* exported TBPlugin */
 
@@ -3937,54 +4048,74 @@ var PluginObject = function PluginObject
 
     return this;
   };
 
   this.isValid = function() {
     return _plugin.valid;
   };
 
-  if (_plugin.attachEvent) {
-    this.on = function (name, callback) {
-      _plugin.attachEvent('on'+name, callback);
-      return this;
-    };
-  } else {
-    this.on = function (name, callback) {
-      _plugin.addEventListener(name, callback, false);
-      return this;
-    };
-  }
-
-  // Firebreath mistakenly adds detachEvent in IE11, so
-  // we'll look on window instead
-  if (window.detachEvent) {
-    this.off = function (name, callback) {
-      _plugin.detachEvent('on'+name, callback);
-      return this;
-    };
-  }
-  else {
-    this.off = function (name, callback) {
-      _plugin.removeEventListener(name, callback);
-      return this;
-    };
-  }
-
-  this.once = function (name, callback) {
-    var fn = OT.$.bind(function () {
-      this.off(name, fn);
-      return callback.apply(null, arguments);
-    }, this);
-
-    this.on(name, fn);
+  // Event Handling Mechanisms
+
+  var eventHandlers = {};
+
+  var onCustomEvent = OT.$.bind(curryCallAsync(function onCustomEvent() {
+    var args = Array.prototype.slice.call(arguments),
+        name = args.shift();
+
+    if (!eventHandlers.hasOwnProperty(name) && eventHandlers[name].length) {
+      return;
+    }
+
+    OT.$.forEach(eventHandlers[name], function(handler) {
+      handler[0].apply(handler[1], args);
+    });
+  }), this);
+
+
+  this.on = function (name, callback, context) {
+    if (!eventHandlers.hasOwnProperty(name)) {
+      eventHandlers[name] = [];
+    }
+
+    eventHandlers[name].push([callback, context]);
     return this;
   };
 
+  this.off = function (name, callback, context) {
+    if (!eventHandlers.hasOwnProperty(name) ||
+        eventHandlers[name].length === 0) {
+      return;
+    }
+
+    OT.$.filter(eventHandlers[name], function(listener) {
+      return listener[0] === callback &&
+              listener[1] === context;
+    });
+
+    return this;
+  };
+
+  this.once = function (name, callback, context) {
+    var fn = function () {
+      this.off(name, fn, this);
+      return callback.apply(context, arguments);
+    };
+
+    this.on(name, fn, this);
+    return this;
+  };
+
+
   this.onReady = function(readyCallback) {
+    if (_plugin.on) {
+      // If the plugin supports custom events we'll use them
+      _plugin.on(-1, {customEvent: curryCallAsync(onCustomEvent, this)});
+    }
+
     // Only the main plugin has an initialise method
     if (_plugin.initialise) {
       this.on('ready', OT.$.bind(curryCallAsync(readyCallback), this));
       _plugin.initialise();
     }
     else {
       readyCallback.call(null);
     }
@@ -3997,17 +4128,29 @@ var PluginObject = function PluginObject
 
     removeObjectFromDom(_plugin);
     _plugin = null;
   };
 
   this.setStream = function(stream, completion) {
     if (completion) {
       if (stream.hasVideo()) {
-        this.once('renderingStarted', completion);
+        // FIX ME renderingStarted currently doesn't first
+        // this.once('renderingStarted', completion);
+        var verifyStream = function() {
+          if (_plugin.videoWidth > 0) {
+            // This fires a little too soon.
+            setTimeout(completion, 200);
+          }
+          else {
+            setTimeout(verifyStream, 500);
+          }
+        };
+
+        setTimeout(verifyStream, 500);
       }
       else {
         // TODO Investigate whether there is a good way to detect
         // when the audio is ready. Does it even matter?
         completion();
       }
     }
     _plugin.setStream(stream);
@@ -4138,16 +4281,86 @@ var createPeerController = function crea
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./plugin_object.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
+/* global OT:true, debug:true */
+/* exported VideoContainer */
+
+var VideoContainer = function VideoContainer (plugin, stream) {
+  this.domElement = plugin._;
+  this.parentElement = plugin._.parentNode;
+
+  plugin.addRef(this);
+
+  this.appendTo = function (parentDomElement) {
+    if (parentDomElement && plugin._.parentNode !== parentDomElement) {
+      debug('VideoContainer appendTo', parentDomElement);
+      parentDomElement.appendChild(plugin._);
+      this.parentElement = parentDomElement;
+    }
+  };
+
+  this.show = function (completion) {
+    debug('VideoContainer show');
+    plugin._.removeAttribute('width');
+    plugin._.removeAttribute('height');
+    plugin.setStream(stream, completion);
+    OT.$.show(plugin._);
+  };
+
+  this.setWidth = function (width) {
+    debug('VideoContainer setWidth to ' + width);
+    plugin._.setAttribute('width', width);
+  };
+
+  this.setHeight = function (height) {
+    debug('VideoContainer setHeight to ' + height);
+    plugin._.setAttribute('height', height);
+  };
+
+  this.setVolume = function (value) {
+    // TODO
+    debug('VideoContainer setVolume not implemented: called with ' + value);
+  };
+
+  this.getVolume = function () {
+    // TODO
+    debug('VideoContainer getVolume not implemented');
+    return 0.5;
+  };
+
+  this.getImgData = function () {
+    return plugin._.getImgData('image/png');
+  };
+
+  this.getVideoWidth = function () {
+    return plugin._.videoWidth;
+  };
+
+  this.getVideoHeight = function () {
+    return plugin._.videoHeight;
+  };
+
+  this.destroy = function () {
+    plugin._.setStream(null);
+    plugin.removeRef(this);
+  };
+};
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./plugin_object.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
 /* global OT:true, TBPlugin:true, pluginInfo:true, ActiveXObject:true,
           injectObject:true, curryCallAsync:true */
 
 /* exported AutoUpdater:true */
 var AutoUpdater;
 
 (function() {
 
@@ -4361,86 +4574,16 @@ var AutoUpdater;
     return getInstalledVersion();
   };
 
 })();
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./plugin_object.js')
-
-/* jshint globalstrict: true, strict: false, undef: true, unused: true,
-          trailing: true, browser: true, smarttabs:true */
-/* global OT:true, debug:true */
-/* exported VideoContainer */
-
-var VideoContainer = function VideoContainer (plugin, stream) {
-  this.domElement = plugin._;
-  this.parentElement = plugin._.parentNode;
-
-  plugin.addRef(this);
-
-  this.appendTo = function (parentDomElement) {
-    if (parentDomElement && plugin._.parentNode !== parentDomElement) {
-      debug('VideoContainer appendTo', parentDomElement);
-      parentDomElement.appendChild(plugin._);
-      this.parentElement = parentDomElement;
-    }
-  };
-
-  this.show = function (completion) {
-    debug('VideoContainer show');
-    plugin._.removeAttribute('width');
-    plugin._.removeAttribute('height');
-    plugin.setStream(stream, completion);
-    OT.$.show(plugin._);
-  };
-
-  this.setWidth = function (width) {
-    debug('VideoContainer setWidth to ' + width);
-    plugin._.setAttribute('width', width);
-  };
-
-  this.setHeight = function (height) {
-    debug('VideoContainer setHeight to ' + height);
-    plugin._.setAttribute('height', height);
-  };
-
-  this.setVolume = function (value) {
-    // TODO
-    debug('VideoContainer setVolume not implemented: called with ' + value);
-  };
-
-  this.getVolume = function () {
-    // TODO
-    debug('VideoContainer getVolume not implemented');
-    return 0.5;
-  };
-
-  this.getImgData = function () {
-    return plugin._.getImgData('image/png');
-  };
-
-  this.getVideoWidth = function () {
-    return plugin._.videoWidth;
-  };
-
-  this.getVideoHeight = function () {
-    return plugin._.videoHeight;
-  };
-
-  this.destroy = function () {
-    plugin._.setStream(null);
-    plugin.removeRef(this);
-  };
-};
-
-// tb_require('./header.js')
-// tb_require('./shims.js')
-// tb_require('./plugin_object.js')
 // tb_require('./video_container.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT:true, VideoContainer:true */
 /* exported MediaStream */
 
 var MediaStreamTrack = function MediaStreamTrack (mediaStreamId, options, plugin) {
@@ -4672,118 +4815,16 @@ var MediaConstraints = function(userCons
 var RTCStatsReport = function (reports) {
   this.forEach = function (callback, context) {
     for (var id in reports) {
       callback.call(context, reports[id]);
     }
   };
 };
 
-
-/*
-Output from FF:
-
-RTCStatsReport {
-6XBq
-  Object { id="6XBq", timestamp=1393144895233.075, type="localcandidate", more...}
-
-A+xj
-  Object { id="A+xj", timestamp=1393144895233.075, type="localcandidate", more...}
-
-M1Yw
-  Object { id="M1Yw", timestamp=1393144895233.075, type="remotecandidate", more...}
-
-OMYS
-  Object { id="OMYS", timestamp=1393144895233.075, type="localcandidate", more...}
-
-UeDG
-  Object { id="UeDG", timestamp=1393144895233.075, type="remotecandidate", more...}
-
-dfHm
-  Object { id="dfHm", timestamp=1393144895233.075, type="localcandidate", more...}
-
-hCfu
-  Object { id="hCfu", timestamp=1393144895233.075, type="localcandidate", more...}
-
-i15H
-  Object { id="i15H", timestamp=1393144895233.075, type="localcandidate", more...}
-
-inbound_rtp_audio_1
-  Object { id="inbound_rtp_audio_1", timestamp=1393144895233.075, type="inboundrtp", more...}
-
-inbound_rtp_video_2
-  Object { id="inbound_rtp_video_2", timestamp=1393144895233.075, type="inboundrtp", more...}
-
-sHQ2
-  Object { id="sHQ2", timestamp=1393144895233.075, type="localcandidate", more...}
-
-xYfs
-  Object { id="xYfs", timestamp=1393144895233.075, type="localcandidate", more...}
-
-forEach
-  forEach()
-
-get
-  get()
-
-has
-  has()
-}
-
-
-
-
-inbound_rtp_audio_1
-  bytesReceived
-    670142
-
-  id
-    "inbound_rtp_audio_1"
-
-  isRemote
-    false
-
-  jitter
-    0
-
-  packetsReceived
-    7366
-
-  ssrc
-    "1709642421"
-
-  timestamp
-    1393144895233.075
-
-  type
-    "inboundrtp"
-
-
-sHQ2
-  candidateType
-    "serverreflexive"
-
-  componentId
-    "1393144747157231 (id=26...=T1==cGF: stream1/audio"
-
-  id
-    "sHQ2"
-
-  ipAddress
-    "216.38.134.120"
-
-  portNumber
-    58592
-
-  timestamp
-    1393144895233.075
-
-  type
-    "localcandidate"
-  */
 // 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 */
@@ -5011,19 +5052,19 @@ var PeerConnection = function PeerConnec
           MediaStream, pluginReady:true, mediaCaptureObject, plugins,
           createMediaCaptureController, createPeerController, removeAllObjects,
           AutoUpdater, PluginRumorSocket, MediaConstraints */
 
 
   /// Private Data
 
 var pluginInfo = {
-    mimeType: 'application/x-opentokie,version=0.4.0.7',
-    activeXName: 'TokBox.OpenTokIE.0.4.0.7',
-    version: '0.4.0.7'
+    mimeType: 'application/x-opentokie,version=0.4.0.8',
+    activeXName: 'TokBox.OpenTokIE.0.4.0.8',
+    version: '0.4.0.8'
   },
   _document = scope.document,
   readyCallbacks = [];
 
 var debug = function (message, object) {
   if (object) {
     scope.OT.info('TB Plugin - ' + message + ' => ', object);
   }
@@ -5407,17 +5448,18 @@ waitForDomReady();
         oldContainerStyles = {},
         dimensionsObserver,
         videoElement,
         videoObserver,
         posterContainer,
         loadingContainer,
         width,
         height,
-        loading = true;
+        loading = true,
+        audioOnly = false;
 
     if (properties) {
       width = properties.width;
       height = properties.height;
 
       if (width) {
         if (typeof(width) === 'number') {
           width = width + 'px';
@@ -5523,17 +5565,30 @@ waitForDomReady();
       }
 
       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;
       }
@@ -5617,16 +5672,28 @@ waitForDomReady();
           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;
@@ -5641,38 +5708,16 @@ waitForDomReady();
       }
     };
   };
 
 })(window);
 // Web OT Helpers
 !(function(window) {
 
-  /* global mozRTCPeerConnection */
-
-  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);
-    }
-  })();
-
   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() {
@@ -5738,16 +5783,188 @@ waitForDomReady();
     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',
@@ -5821,123 +6038,16 @@ waitForDomReady();
         continue;
       }
       if (constraints[key]) return false;
     }
 
     return true;
   };
 
-  // Returns true if the client supports Web RTC, false otherwise.
-  //
-  // Chrome Issues:
-  // * The explicit prototype.addStream check is because webkitRTCPeerConnection was
-  // partially implemented, but not functional, in Chrome 22.
-  //
-  // Firefox Issues:
-  // * No real support before Firefox 19
-  // * Firefox 19 has issues with generating Offers.
-  // * Firefox 20 doesn't interoperate with Chrome.
-  //
-  OT.$.supportsWebRTC = function() {
-    var _supportsWebRTC = false;
-
-    var browser = OT.$.browserVersion(),
-        minimumVersions = OT.properties.minimumVersion || {},
-        minimumVersion = minimumVersions[browser.browser.toLowerCase()];
-
-    if(minimumVersion && minimumVersion > browser.version) {
-      OT.debug('Support for', browser.browser, 'is disabled because we require',
-        minimumVersion, 'but this is', browser.version);
-      _supportsWebRTC = false;
-
-    } else if (navigator.webkitGetUserMedia) {
-      _supportsWebRTC = typeof(webkitRTCPeerConnection) === 'function' &&
-        !!webkitRTCPeerConnection.prototype.addStream;
-
-    } else if (navigator.mozGetUserMedia) {
-      if (typeof(mozRTCPeerConnection) === 'function' && browser.version > 20.0) {
-        try {
-          new mozRTCPeerConnection();
-          _supportsWebRTC = true;
-        } catch (err) {
-          _supportsWebRTC = false;
-        }
-      }
-    } else if (TBPlugin.isInstalled()) {
-      _supportsWebRTC = true;
-    }
-
-    OT.$.supportsWebRTC = function() {
-      return _supportsWebRTC;
-    };
-
-    return _supportsWebRTC;
-  };
-
-  // Returns a String representing the supported WebRTC crypto scheme. The possible
-  // values are SDES_SRTP, DTLS_SRTP, and NONE;
-  //
-  // Broadly:
-  // * Firefox only supports DTLS
-  // * Older versions of Chrome (<= 24) only support SDES
-  // * Newer versions of Chrome (>= 25) support DTLS and SDES
-  //
-  OT.$.supportedCryptoScheme = function() {
-    if (!OT.$.supportsWebRTC()) return 'NONE';
-
-    var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i);
-    return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP';
-  };
-
-  // Returns true if the browser supports bundle
-  //
-  // Broadly:
-  // * Firefox doesn't support bundle
-  // * Chrome support bundle
-  // * OT Plugin supports bundle
-  //
-  OT.$.supportsBundle = function() {
-    return OT.$.supportsWebRTC() && (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled());
-  };
-
-  // Returns true if the browser supports rtcp mux
-  //
-  // Broadly:
-  // * Older versions of Firefox (<= 25) don't support rtcp mux
-  // * Older versions of Firefox (>= 26) support rtcp mux (not tested yet)
-  // * Chrome support rtcp mux
-  // * OT Plugin supports rtcp mux
-  //
-  OT.$.supportsRtcpMux = function() {
-    return OT.$.supportsWebRTC() && (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled());
-  };
-
-  OT.$.shouldAskForDevices = function(callback) {
-    var memoiseReply = function(audio, video) {
-      OT.$.shouldAskForDevices = function(callback) {
-        setTimeout(OT.$.bind(callback, null, { video: video, audio: audio }));
-      };
-      OT.$.shouldAskForDevices(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';
-        });
-        memoiseReply(hasAudio, hasVideo);
-      });
-    } else {
-      memoiseReply(true, 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
   //
@@ -5959,46 +6069,16 @@ waitForDomReady();
   //
   // @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.
   //
-
-  var chromeToW3CDeviceKinds = {
-    audio: 'audioInput',
-    video: 'videoInput'
-  };
-
-  /*global MediaStreamTrack*/
-  OT.$.canGetMediaDevices = function() {
-    return typeof MediaStreamTrack === 'function' && OT.$.isFunction(MediaStreamTrack.getSources);
-  };
-
-  OT.$.getMediaDevices = function(callback) {
-    if(OT.$.canGetMediaDevices()) {
-      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'));
-    }
-  };
-
   OT.$.getUserMedia = function(constraints, success, failure, accessDialogOpened,
     accessDialogClosed, accessDenied, customGetUserMedia) {
 
     var getUserMedia = nativeGetUserMedia;
 
     if(OT.$.isFunction(customGetUserMedia)) {
       getUserMedia = customGetUserMedia;
     }
@@ -6076,35 +6156,81 @@ waitForDomReady();
       triggerOpenedTimer = setTimeout(triggerOpened, 100);
 
     } else {
       // wait a second and then trigger accessDialogOpened
       triggerOpenedTimer = setTimeout(triggerOpened, 500);
     }
   };
 
-  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);
-    }
-  };
-
+})();
+// 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)',
@@ -6380,18 +6506,17 @@ waitForDomReady();
     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.call(null, null, reason, this, 'VideoElement');
+          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, ' +
@@ -6465,24 +6590,17 @@ waitForDomReady();
 
       return this;
     };
 
 
     // Unbind the currently bound stream from the video element.
     this.unbindStream = function() {
       if (_domElement) {
-        if (!navigator.mozGetUserMedia) {
-          // The browser would have released this on unload anyway, but
-          // we're being a good citizen.
-          window.URL.revokeObjectURL(_domElement.src);
-        }
-        else {
-          _domElement.mozSrcObject = null;
-        }
+        unbindNativeStream(_domElement);
       }
 
       return this;
     };
 
     this.setAudioVolume = function(value) {
       if (_domElement) _domElement.volume = value;
     };
@@ -6715,41 +6833,46 @@ waitForDomReady();
 
       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 {
-          // This should never happen
+
           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.
@@ -6759,16 +6882,123 @@ waitForDomReady();
       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;
 
 
@@ -6943,82 +7173,98 @@ waitForDomReady();
     // @option options [String] section ...
     // @option options [String] build ...
     //
     this.logEvent = function(options) {
       var partnerId = options.partnerId;
 
       if (!options) options = {};
 
-      // Set a bunch of defaults
-      var data = OT.$.extend({
-        'variation' : '',
-        'guid' : this.getClientGuid(),
-        'widget_id' : '',
-        'session_id': '',
-        'connection_id': '',
-        'stream_id' : '',
-        'partner_id' : partnerId,
-        'source' : window.location.href,
-        'section' : '',
-        'build' : ''
-      }, options),
-
-      onComplete = function(){
-        //  OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation']
-        //  + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}');
-      };
-
-      // We camel-case our names, but the ClientEvents backend wants them
-      // underscored...
-      for (var key in camelCasedKeys) {
-        if (camelCasedKeys.hasOwnProperty(key) && data[key]) {
-          data[camelCasedKeys[key]] = data[key];
-          delete data[key];
-        }
-      }
-
-      post(data, onComplete, false);
+      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 = {};
 
-      // Set a bunch of defaults
-      var data = OT.$.extend({
-        'guid' : this.getClientGuid(),
-        'widget_id' : '',
-        'session_id': '',
-        'connection_id': '',
-        'stream_id' : '',
-        'partner_id' : partnerId,
-        'source' : window.location.href,
-        'build' : '',
-        'duration' : 0 //in milliseconds
-      }, options),
-
-      onComplete = function(){
-        // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation']
-        //  + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}');
-      };
-
-      // We camel-case our names, but the ClientEvents backend wants them
-      // underscored...
-      for (var key in camelCasedKeys) {
-        if (camelCasedKeys.hasOwnProperty(key) && data[key]) {
-          data[camelCasedKeys[key]] = data[key];
-          delete data[key];
-        }
-      }
-
-      post(data, onComplete, true);
+      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 = [],
@@ -7031,34 +7277,38 @@ waitForDomReady();
         }
       }
 
       return [
         escapedPayloadDesc.join('|'),
         escapedPayload.join('|')
       ];
     };
-
-    // Uses HTML5 local storage to save a client ID.
-    this.getClientGuid = function() {
-      var guid = OT.$.getCookie('opentok_client_id');
-      if (!guid) {
-        guid = OT.$.uuid();
-        OT.$.setCookie('opentok_client_id', guid);
-      }
-      // once we have a guid, memoise this function so if cookies & local storage are disabled
-      // we still hand back the same guid each call within this page at least. OPENTOK-14015
-      this.getClientGuid = function() {
-        return guid;
-      };
-      return guid;
-    };
-  };
-
-})(window);
+  };
+
+})(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 ' +
@@ -7066,16 +7316,18 @@ waitForDomReady();
   }
 
   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 <code>OT.initSession()</code>
 * method. Other methods of the OT object check for system requirements and set up error logging.
@@ -7255,27 +7507,39 @@ waitForDomReady();
 *   frame rate or resolution may not reduce the stream's bandwidth.
 *   </p>
 * </li>
 * <li>
 *   <strong>style</strong> (Object) &#151; An object containing properties that define the initial
 *   appearance of user interface controls of the Publisher. The <code>style</code> object includes
 *   the following properties:
 *     <ul>
-*       <li><code>backgroundImageURI</code> (String) &mdash; A URI for an image to display as
+*       <li><code>audioLevelDisplayMode</code> (String) &mdash; How to display the audio level
+*       indicator. Possible values are: <code>"auto"</code> (the indicator is displayed when the
+*       video is disabled), <code>"off"</code> (the indicator is not displayed), and
+*       <code>"on"</code> (the indicator is always displayed).</li>
+*
+*       <li><p><code>backgroundImageURI</code> (String) &mdash; 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 <code>publishVideo(false)</code> 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
 *       <code>data</code> URI scheme (instead of http or https) and pass in base-64-encrypted
 *       PNG data, such as that obtained from the
 *       <a href="Publisher.html#getImgData">Publisher.getImgData()</a> method. For example,
 *       you could set the property to <code>"data:VBORw0KGgoAA..."</code>, where the portion of the
 *       string after <code>"data:"</code> is the result of a call to
 *       <code>Publisher.getImgData()</code>. If the URL or the image data is invalid, the property
-*       is ignored (the attempt to set the image fails silently).</li>
+*       is ignored (the attempt to set the image fails silently).
+*       <p>
+*       Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+*       you cannot set the <code>backgroundImageURI</code> style to a string larger than 32&nbsp;kB.
+*       This is due to an IE 8 limitation on the size of URI strings. Due to this limitation,
+*       you cannot set the <code>backgroundImageURI</code> style to a string obtained with the
+*       <code>getImgData()</code> method.
+*       </p></li>
 *
 *       <li><code>buttonDisplayMode</code> (String) &mdash; How to display the microphone controls
 *       Possible values are: <code>"auto"</code> (controls are displayed when the stream is first
 *       displayed and when the user mouses over the display), <code>"off"</code> (controls are not
 *       displayed), and <code>"on"</code> (controls are always displayed).</li>
 *
 *       <li><code>nameDisplayMode</code> (String) &#151; Whether to display the stream name.
 *       Possible values are: <code>"auto"</code> (the name is displayed when the stream is first
@@ -7434,26 +7698,37 @@ waitForDomReady();
 * @see <a href="#upgradeSystemRequirements">OT.upgradeSystemRequirements()</a>
 * @method OT.checkSystemRequirements
 * @memberof OT
 */
   OT.checkSystemRequirements = function() {
     OT.debug('OT.checkSystemRequirements()');
 
     // Try native support first, then TBPlugin...
-    var systemRequirementsMet = (OT.$.supportsWebSockets() && OT.$.supportsWebRTC());
+    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.
 * <p>
@@ -7463,16 +7738,30 @@ waitForDomReady();
 * </p>
 * @see <a href="#checkSystemRequirements">OT.checkSystemRequirements()</a>
 * @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';
@@ -7492,17 +7781,18 @@ waitForDomReady();
           d.setAttribute('allowTransparency', 'true');
         }
         d.setAttribute('frameBorder', '0');
         d.frameBorder = '0';
         d.scrolling = 'no';
         d.setAttribute('scrolling', 'no');
 
         var browser = OT.$.browserVersion(),
-            isSupportedButOld = OT.properties.minimumVersion[browser.browser.toLowerCase()];
+            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;
       })());
 
@@ -8065,17 +8355,20 @@ waitForDomReady();
     DEVICES_SELECTED: 'devicesSelected',
     CLOSE_BUTTON_CLICK: 'closeButtonClick',
 
     MICLEVEL : 'microphoneActivityLevel',
     MICGAINCHANGED : 'microphoneGainChanged',
 
     // Environment Loader
     ENV_LOADED: 'envLoaded',
-    ENV_UNLOADED: 'envUnloaded'
+    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,
@@ -8288,46 +8581,40 @@ waitForDomReady();
 
   // Triggered when the JS dynamic config and the DOM have loaded.
   OT.EnvLoadedEvent = function (type) {
     OT.Event.call(this, type);
   };
 
 
 /**
- * Connection event is an event that can have type "connectionCreated" or "connectionDestroyed".
- * These events are dispatched by the Session object when another client connects to or
- * disconnects from a {@link Session}. For the local client, the Session object dispatches a
- * "sessionConnected" or "sessionDisconnected" event, defined by the SessionConnectEvent and
- * SessionDisconnectEvent classes.
+ * 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.
  *
  * <h5><a href="example"></a>Example</h5>
  *
  * <p>The following code keeps a running total of the number of connections to a session
  * by monitoring the <code>connections</code> property of the <code>sessionConnect</code>,
  * <code>connectionCreated</code> and <code>connectionDestroyed</code> events:</p>
  *
  * <pre>var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
  * var sessionID = ""; // Replace with your own session ID.
  *                     // See https://dashboard.tokbox.com/projects
  * var token = ""; // Replace with a generated token that has been assigned the moderator role.
  *                 // See https://dashboard.tokbox.com/projects
  * var connectionCount = 0;
  *
  * var session = OT.initSession(apiKey, sessionID);
- * session.on("sessionConnected", function(event) {
- *    connectionCount = 1; // This represent's your client's connection to the session
- *    displayConnectionCount();
- * });
  * session.on("connectionCreated", function(event) {
- *    connectionCount += 1;
+ *    connectionCount++;
  *    displayConnectionCount();
  * });
  * session.on("connectionDestroyed", function(event) {
- *    connectionCount -= 1;
+ *    connectionCount--;
  *    displayConnectionCount();
  * });
  * session.connect(token);
  *
  * function displayConnectionCount() {
  *     document.getElementById("connectionCountField").value = connectionCount.toString();
  * }</pre>
  *
@@ -8443,23 +8730,22 @@ waitForDomReady();
  * var publisher = session.publish(targetElement)
  *   .on("streamDestroyed", function(event) {
  *     event.preventDefault();
  *     console.log("Publisher stopped streaming.");
  *   );
  * </pre>
  *
  * @class StreamEvent
- * @property {Stream} stream A Stream object corresponding to the stream that was added (in the
- * case of a <code>streamCreated</code> event) or deleted (in the case of a
- * <code>streamDestroyed</code> event).
- *
- * @property {Array} streams Deprecated. Use the <code>stream</code> property. A
- * <code>streamCreated</code> or <code>streamDestroyed</code> event is dispatched for
- * each stream added or destroyed.
+ *
+ * @property {Boolean} cancelable   Whether the event has a default behavior that is cancelable
+ *  (<code>true</code>) or not (<code>false</code>). You can cancel the default behavior by calling
+ *  the <code>preventDefault()</code> method of the StreamEvent object in the event listener
+ *  function. The <code>streamDestroyed</code>
+ *  event is cancelable. (See <a href="#preventDefault">preventDefault()</a>.)
  *
  * @property {String} reason For a <code>streamDestroyed</code> event,
  *  a description of why the session disconnected. This property can have one of the following
  *  values:
  * </p>
  * <ul>
  *  <li><code>"clientDisconnected"</code> &#151; A client disconnected from the session by calling
  *     the <code>disconnect()</code> method of the Session object or by closing the browser.
@@ -8478,22 +8764,24 @@ waitForDomReady();
  *
  * </ul>
  *
  * <p>Depending on the context, this description may allow the developer to refine
  * the course of action they take in response to an event.</p>
  *
  * <p>For a <code>streamCreated</code> event, this string is undefined.</p>
  *
- *
- * @property {Boolean} cancelable   Whether the event has a default behavior that is cancelable
- *  (<code>true</code>) or not (<code>false</code>). You can cancel the default behavior by calling
- *  the <code>preventDefault()</code> method of the StreamEvent object in the event listener
- *  function. The <code>streamDestroyed</code>
- *  event is cancelable. (See <a href="#preventDefault">preventDefault()</a>.)
+ * @property {Stream} stream A Stream object corresponding to the stream that was added (in the
+ * case of a <code>streamCreated</code> event) or deleted (in the case of a
+ * <code>streamDestroyed</code> event).
+ *
+ * @property {Array} streams Deprecated. Use the <code>stream</code> property. A
+ * <code>streamCreated</code> or <code>streamDestroyed</code> 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) {
@@ -8548,18 +8836,18 @@ waitForDomReady();
  * <p>
  * In version 2.2, the completionHandler of the <code>Session.connect()</code> 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 <code>connectionCreated</code> event dispatched by the Session
  * object. In version 2.2, the Session object dispatches a <code>connectionCreated</code> event
- * for each connection other than that of your client. This includes connections
- * present when you first connect to the session.
+ * 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 <code>streamCreated</code> event dispatched by the Session object. In
  * version 2.2, the Session object dispatches a <code>streamCreated</code> event for each stream
  * other than those published by your client. This includes streams
  * present when you first connect to the session.
  *
  * @see <a href="Session.html#connect">Session.connect()</a></p>
@@ -8692,19 +8980,19 @@ waitForDomReady();
  *  <li>When the <code>videoDimensions</code> property of a stream changes. For more information,
  *  see <a href="Stream.html#properties">Stream.videoDimensions</a>.</li>
  *
  * </ul>
  *
  * @class StreamPropertyChangedEvent
  * @property {String} changedProperty The property of the stream that changed. This value
  * is either <code>"hasAudio"</code>, <code>"hasVideo"</code>, or <code>"videoDimensions"</code>.
- * @property {Stream} stream The Stream object for which a property has changed.
  * @property {Object} newValue The new value of the property (after the change).
  * @property {Object} oldValue The old value of the property (before the change).
+ * @property {Stream} stream The Stream object for which a property has changed.
  *
  * @see <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a></p>
  * @see <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a></p>
  * @see <a href="Stream.html#properties">Stream.videoDimensions</a></p>
  * @augments Event
  */
   OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) {
     OT.Event.call(this, type, false);
@@ -8759,31 +9047,110 @@ waitForDomReady();
  * @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 <code>videoDisabled</code> and <code>videoEnabled</code> events
+ * dispatched by the Subscriber.
+ *
+ * @class VideoEnabledChangedEvent
+ *
+ * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
+ * (<code>true</code>) or not (<code>false</code>). You can cancel the default behavior by
+ * calling the <code>preventDefault()</code> method of the event object in the callback
+ * function. (See <a href="#preventDefault">preventDefault()</a>.)
+ *
+ * @property {String} reason The reason the video was disabled or enabled. This can be set to one of
+ * the following values:
+ *
+ * <ul>
+ *
+ *   <li><code>"publishVideo"</code> &mdash; The publisher started or stopped publishing video,
+ *   by calling <code>publishVideo(true)</code> or <code>publishVideo(false)</code>.</li>
+ *
+ *   <li><code>"quality"</code> &mdash; 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.)
+ *   <p>
+ *   If connectivity improves to support video again, the Subscriber object dispatches
+ *   a <code>videoEnabled</code> event, and the Subscriber resumes receiving video.
+ *   <p>
+ *   By default, the Subscriber displays a video disabled indicator when a
+ *   <code>videoDisabled</code> event with this reason is dispatched and removes the indicator
+ *   when the <code>videoDisabled</code> event with this reason is dispatched. You can control
+ *   the display of this icon by calling the <code>setStyle()</code> method of the Subscriber,
+ *   setting the <code>videoDisabledDisplayMode</code> property(or you can set the style when
+ *   calling the <code>Session.subscribe()</code> method, setting the <code>style</code> property
+ *   of the <code>properties</code> parameter).
+ *   <p>
+ *   This feature is only available in sessions that use the OpenTok Media Router (sessions with
+ *   the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
+ *   set to routed), not in sessions with the media mode set to relayed.
+ *   </li>
+ *
+ *   <li><code>"subscribeToVideo"</code> &mdash; The subscriber started or stopped subscribing to
+ *   video, by calling <code>subscribeToVideo(true)</code> or <code>subscribeToVideo(false)</code>.
+ *   </li>
+ *
+ * </ul>
+ *
+ * @property {Object} target The object that dispatched the event.
+ *
+ * @property {String} type  The type of event: <code>"videoDisabled"</code> or
+ * <code>"videoEnabled"</code>.
+ *
+ * @see <a href="Subscriber.html#event:videoDisabled">Subscriber videoDisabled event</a></p>
+ * @see <a href="Subscriber.html#event:videoEnabled">Subscriber videoEnabled event</a></p>
+ * @augments Event
+ */
+  OT.VideoEnabledChangedEvent = function(type, properties) {
+    OT.Event.call(this, type, false);
+    this.reason = properties.reason;
+  };
+
+  OT.VideoDisableWarningEvent = function(type/*, properties*/) {
+    OT.Event.call(this, type, false);
+  };
+
+/**
+ * Dispatched periodically by a Subscriber or Publisher object to indicate the audio
+ * level. This event is dispatched up to 60 times per second, depending on the browser.
+ *
+ * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically
+ * for use in adjusting a user interface element, such as a volume meter. Use a moving average
+ * to smooth the data.
+ *
+ * @class AudioLevelUpdatedEvent
+ * @augments Event
+ */
+  OT.AudioLevelUpdatedEvent = function(audioLevel) {
+    OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false);
+    this.audioLevel = audioLevel;
+  };
+
 })(window);
 /* jshint ignore:start */
 // https://code.google.com/p/stringencoding/
 // An implementation of http://encoding.spec.whatwg.org/#api
 
 /**
  * @license  Copyright 2014 Joshua Bell
  *
@@ -11416,25 +11783,25 @@ waitForDomReady();
         }, this),
 
         hasLostConnectivity = function hasLostConnectivity () {
           if (!lastMessageTimestamp) return false;
 
           return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT;
         },
 
-        sendKeepAlive = OT.$.bind(function sendKeepAlive () {
+        sendKeepAlive = OT.$.bind(function() {
           if (!this.is('connected')) return;
 
           if ( hasLostConnectivity() ) {
             webSocketDisconnected({code: 4001});
           }
           else  {
             webSocket.send(OT.Rumor.Message.Ping());
-            keepAliveTimer = setTimeout(sendKeepAlive.bind(this), WEB_SOCKET_KEEP_ALIVE_INTERVAL);
+            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;
@@ -11458,17 +11825,17 @@ waitForDomReady();
           setState('connected');
           if (connectCallback) {
             connectCallback(null, id);
             connectCallback = null;
           }
 
           if (onOpen) onOpen(id);
 
-          setTimeout(function() {
+          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.');
@@ -11742,17 +12109,16 @@ waitForDomReady();
 
         webSocket.onOpen(function() {
           state = 'open';
           events.onOpen();
         });
         webSocket.onClose(function(error) {
           state = 'closed'; /* CLOSED */
           events.onClose({ code: error });
-          webSocket.finalize();
         });
         webSocket.onError(function(error) {
           state = 'closed'; /* CLOSED */
           events.onError(error);
           /* native websockets seem to do this, so should we */
           events.onClose({ code: error });
         });
 
@@ -12170,17 +12536,17 @@ waitForDomReady();
 
   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 [];
     }
   };
 
@@ -12321,18 +12687,18 @@ waitForDomReady();
   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.$.supportsBundle(),
-      rtcpMuxSupport: OT.$.supportsRtcpMux()
+      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
@@ -12855,27 +13221,27 @@ waitForDomReady();
     409: 'This P2P session already has 2 participants.',
     410: 'The session already has four participants.',
     1004: 'The token passed is invalid.'
   };
 
 
   OT.Raptor.Dispatcher = function () {
 
-    if(typeof EventEmitter !== 'undefined') {
+    if(OT.isNodeModule) {
       EventEmitter.call(this);
     } else {
       OT.$.eventing(this, true);
       this.emit = this.trigger;
     }
 
     this.callbacks = {};
   };
 
-  if(typeof EventEmitter !== 'undefined') {
+  if(OT.isNodeModule) {
     util.inherits(OT.Raptor.Dispatcher, EventEmitter);
   }
 
   OT.Raptor.Dispatcher.prototype.registerCallback = function (transactionId, completion) {
     this.callbacks[transactionId] = completion;
   };
 
   OT.Raptor.Dispatcher.prototype.triggerCallback = function (transactionId) {
@@ -12915,17 +13281,17 @@ waitForDomReady();
 
       this.triggerCallback(rumorMessage.transactionId, error, rumorMessage);
 
       return;
     }
 
     var message = OT.Raptor.unboxFromRumorMessage(rumorMessage);
     OT.debug('OT.Raptor.dispatch ' + message.signature);
-    OT.debug(message);
+    OT.debug(rumorMessage.data);
 
     switch(message.resource) {
       case 'session':
         this.dispatchSession(message);
         break;
 
       case 'connection':
         this.dispatchConnection(message);
@@ -13167,16 +13533,25 @@ waitForDomReady();
     if (session.archives.has(dict.id)) return;
 
     var archive = parseArchive(dict);
     session.archives.add(archive);
 
     return archive;
   }
 
+  var sessionRead;
+  var sessionReadQueue = [];
+
+  function sessionReadQueuePush(type, args) {
+    var triggerArgs = ['signal'];
+    triggerArgs.push.apply(triggerArgs, args);
+    sessionReadQueue.push(triggerArgs);
+  }
+
   window.OT.SessionDispatcher = function(session) {
 
     var dispatcher = new OT.Raptor.Dispatcher();
 
     dispatcher.on('close', function(reason) {
 
       var connection = session.connection;
 
@@ -13215,16 +13590,23 @@ waitForDomReady();
 
       OT.$.forEach(content.archive || content.archives, function(archiveParams) {
         state.archives.push( parseAndAddArchiveToSession(archiveParams, session) );
       });
 
       session._.subscriberMap = {};
 
       dispatcher.triggerCallback(transactionId, null, state);
+
+      sessionRead = true;
+      for (var i = 0; i < sessionReadQueue.length; ++i) {
+        dispatcher.trigger.apply(dispatcher, sessionReadQueue[i]);
+      }
+      sessionReadQueue = [];
+
     });
 
     dispatcher.on('connection#created', function(connection) {
       connection = OT.Connection.fromHash(connection);
       if (session.connection && connection.id !== session.connection.id) {
         session.connections.add( connection );
       }
     });
@@ -13427,18 +13809,24 @@ waitForDomReady();
         // @todo error
         return;
       }
 
       delete session._.subscriberMap[fromAddress + '_' + stream.id];
     });
 
     dispatcher.on('signal', function(fromAddress, signalType, data) {
-      session._.dispatchSignal(session.connections.get(fromAddress),
-                               signalType, data);
+      if (sessionRead) {
+        var fromConnection = session.connections.get(fromAddress);
+        session._.dispatchSignal(fromConnection, signalType, data);
+      } else {
+        if (!sessionRead) {
+          sessionReadQueuePush('signal', arguments);
+        }
+      }
     });
 
     dispatcher.on('archive#created', function(archive) {
       parseAndAddArchiveToSession(archive, session);
     });
 
     dispatcher.on('archive#updated', function(archiveId, update) {
       var archive = session.archives.get(archiveId);
@@ -13882,40 +14270,16 @@ waitForDomReady();
       };
 
       if (!options.target) options.target = null;
     }
 
     _exceptionHandler(options.target, errorMsg, code, context);
   };
 
-
-// @todo redo this when we have time to tidy up
-//
-// Public callback for exceptions from Flash.
-//
-// Called from Flash like:
-//  OT.exceptionHandler('publisher_1234,1234',
-//  "Descriptive Error Message", "Error Title", 2000, contextObj)
-//
-  OT.exceptionHandler = function(componentId, msg, errorTitle, errorCode, context) {
-    var target;
-
-    if (componentId) {
-      target = OT.components[componentId];
-
-      if (!target) {
-        OT.warn('Could not find the component with component ID ' + componentId);
-      }
-    }
-
-    _exceptionHandler(target, msg, errorCode, context);
-  };
-
-
   // This is a placeholder until error handling can be rewritten
   OT.dispatchError = function (code, message, completionHandler, session) {
     OT.error(code, message);
 
     if (completionHandler && OT.$.isFunction(completionHandler)) {
       completionHandler.call(null, new OT.Error(code, message));
     }
 
@@ -13947,20 +14311,21 @@ waitForDomReady();
    * to a session has a unique connection, with a unique connection ID (represented by the
    * <code>id</code> property of the Connection object for the client).
    * <p>
    * The Session object has a <code>connection</code> 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 <code>connect()</code> method of the {@link Session}
    * object.)
    * <p>
-   * The Session object dispatches a <code>connectionCreated</code> event when each client (other
-   * than your own) connects to a session (and for clients that are present in the session when you
-   * connect). The <code>connectionCreated</code> event object has a <code>connection</code>
-   * property, which is a Connection object corresponding to the client the event pertains to.
+   * The Session object dispatches a <code>connectionCreated</code> event when each client
+   * (including your own) connects to a session (and for clients that are present in the
+   * session when you connect). The <code>connectionCreated</code> event object has a
+   * <code>connection</code> property, which is a Connection object corresponding to the client
+   * the event pertains to.
    * <p>
    * The Stream object has a <code>connection</code> 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.
@@ -14052,16 +14417,20 @@ waitForDomReady();
         // 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);
 
@@ -14139,29 +14508,29 @@ waitForDomReady();
  * {@link StreamPropertyChangedEvent}.)
  *
  * @property {Boolean} hasVideo Whether the stream has video. This property can change if the
  * publisher turns on or off video (by calling
  * <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>). When this occurs, the
  * {@link Session} object dispatches a <code>streamPropertyChanged</code> event (see
  * {@link StreamPropertyChangedEvent}.)
  *
+ * @property {String} name The name of the stream. Publishers can specify a name when publishing
+ * a stream (using the <code>publish()</code> method of the publisher's Session object).
+ *
+ * @property {String} streamId The unique ID of the stream.
+ *
  * @property {Object} videoDimensions This object has two properties: <code>width</code> and
  * <code>height</code>. Both are numbers. The <code>width</code> property is the width of the
  * encoded stream; the <code>height</code> 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 <code>streamPropertyChanged</code> event (see
  * {@link StreamPropertyChangedEvent}.)
- *
- * @property {String} name The name of the stream. Publishers can specify a name when publishing
- * a stream (using the <code>publish()</code> method of the publisher's Session object).
- *
- * @property {String} streamId The unique ID of the stream.
  */
 
 
   OT.Stream = function(id, name, creationTime, connection, session, channel) {
     var destroyedReason;
 
     this.id = this.streamId = id;
     this.name = name;
@@ -14177,16 +14546,24 @@ waitForDomReady();
       var _key = key;
 
       switch(_key) {
         case 'active':
           _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
           this[_key] = newValue;
           break;
 
+        case 'disableWarning':
+          _key = channel.type === 'audio' ? 'audioDisableWarning': 'videoDisableWarning';
+          this[_key] = newValue;
+          if (!this[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) {
+            return; // Do NOT event in this case.
+          }
+          break;
+
         case 'orientation':
         case 'width':
         case 'height':
           this.videoDimensions = {
             width: channel.width,
             height: channel.height,
             orientation: channel.orientation
           };
@@ -14388,16 +14765,180 @@ waitForDomReady();
       }
     }, this);
 
     this.destroy = function() {};
 
   };
 
 })(window);
+!(function() {
+
+
+  /*
+   * A <code>RTCPeerConnection.getStats</code> based audio level sampler.
+   *
+   * It uses the the <code>getStats</code> method to get the <code>audioOutputLevel</code>.
+   * This implementation expects the single parameter version of the <code>getStats</code> method.
+   *
+   * Currently the <code>audioOutputLevel</code> stats is only supported in Chrome.
+   *
+   * @param {OT.SubscriberPeerConnection} peerConnection the peer connection to use to get the stats
+   * @constructor
+   */
+  OT.GetStatsAudioLevelSampler = function(peerConnection) {
+
+    if (!OT.$.hasCapabilities('audioOutputLevelStat', 'getStatsWithSingleParameter')) {
+      throw new Error('The current platform does not provide the required capabilities');
+    }
+
+    var _peerConnection = peerConnection,
+        _statsProperty = 'audioOutputLevel';
+
+    /*
+     * Acquires the audio level.
+     *
+     * @param {function(?number)} done a callback to be called with the acquired value in the
+     * [0, 1] range when available or <code>null</code> if no value could be acquired
+     */
+    this.sample = function(done) {
+      _peerConnection.getStatsWithSingleParameter(function(statsReport) {
+        var results = statsReport.result();
+
+        for (var i = 0; i < results.length; i++) {
+          var result = results[i];
+          if (result.local) {
+            var audioOutputLevel = parseFloat(result.local.stat(_statsProperty));
+            if (!isNaN(audioOutputLevel)) {
+              // the mex value delivered by getStats for audio levels is 2^15
+              done(audioOutputLevel / 32768);
+              return;
+            }
+          }
+        }
+
+        done(null);
+      });
+    };
+  };
+
+
+  /*
+   * An <code>AudioContext</code> based audio level sampler. It returns the maximum value in the
+   * last 1024 samples.
+   *
+   * It is worth noting that the remote <code>MediaStream</code> audio analysis is currently only
+   * available in FF.
+   *
+   * This implementation gracefully handles the case where the <code>MediaStream</code> has not
+   * been set yet by returning a <code>null</code> 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 <code>window.setInterval</code>.
+   *
+   * @param {function()} callback
+   * @param {number} frequency how many times per second we want to execute the callback
+   * @constructor
+   */
+  OT.IntervalRunner = function(callback, frequency) {
+    var _callback = callback,
+      _frequency = frequency,
+      _intervalId = null;
+
+    this.start = function() {
+      _intervalId = window.setInterval(_callback, 1000 / _frequency);
+    };
+
+    this.stop = function() {
+      window.clearInterval(_intervalId);
+      _intervalId = null;
+    };
+  };
+
+})(window);
 !(function(window) {
 
   // Normalise these
   var NativeRTCSessionDescription,
       NativeRTCIceCandidate;
 
   if (!TBPlugin.isInstalled()) {
     // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless
@@ -14497,16 +15038,45 @@ waitForDomReady();
       // Remove all CN payload types from the audio media line.
       sdpLines[audioMediaLineIndex] = sdpLines[audioMediaLineIndex].replace(
         new RegExp(payloadTypes.join('|'), 'ig') , '').replace(/\s+/g, ' ');
     }
 
     return sdpLines.join('\r\n');
   };
 
+  var removeVideoCodec = function removeVideoCodec (sdp, codec) {
+    var matcher =  new RegExp('a=rtpmap:(\\d+) ' + codec + '\\/\\d+', 'i'),
+        payloadTypes = [],
+        videoMediaLineIndex,
+        sdpLines,
+        match;
+
+    sdpLines = OT.$.filter(sdp.split('\r\n'), function(line, index) {
+      if (line.indexOf('m=video') !== -1) videoMediaLineIndex = index;
+
+      match = line.match(matcher);
+      if (match !== null) {
+        payloadTypes.push(match[1]);
+
+        // remove this line as it contains the codec
+        return false;
+      }
+
+      return true;
+    });
+
+    if (payloadTypes.length && videoMediaLineIndex) {
+      sdpLines[videoMediaLineIndex] = sdpLines[videoMediaLineIndex].replace(
+        new RegExp(payloadTypes.join('|'), 'ig') , '').replace(/\s+/g, ' ');
+    }
+
+    return sdpLines.join('\r\n');
+  };
+
   // Attempt to completely process +offer+. This will:
   // * set the offer as the remote description
   // * create an answer and
   // * set the new answer as the location description
   //
   // If there are no issues, the +success+ callback will be executed on completion.
   // Errors during any step will result in the +failure+ callback being executed.
   //
@@ -14521,16 +15091,18 @@ waitForDomReady();
         OT.error(errorReason);
 
         if (failure) failure(message, errorReason, prefix);
       };
     };
 
     setLocalDescription = function(answer) {
       answer.sdp = removeComfortNoise(answer.sdp);
+      answer.sdp = removeVideoCodec(answer.sdp, 'ulpfec');
+      answer.sdp = removeVideoCodec(answer.sdp, 'red');
 
       peerConnection.setLocalDescription(
         answer,
 
         // Success
         function() {
           success(answer);
         },
@@ -14605,16 +15177,18 @@ waitForDomReady();
         OT.error(errorReason);
 
         if (failure) failure(message, errorReason, prefix);
       };
     };
 
     setLocalDescription = function(offer) {
       offer.sdp = removeComfortNoise(offer.sdp);
+      offer.sdp = removeVideoCodec(offer.sdp, 'ulpfec');
+      offer.sdp = removeVideoCodec(offer.sdp, 'red');
 
       peerConnection.setLocalDescription(
         offer,
 
         // Success
         function() {
           success(offer);
         },
@@ -15024,16 +15598,24 @@ waitForDomReady();
       }
       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
@@ -15601,17 +16183,17 @@ waitForDomReady();
     };
 
     _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);
+      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 ;
@@ -15753,22 +16335,28 @@ waitForDomReady();
                 properties.restrictFrameRate : false
             };
           }));
         }
 
         session._.subscriberCreate(stream, subscriber, channelsToSubscribeTo,
           OT.$.bind(function(err, message) {
             if (err) {
-              this.trigger('error', null, err.message, this, 'Subscribe');
+              this.trigger('error', err.message, this, 'Subscribe');
             }
             _peerConnection.setIceServers(OT.Raptor.parseIceServers(message));
           }, this));
       }
     };
+
+    this.getStatsWithSingleParameter = function(callback) {
+      if(_peerConnection) {
+        _peerConnection.getStatsWithSingleParameter(callback);
+      }
+    };
   };
 
 })(window);
 !(function() {
 
 // Manages N Chrome elements
   OT.Chrome = function(properties) {
     var _visible = false,
@@ -15900,28 +16488,28 @@ waitForDomReady();
     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);
 
-      // if the mode isn't auto, then we can directly set it
-      if (_options.mode !== 'auto') {
-        widget.setDisplayMode(_options.mode);
-      } else {
-        // we set it to on at first, and then apply the desired mode
-        // this will let the proper widgets nicely fade away
-        widget.setDisplayMode('on');
+      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() {
-          widget.setDisplayMode(_options.mode);
+          OT.$.removeClass(widget.domElement, 'OT_mode-on-hold');
         }, 2000);
       }
 
+
       // add the widget to the parent
       parent.appendChild(this.domElement);
 
       return widget;
     };
   };
 
 })(window);
@@ -16231,16 +16819,119 @@ waitForDomReady();
       if(this.domElement) {
         renderStage.call(this);
       }
     }, this);
 
   };
 
 })(window);
+!(function() {
+
+  OT.Chrome.AudioLevelMeter = function(options) {
+
+    var widget = this,
+        _meterBarElement,
+        _voiceOnlyIconElement,
+        _meterValueElement,
+        _value,
+        _maxValue = options.maxValue || 1,
+        _minValue = options.minValue || 0;
+
+    // Mixin common widget behaviour
+    OT.Chrome.Behaviour.Widget(this, {
+      mode: options ? options.mode : 'auto',
+      nodeName: 'div',
+      htmlAttributes: {
+        className: 'OT_audio-level-meter'
+      },
+      onCreate: function() {
+        _meterBarElement = OT.$.createElement('div', {
+          className: 'OT_audio-level-meter__bar'
+        }, '');
+        _meterValueElement = OT.$.createElement('div', {
+          className: 'OT_audio-level-meter__value'
+        }, '');
+        _voiceOnlyIconElement = OT.$.createElement('div', {
+          className: 'OT_audio-level-meter__audio-only-img'
+        }, '');
+
+        widget.domElement.appendChild(_meterBarElement);
+        widget.domElement.appendChild(_voiceOnlyIconElement);
+        widget.domElement.appendChild(_meterValueElement);
+      }
+    });
+
+    function updateView() {
+      var percentSize = _value * 100 / (_maxValue - _minValue);
+      _meterValueElement.style.width = _meterValueElement.style.height = 2 * percentSize + '%';
+      _meterValueElement.style.top = _meterValueElement.style.right = -percentSize + '%';
+    }
+
+    widget.setValue = function(value) {
+      _value = value;
+      updateView();
+    };
+  };
+
+})(window);
+!(function() {
+  OT.Chrome.VideoDisabledIndicator = function(options) {
+    var _mode,
+        _videoDisabled = false,
+        _warning = false,
+        updateClasses;
+
+    _mode = options.mode || 'auto';
+    updateClasses = function(domElement) {
+      if (_videoDisabled) {
+        OT.$.addClass(domElement, 'OT_video-disabled');
+      } else {
+        OT.$.removeClass(domElement, 'OT_video-disabled');
+      }
+      if(_warning) {
+        OT.$.addClass(domElement, 'OT_video-disabled-warning');
+      } else {
+        OT.$.removeClass(domElement, 'OT_video-disabled-warning');
+      }
+      if ((_videoDisabled || _warning) && (_mode === 'auto' || _mode === 'on')) {
+        OT.$.addClass(domElement, 'OT_active');
+      } else {
+        OT.$.removeClass(domElement, 'OT_active');
+      }
+    };
+
+    this.disableVideo = function(value) {
+      _videoDisabled = value;
+      if(value === true) {
+        _warning = false;
+      }
+      updateClasses(this.domElement);
+    };
+
+    this.setWarning = function(value) {
+      _warning = value;
+      updateClasses(this.domElement);
+    };
+
+    // Mixin common widget behaviour
+    OT.Chrome.Behaviour.Widget(this, {
+      mode: _mode,
+      nodeName: 'div',
+      htmlAttributes: {
+        className: 'OT_video-disabled-indicator'
+      }
+    });
+
+    this.setDisplayMode = function(mode) {
+      _mode = mode;
+      updateClasses(this.domElement);
+    };
+  };
+})(window);
 (function() {
 /* Stylable Notes
  * RTC doesn't need to wait until anything is loaded
  * Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode.
  * When there are multiple flags how is the final setting chosen?
  * When some style bits are set updates will need to be pushed through to the Chrome
  */
 
@@ -16312,27 +17003,39 @@ waitForDomReady();
    * Sets properties that define the appearance of some user interface controls of the Publisher.
    *
    * <p>You can either pass one parameter or two parameters to this method.</p>
    *
    * <p>If you pass one parameter, <code>style</code>, it is an object that has the following
    * properties:
    *
    *     <ul>
-   *       <li><code>backgroundImageURI</code> (String) &mdash; A URI for an image to display as
+   *       <li><code>audioLevelDisplayMode</code> (String) &mdash; How to display the audio level
+   *       indicator. Possible values are: <code>"auto"</code> (the indicator is displayed when the
+   *       video is disabled), <code>"off"</code> (the indicator is not displayed), and
+   *       <code>"on"</code> (the indicator is always displayed).</li>
+   *
+   *       <li><p><code>backgroundImageURI</code> (String) &mdash; 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 <code>publishVideo(false)</code> 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
    *       <code>data</code> URI scheme (instead of http or https) and pass in base-64-encrypted
    *       PNG data, such as that obtained from the
    *       <a href="Publisher.html#getImgData">Publisher.getImgData()</a> method. For example,
    *       you could set the property to <code>"data:VBORw0KGgoAA..."</code>, where the portion of
    *       the string after <code>"data:"</code> is the result of a call to
    *       <code>Publisher.getImgData()</code>. If the URL or the image data is invalid, the
-   *       property is ignored (the attempt to set the image fails silently).</li>
+   *       property is ignored (the attempt to set the image fails silently).
+   *       <p>
+   *       Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+   *       you cannot set the <code>backgroundImageURI</code> style to a string larger than
+   *       32&nbsp;kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+   *       limitation, you cannot set the <code>backgroundImageURI</code> style to a string obtained
+   *       with the <code>getImgData()</code> method.
+   *       </p></li>
    *
    *       <li><code>buttonDisplayMode</code> (String) &mdash; How to display the microphone
    *       controls. Possible values are: <code>"auto"</code> (controls are displayed when the
    *       stream is first displayed and when the user mouses over the display), <code>"off"</code>
    *       (controls are not displayed), and <code>"on"</code> (controls are always displayed).</li>
    *
    *       <li><code>nameDisplayMode</code> (String) &#151; Whether to display the stream name.
    *       Possible values are: <code>"auto"</code> (the name is displayed when the stream is first
@@ -16377,37 +17080,57 @@ waitForDomReady();
    * Sets properties that define the appearance of some user interface controls of the Subscriber.
    *
    * <p>You can either pass one parameter or two parameters to this method.</p>
    *
    * <p>If you pass one parameter, <code>style</code>, it is an object that has the following
    * properties:
    *
    *     <ul>
-   *       <li><code>backgroundImageURI</code> (String) &mdash; A URI for an image to display as
+   *       <li><code>audioLevelDisplayMode</code> (String) &mdash; How to display the audio level
+   *       indicator. Possible values are: <code>"auto"</code> (the indicator is displayed when the
+   *       video is disabled), <code>"off"</code> (the indicator is not displayed), and
+   *       <code>"on"</code> (the indicator is always displayed).</li>
+   *
+   *       <li><p><code>backgroundImageURI</code> (String) &mdash; 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 <code>subscribeToVideo(false)</code> 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
    *       <code>data</code> URI scheme (instead of http or https) and pass in base-64-encrypted
    *       PNG data, such as that obtained from the
    *       <a href="Subscriber.html#getImgData">Subscriber.getImgData()</a> method. For example,
    *       you could set the property to <code>"data:VBORw0KGgoAA..."</code>, where the portion of
    *       the string after <code>"data:"</code> is the result of a call to
    *       <code>Publisher.getImgData()</code>. If the URL or the image data is invalid, the
-   *       property is ignored (the attempt to set the image fails silently).</li>
+   *       property is ignored (the attempt to set the image fails silently).
+   *       <p>
+   *       Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+   *       you cannot set the <code>backgroundImageURI</code> style to a string larger than
+   *       32&nbsp;kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+   *       limitation, you cannot set the <code>backgroundImageURI</code> style to a string obtained
+   *       with the <code>getImgData()</code> method.
+   *       </p></li>
    *
    *       <li><code>buttonDisplayMode</code> (String) &mdash; How to display the speaker
    *       controls. Possible values are: <code>"auto"</code> (controls are displayed when the
    *       stream is first displayed and when the user mouses over the display), <code>"off"</code>
    *       (controls are not displayed), and <code>"on"</code> (controls are always displayed).</li>
    *
    *       <li><code>nameDisplayMode</code> (String) &#151; Whether to display the stream name.
    *       Possible values are: <code>"auto"</code> (the name is displayed when the stream is first
    *       displayed and when the user mouses over the display), <code>"off"</code> (the name is not
    *       displayed), and <code>"on"</code> (the name is always displayed).</li>
+   *
+   *       <li><code>videoDisabledDisplayMode</code> (String) &#151; 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: <code>"auto"</code>
+   *       (the icons are automatically when the displayed video is disabled or in risk of being
+   *       disabled due to poor stream quality), <code>"off"</code> (do not display the icons), and
+   *       <code>"on"</code> (display the icons).</li>
    *   </ul>
    * </p>
    *
    * <p>For example, the following code passes one parameter to the method:</p>
    *
    * <pre>mySubscriber.setStyle({nameDisplayMode: "off"});</pre>
    *
    * <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are key-value
@@ -16462,21 +17185,23 @@ waitForDomReady();
       'backgroundImageURI',
       'bugDisplayMode'
     ];
 
     _validStyleValues = {
       buttonDisplayMode: ['auto', 'mini', 'mini-auto', 'off', 'on'],
       nameDisplayMode: ['auto', 'off', 'on'],
       bugDisplayMode: ['auto', 'off', 'on'],
+      audioLevelDisplayMode: ['auto', 'off', 'on'],
       showSettingsButton: [true, false],
       showMicButton: [true, false],
       backgroundImageURI: null,
       showControlBar: [true, false],
-      showArchiveStatus: [true, false]
+      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 );
@@ -16542,17 +17267,17 @@ waitForDomReady();
           OT.warn('Style.setAll::Invalid style property passed ' + key + ' : ' + newValue);
         }
       }
 
       return this;
     };
 
     this.set = function(key, value) {
-      OT.debug('Publisher.setStyle: ' + key.toString());
+      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;
       }
@@ -16955,41 +17680,71 @@ waitForDomReady();
         _webRTCStream,
         _session,
         _peerConnections = {},
         _loaded = false,
         _publishProperties,
         _publishStartTime,
         _microphone,
         _chrome,
+        _audioLevelMeter,
         _analytics = new OT.Analytics(),
         _validResolutions,
         _validFrameRates = [ 1, 7, 15, 30 ],
         _prevStats,
         _state,
-        _iceServers;
+        _iceServers,
+        _audioLevelCapable = OT.$.hasCapabilities('webAudio'),
+        _audioLevelSampler;
 
     _validResolutions = {
       '320x240': {width: 320, height: 240},
       '640x480': {width: 640, height: 480},
       '1280x720': {width: 1280, height: 720}
     };
 
     _prevStats = {
       'timeStamp' : OT.$.now()
     };
 
     OT.$.eventing(this);
 
+    if(_audioLevelCapable) {
+      _audioLevelSampler = new OT.AnalyserAudioLevelSampler(new window.AudioContext());
+
+      var publisher = this;
+      var audioLevelRunner = new OT.IntervalRunner(function() {
+        _audioLevelSampler.sample(function(audioInputLevel) {
+          OT.$.requestAnimationFrame(function() {
+            publisher.dispatchEvent(
+              new OT.AudioLevelUpdatedEvent(audioInputLevel));
+          });
+        });
+      }, 60);
+
+      this.on({
+        'audioLevelUpdated:added': function(count) {
+          if (count === 1) {
+            audioLevelRunner.start();
+          }
+        },
+        'audioLevelUpdated:removed': function(count) {
+          if (count === 0) {
+            audioLevelRunner.stop();
+          }
+        }
+      });
+    }
+
     OT.StylableComponent(this, {
-      showMicButton: true,
       showArchiveStatus: true,
       nameDisplayMode: 'auto',
       buttonDisplayMode: 'auto',
       bugDisplayMode: 'auto',
+      audioLevelDisplayMode: 'auto',
       backgroundImageURI: null
     });
 
         /// Private Methods
     var logAnalyticsEvent = function(action, variation, payloadType, payload) {
           _analytics.logEvent({
             action: action,
             variation: variation,
@@ -17001,16 +17756,19 @@ waitForDomReady();
             '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,
@@ -17100,16 +17858,20 @@ waitForDomReady();
             if (err) {
               onLoadFailure.call(this, err);
               return;
             }
 
             onLoaded.call(this);
           }, this));
 
+          if(_audioLevelSampler) {
+            _audioLevelSampler.webOTStream = webOTStream;
+          }
+
         },
 
         onStreamAvailableError = function(error) {
           OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
 
           _state.set('Failed');
           this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
               error.message));
@@ -17184,43 +17946,59 @@ waitForDomReady();
           var browser = OT.$.browserVersion();
 
           this.dispatchEvent(
             new OT.Event(OT.Event.names.ACCESS_DIALOG_OPENED, true),
             function(event) {
               if(!event.isDefaultPrevented()) {
                 if(browser.browser === 'Chrome') {
                   accessDialogChromeTimeout = setTimeout(function() {
+                    accessDialogChromeTimeout = null;
+                    logAnalyticsEvent('allowDenyHelpers', 'show', 'version', 'Chrome');
                     accessDialogPrompt = OT.Dialogs.AllowDeny.Chrome.initialPrompt();
+                    accessDialogPrompt.on('closeButtonClicked', function() {
+                      logAnalyticsEvent('allowDenyHelpers', 'dismissed', 'version', 'Chrome');
+                    });
                   }, 5000);
                 } else if(browser.browser === 'Firefox') {
                   accessDialogFirefoxTimeout = setTimeout(function() {
+                    accessDialogFirefoxTimeout = null;
+                    logAnalyticsEvent('allowDenyHelpers', 'show', 'version', 'Firefox');
                     accessDialogPrompt = OT.Dialogs.AllowDeny.Firefox.maybeDenied();
+                    accessDialogPrompt.on('closeButtonClicked', function() {
+                      logAnalyticsEvent('allowDenyHelpers', 'dismissed', 'version', 'Firefox');
+                    });
                   }, 7000);
                 }
+              } else {
+                logAnalyticsEvent('allowDenyHelpers', 'developerPrevented', '', '');
               }
             }
           );
         },
 
         onAccessDialogClosed = function() {
           logAnalyticsEvent('accessDialog', 'Closed', '', '');
 
           if(accessDialogChromeTimeout) {
             clearTimeout(accessDialogChromeTimeout);
+            logAnalyticsEvent('allowDenyHelpers', 'notShown', 'version', 'Chrome');
             accessDialogChromeTimeout = null;
           }
 
           if(accessDialogFirefoxTimeout) {
             clearTimeout(accessDialogFirefoxTimeout);
+            logAnalyticsEvent('allowDenyHelpers', 'notShown', 'version', 'Firefox');
             accessDialogFirefoxTimeout = null;
           }
 
           if(accessDialogPrompt) {
             accessDialogPrompt.close();
+            var browser = OT.$.browserVersion();
+            logAnalyticsEvent('allowDenyHelpers', 'closed', 'version', browser.browser);
             accessDialogPrompt = null;
           }
 
           this.dispatchEvent(
             new OT.Event(OT.Event.names.ACCESS_DIALOG_CLOSED, false)
           );
         },
 
@@ -17360,71 +18138,93 @@ waitForDomReady();
         },
 
         updateChromeForStyleChange = function(key, value) {
           if (!_chrome) return;
 
           switch(key) {
             case 'nameDisplayMode':
               _chrome.name.setDisplayMode(value);
-              _chrome.backingBar.nameMode = value;
+              _chrome.backingBar.setNameMode(value);
               break;
 
             case 'showArchiveStatus':
               logAnalyticsEvent('showArchiveStatus', 'styleChange', 'mode', value ? 'on': 'off');
               _chrome.archive.setShowArchiveStatus(value);
               break;
 
             case 'buttonDisplayMode':
-            case 'showMicButton':
-              // _chrome.muteButton.setDisplayMode(
-              //     chromeButtonMode.call(this, this.getStyle('showMicButton'))
-              // );
+              _chrome.muteButton.setDisplayMode(value);
+              _chrome.backingBar.setMuteMode(value);
+              break;
+
+            case 'audioLevelDisplayMode':
+              _chrome.audioLevel.setDisplayMode(value);
+              break;
+
             case 'bugDisplayMode':
-              // _chrome.name.bugMode = value;
+              // bugDisplayMode can't be updated but is used by some partners
+
+            case 'backgroundImageURI':
+              _container.setBackgroundImageURI(value);
           }
         },
 
         _createChrome = function() {
+
           if(this.getStyle('bugDisplayMode') === 'off') {
             logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off');
           }
           if(!this.getStyle('showArchiveStatus')) {
             logAnalyticsEvent('showArchiveStatus', 'createChrome', 'mode', 'off');
           }
-          _chrome = new OT.Chrome({
-            parent: _container.domElement
-          }).set({
-
+
+          var widgets = {
             backingBar: new OT.Chrome.BackingBar({
-              nameMode: this.getStyle('nameDisplayMode'),
-              muteMode: chromeButtonMode.call(this, this.getStyle('showMicButton'))
+              nameMode: !_publishProperties.name ? 'off' : this.getStyle('nameDisplayMode'),
+              muteMode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
             }),
 
             name: new OT.Chrome.NamePanel({
               name: _publishProperties.name,
               mode: this.getStyle('nameDisplayMode'),
               bugMode: this.getStyle('bugDisplayMode')
             }),
 
             muteButton: new OT.Chrome.MuteButton({
               muted: _publishProperties.publishAudio === false,
-              mode: chromeButtonMode.call(this, this.getStyle('showMicButton'))
+              mode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
             }),
 
             opentokButton: new OT.Chrome.OpenTokButton({
               mode: this.getStyle('bugDisplayMode')
             }),
 
             archive: new OT.Chrome.Archiving({
               show: this.getStyle('showArchiveStatus'),
               archiving: false
             })
-
-          }).on({
+          };
+
+          if(_audioLevelCapable) {
+            _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
+              mode: this.getStyle('audioLevelDisplayMode')
+            });
+
+            var audioLevelTransformer = new OT.AudioLevelTransformer();
+            this.on('audioLevelUpdated', function(evt) {
+              _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
+            });
+
+            widgets.audioLevel = _audioLevelMeter;
+          }
+
+          _chrome = new OT.Chrome({
+            parent: _container.domElement
+          }).set(widgets).on({
             muted: OT.$.bind(this.publishAudio, this, false),
             unmuted: OT.$.bind(this.publishAudio, this, true)
           });
         },
 
         reset = OT.$.bind(function() {
           if (_chrome) {
             _chrome.destroy();
@@ -17550,17 +18350,17 @@ waitForDomReady();
                 _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 = {};
@@ -17686,16 +18486,17 @@ waitForDomReady();
       if (_webRTCStream) {
         var videoTracks = _webRTCStream.getVideoTracks();
         for (var i=0, num=videoTracks.length; i<num; ++i) {
           videoTracks[i].setEnabled(value);
         }
       }
 
       if(_container) {
+        _container.audioOnly(!value);
         _container.showPoster(!value);
       }
 
       return this;
     };
 
 
     /**
@@ -18025,16 +18826,50 @@ waitForDomReady();
 	* 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 <code>audioLevel</code> property
+    * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
+    * information.
+    * <p>
+    * 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:
+    * <p>
+    * <pre>
+    * 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;
+    * });
+    * </pre>
+    * <p>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 <a href="Session.html#publish">Session.publish()</a>
 	 */
@@ -18081,29 +18916,34 @@ waitForDomReady();
  * @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 = 50,
+        _audioVolume = 100,
         _state,
-        _subscribeAudioFalseWorkaround, // OPENTOK-6844
         _prevStats,
-        _lastSubscribeToVideoReason;
+        _lastSubscribeToVideoReason,
+        _audioLevelCapable =  OT.$.hasCapabilities('audioOutputLevelStat') ||
+                              OT.$.hasCapabilities('webAudioCapableRemoteStream'),
+        _audioLevelSampler,
+        _audioLevelRunner,
+        _frameRateRestricted = false;
 
     this.id = _domId;
     this.widgetId = _widgetId;
     this.session = _session;
 
     _prevStats = {
       timeStamp: OT.$.now()
     };
@@ -18112,24 +18952,42 @@ waitForDomReady();
       OT.handleJsException('Subscriber must be passed a session option', 2000, {
         session: _session,
         target: this
       });
 
       return;
     }
 
-    OT.$.eventing(this);
+    OT.$.eventing(this, false);
+
+    if(_audioLevelCapable) {
+      this.on({
+        'audioLevelUpdated:added': function(count) {
+          if (count === 1 && _audioLevelRunner) {
+            _audioLevelRunner.start();
+          }
+        },
+        'audioLevelUpdated:removed': function(count) {
+          if (count === 0 && _audioLevelRunner) {
+            _audioLevelRunner.stop();
+          }
+        }
+      });
+    }
 
     OT.StylableComponent(this, {
       nameDisplayMode: 'auto',
       buttonDisplayMode: 'auto',
+      audioLevelDisplayMode: 'auto',
+      videoDisabledIndicatorDisplayMode: 'auto',
       backgroundImageURI: null,
       showArchiveStatus: true,
-      showMicButton: true
+      showMicButton: true,
+      bugDisplayMode: 'auto'
     });
 
     var logAnalyticsEvent = function(action, variation, payloadType, payload) {
           /* jshint camelcase:false*/
           _analytics.logEvent({
             action: action,
             variation: variation,
             payload_type: payloadType,
@@ -18183,24 +19041,22 @@ waitForDomReady();
           _state.set('Subscribing');
           _subscribeStartTime = OT.$.now();
 
           logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
             parseInt(_subscribeStartTime - _startConnectingTime, 10),
             _peerConnection && _peerConnection.hasRelayCandidates()
           ].join('|'));
 
-          if(_subscribeAudioFalseWorkaround) {
-            _subscribeAudioFalseWorkaround = null;
-            this.subscribeToVideo(false);
-          }
-
           _container.loading(false);
 
           _createChrome.call(this);
+          if(_frameRateRestricted) {
+            _stream.setRestrictFrameRate(true);
+          }
 
           this.trigger('subscribeComplete', null, this);
           this.trigger('loaded', this);
 
           logAnalyticsEvent('subscribe', 'Success', 'streamId', _stream.id);
         },
 
         onDisconnected = function() {
@@ -18216,17 +19072,17 @@ waitForDomReady();
 
             // we were disconnected after we were already subscribing
             // probably do nothing?
           }
 
           this.disconnect();
         },
 
-        onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
+        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('|'));
 
@@ -18246,32 +19102,31 @@ waitForDomReady();
 
           OT.handleJsException('Subscriber PeerConnection Error: ' + reason,
             OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
               session: _session,
               target: this
             }
           );
           _showError.call(this, reason);
-        },
+        }, this),
 
         onRemoteStreamAdded = function(webOTStream) {
           OT.debug('OT.Subscriber.onRemoteStreamAdded');
 
           _state.set('BindingRemoteStream');
 
           // Disable the audio/video, if needed
           this.subscribeToAudio(_properties.subscribeToAudio);
 
-          var preserver = _subscribeAudioFalseWorkaround;
-          this.subscribeToVideo(_properties.subscribeToVideo);
-          _subscribeAudioFalseWorkaround = preserver;
+          _lastSubscribeToVideoReason = 'loading';
+          this.subscribeToVideo(_properties.subscribeToVideo, 'loading');
 
           var videoContainerOptions = {
-            error: OT.$.bind(onPeerConnectionFailure, this),
+            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,
@@ -18283,18 +19138,17 @@ waitForDomReady();
               reenableVideoTrack = tracks[0];
             }
           }
 
           _streamContainer = _container.bindVideo(webOTStream,
                                               videoContainerOptions,
                                               OT.$.bind(function(err) {
             if (err) {
-              onPeerConnectionFailure.call(this, null, err.message || err, _peerConnection,
-                'VideoElement');
+              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;
             }
 
@@ -18302,16 +19156,20 @@ waitForDomReady();
               width: _stream.videoDimensions.width,
               height: _stream.videoDimensions.height,
               videoOrientation: _stream.videoDimensions.orientation
             });
 
             onLoaded.call(this, null);
           }, this));
 
+          if (OT.$.hasCapabilities('webAudioCapableRemoteStream') && _audioLevelSampler) {
+            _audioLevelSampler.webOTStream = webOTStream;
+          }
+
           logAnalyticsEvent('createPeerConnection', 'StreamAdded', '', '');
           this.trigger('streamAdded', this);
         },
 
         onRemoteStreamRemoved = function(webOTStream) {
           OT.debug('OT.Subscriber.onStreamRemoved');
 
           if (_streamContainer.stream === webOTStream) {
@@ -18333,20 +19191,33 @@ waitForDomReady();
             case 'videoDimensions':
               _streamContainer.orientation({
                 width: event.newValue.width,
                 height: event.newValue.height,
                 videoOrientation: event.newValue.orientation
               });
               break;
 
+            case 'videoDisableWarning':
+              _chrome.videoDisabledIndicator.setWarning(event.newValue);
+              this.dispatchEvent(new OT.VideoDisableWarningEvent(
+                event.newValue ? 'videoDisableWarning' : 'videoDisableWarningLifted'
+              ));
+              break;
+
             case 'hasVideo':
               if(_container) {
-                _container.showPoster(!(_stream.hasVideo && _properties.subscribeToVideo));
+                var audioOnly = !(_stream.hasVideo && _properties.subscribeToVideo);
+                _container.audioOnly(audioOnly);
+                _container.showPoster(audioOnly);
               }
+              this.dispatchEvent(new OT.VideoEnabledChangedEvent(
+                _stream.hasVideo ? 'videoEnabled' : 'videoDisabled', {
+                reason: 'publishVideo'
+              }));
               break;
 
             case 'hasAudio':
               // noop
           }
         },
 
         /// Chrome
@@ -18367,40 +19238,53 @@ waitForDomReady();
         },
 
         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.muteButton.setDisplayMode(value);
+              _chrome.backingBar.setMuteMode(value);
+              break;
+
+            case 'audioLevelDisplayMode':
+              _chrome.audioLevel.setDisplayMode(value);
+              break;
 
             case 'bugDisplayMode':
-              // _chrome.name.bugMode = value;
+              // bugDisplayMode can't be updated but is used by some partners
+
+            case 'backgroundImageURI':
+              _container.setBackgroundImageURI(value);
           }
         },
 
         _createChrome = function() {
+          
           if(this.getStyle('bugDisplayMode') === 'off') {
             logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off');
           }
-          _chrome = new OT.Chrome({
-            parent: _container.domElement
-          }).set({
-
+
+          var widgets = {
             backingBar: new OT.Chrome.BackingBar({
-              nameMode: this.getStyle('nameDisplayMode'),
+              nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'),
               muteMode: chromeButtonMode.call(this, this.getStyle('showMuteButton'))
             }),
 
             name: new OT.Chrome.NamePanel({
               name: _properties.name,
               mode: this.getStyle('nameDisplayMode'),
               bugMode: this.getStyle('bugDisplayMode')
             }),
@@ -18413,18 +19297,38 @@ waitForDomReady();
             opentokButton: new OT.Chrome.OpenTokButton({
               mode: this.getStyle('bugDisplayMode')
             }),
 
             archive: new OT.Chrome.Archiving({
               show: this.getStyle('showArchiveStatus'),
               archiving: false
             })
-
-          }).on({
+          };
+
+          if(_audioLevelCapable) {
+            _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
+              mode: this.getStyle('audioLevelDisplayMode')
+            });
+
+            var audioLevelTransformer = new OT.AudioLevelTransformer();
+            this.on('audioLevelUpdated', function(evt) {
+              _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
+            });
+
+            widgets.audioLevel = _audioLevelMeter;
+          }
+
+          widgets.videoDisabledIndicator = new OT.Chrome.VideoDisabledIndicator({
+            mode: this.getStyle('videoDisabledDisplayMode')
+          });
+
+          _chrome = new OT.Chrome({
+            parent: _container.domElement
+          }).set(widgets).on({
             muted: function() {
               muteAudio.call(this, true);
             },
 
             unmuted: function() {
               muteAudio.call(this, false);
             }
           }, this);
@@ -18485,21 +19389,16 @@ waitForDomReady();
 
       _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;
 
-      if(!_properties.subscribeToVideo && OT.$.browser() === 'Chrome') {
-        _subscribeAudioFalseWorkaround = true;
-        _properties.subscribeToVideo = true;
-      }
-
       _startConnectingTime = OT.$.now();
 
       if (_stream.connection.id !== _session.connection.id) {
         logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
 
         _state.set('ConnectingToPeer');
 
         _peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session,
@@ -18510,16 +19409,38 @@ waitForDomReady();
           error: onPeerConnectionFailure,
           remoteStreamAdded: onRemoteStreamAdded,
           remoteStreamRemoved: onRemoteStreamRemoved,
           qos: recordQOS
         }, this);
 
         // initialize the peer connection AFTER we've added the event listeners
         _peerConnection.init();
+
+        if (OT.$.hasCapabilities('audioOutputLevelStat')) {
+          _audioLevelSampler = new OT.GetStatsAudioLevelSampler(_peerConnection, 'out');
+        } else if (OT.$.hasCapabilities('webAudioCapableRemoteStream')) {
+          _audioLevelSampler = new OT.AnalyserAudioLevelSampler(new window.AudioContext());
+        }
+
+        if(_audioLevelSampler) {
+          var subscriber = this;
+          // sample with interval to minimise disturbance on animation loop but dispatch the
+          // event with RAF since the main purpose is animation of a meter
+          _audioLevelRunner = new OT.IntervalRunner(function() {
+            _audioLevelSampler.sample(function(audioOutputLevel) {
+              if (audioOutputLevel !== null) {
+                OT.$.requestAnimationFrame(function() {
+                  subscriber.dispatchEvent(
+                    new OT.AudioLevelUpdatedEvent(audioOutputLevel));
+                });
+              }
+            });
+          }, 60);
+        }
       } else {
         logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
 
         var publisher = _session.getPublisherForStream(_stream);
         if(!(publisher && publisher._.webRtcStream())) {
           this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID'));
           return this;
         }
@@ -18541,16 +19462,20 @@ waitForDomReady();
           // 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) {
@@ -18619,23 +19544,26 @@ waitForDomReady();
     };
 
     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');
-      this.dispatchEvent(new OT.Event(active ? 'videoEnabled' : 'videoDisabled'));
+      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.
      *
      *  <p>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:</p>
@@ -18780,16 +19708,22 @@ waitForDomReady();
         } 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 <code>value</code> is <code>true</code>;
     * stops subscribing to video (if it is currently being subscribed to) when the
     * <code>value</code> is <code>false</code>.
     * <p>
     * <i>Note:</i> This method only affects the local playback of video. It has no impact on
@@ -18808,46 +19742,55 @@ waitForDomReady();
     * @see <a href="#subscribeToAudio">subscribeToAudio()</a>
     * @see <a href="Session.html#subscribe">Session.subscribe()</a>
     * @see <a href="StreamPropertyChangedEvent.html">StreamPropertyChangedEvent</a>
     *
     * @method #subscribeToVideo
     * @memberOf Subscriber
     */
     this.subscribeToVideo = function(pValue, reason) {
-      if(_subscribeAudioFalseWorkaround && pValue === true) {
-        // Turn off the workaround if they enable the video
-        _subscribeAudioFalseWorkaround = false;
-        return;
-      }
-
       var value = OT.$.castToBoolean(pValue, true);
 
       if(_container) {
-        _container.showPoster(!(value && _stream.hasVideo));
+        var audioOnly = !(value && _stream.hasVideo);
+        _container.audioOnly(audioOnly);
+        _container.showPoster(audioOnly);
         if(value && _container.video()) {
           _container.loading(value);
           _container.video().whenTimeIncrements(function(){
             _container.loading(false);
           }, this);
         }
       }
 
+      if (_chrome && _chrome.videoDisabledIndicator) {
+        _chrome.videoDisabledIndicator.disableVideo(false);
+      }
+
       if (_peerConnection) {
         _peerConnection.subscribeToVideo(value);
 
         if (_session && _stream && (value !== _properties.subscribeToVideo ||
             reason !== _lastSubscribeToVideoReason)) {
           _stream.setChannelActiveState('video', value, reason);
         }
       }
 
       _properties.subscribeToVideo = value;
       _lastSubscribeToVideoReason = reason;
 
+      if (reason !== 'loading') {
+        this.dispatchEvent(new OT.VideoEnabledChangedEvent(
+          value ? 'videoEnabled' : 'videoDisabled',
+          {
+            reason: reasonMap[reason] || 'subscribeToVideo'
+          }
+        ));
+      }
+
       return this;
     };
 
     this.isSubscribing = function() {
       return _state.isSubscribing();
     };
 
     this.isWebRTC = true;
@@ -18905,16 +19848,17 @@ waitForDomReady();
 
       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._ = {
@@ -18922,53 +19866,175 @@ waitForDomReady();
         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 <code>audioLevel</code> property
+   * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
+   * information.
+   * <p>
+   * 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:
+   * <pre>
+   * 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;
+   * });
+   * </pre>
+   * <p>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 OpenTok Media Router stops sending video to the subscriber.
-	* 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.
+	* Dispatched when the video for the subscriber is disabled.
+	* <p>
+	* The <code>reason</code> property defines the reason the video was disabled. This can be set to
+	* one of the following values:
 	* <p>
-	* If connectivity improves to support video again, the Subscriber object dispatches
-	* a videoEnabled event, and the Subscriber resumes receiving video.
+	*
+	* <ul>
+	*
+	*   <li><code>"publishVideo"</code> &mdash; The publisher stopped publishing video by calling
+	*   <code>publishVideo(false)</code>.</li>
+	*
+	*   <li><code>"quality"</code> &mdash; 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.)
+	*   <p>
+	*   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 <code>videoDisableWarning</code> event.
+	*   <p>
+	*   If connectivity improves to support video again, the Subscriber object dispatches
+	*   a <code>videoEnabled</code> event, and the Subscriber resumes receiving video.
+	*   <p>
+	*   By default, the Subscriber displays a video disabled indicator when a
+	*   <code>videoDisabled</code> event with this reason is dispatched and removes the indicator
+	*   when the <code>videoDisabled</code> event with this reason is dispatched. You can control
+	*   the display of this icon by calling the <code>setStyle()</code> method of the Subscriber,
+	*   setting the <code>videoDisabledDisplayMode</code> property(or you can set the style when
+	*   calling the <code>Session.subscribe()</code> method, setting the <code>style</code> property
+	*   of the <code>properties</code> parameter).
+	*   <p>
+	*   This feature is only available in sessions that use the OpenTok Media Router (sessions with
+	*   the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
+	*   set to routed), not in sessions with the media mode set to relayed.
+	*   </li>
+	*
+	*   <li><code>"subscribeToVideo"</code> &mdash; The subscriber started or stopped subscribing to
+	*   video, by calling <code>subscribeToVideo(false)</code>.
+	*   </li>
+	*
+	* </ul>
+	*
+	* @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 <code>videoDisabled</code> event.
+	* <p>
+	* 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 <code>setStyle()</code> method and setting the
+	* <code>videoDisabledDisplayMode</code> property (or you can set the style when calling
+	* the <code>Session.subscribe()</code> method and setting the <code>style</code> property
+	* of the <code>properties</code> parameter).
 	* <p>
 	* This feature is only available in sessions that use the OpenTok Media Router (sessions with
 	* the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
 	* set to routed), not in sessions with the media mode set to relayed.
 	*
 	* @see Event
-	* @see event:videoEnabled
-	* @name videoDisabled
+	* @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 <code>videoDisableWarning</code> event.
+	* <p>
+	* This feature is only available in sessions that use the OpenTok Media Router (sessions with
+	* the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
+	* 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.
 	* <p>
-	* The OpenTok Media Router has a subscriber drop the video stream when connectivity
-	* degrades (and the Subscriber dispatches a videoDisabled event). When the connectivity
-	* improves to support video the Subscriber dispatches the videoEnabled event and
-	* video resumes.
+	* The <code>reason</code> property defines the reason the video was enabled. This can be set to
+	* one of the following values:
+	* <p>
+	*
+	* <ul>
+	*
+	*   <li><code>"publishVideo"</code> &mdash; The publisher started publishing video by calling
+	*   <code>publishVideo(true)</code>.</li>
+	*
+	*   <li><code>"quality"</code> &mdash; 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.
+	*   <p>
+	*   This feature is only available in sessions that use the OpenTok Media Router (sessions with
+	*   the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
+	*   set to routed), not in sessions with the media mode set to relayed.
+	*   </li>
+	*
+	*   <li><code>"subscribeToVideo"</code> &mdash; The subscriber started or stopped subscribing to
+	*   video, by calling <code>subscribeToVideo(false)</code>.
+	*   </li>
+	*
+	* </ul>
+	*
 	* <p>
 	* To prevent video from resuming, in the <code>videoEnabled</code> event listener,
 	* call <code>subscribeToVideo(false)</code> on the Subscriber object.
-	* <p>
-	* This feature is only available in sessions that use the OpenTok Media Router (sessions with
-	* the <a href="http://tokbox.com/opentok/tutorials/create-session/#media-mode">media mode</a>
-	* set to routed, not in sessions with the media mode set to relayed.
 	*
-	* @see Event
+	* @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
@@ -18999,27 +20065,27 @@ waitForDomReady();
     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 &&
+    this.p2pEnabled = !!(sessionJSON.properties &&
       sessionJSON.properties.p2p &&
       sessionJSON.properties.p2p.preference &&
-      sessionJSON.properties.p2p.preference.value === 'enabled';
+      sessionJSON.properties.p2p.preference.value === 'enabled');
   };
 
   // Retrieves Session Info for +session+. The SessionInfo object will be passed
   // to the +onSuccess+ callback. The +onFailure+ callback will be passed an error
   // object and the DOMEvent that relates to the error.
   OT.SessionInfo.get = function(session, onSuccess, onFailure) {
-    var sessionInfoURL = OT.properties.apiURLSSL + '/session/' + session.id + '?extended=true',
+    var sessionInfoURL = OT.properties.apiURL + '/session/' + session.id + '?extended=true',
 
         browser = OT.$.browserVersion(),
 
         startTime = OT.$.now(),
 
         options,
 
         validateRawSessionInfo = function(sessionInfo) {
@@ -19044,17 +20110,17 @@ waitForDomReady();
       options = {
         headers: {
           'X-TB-TOKEN-AUTH': session.token,
           'X-TB-VERSION': 1
         }
       };
     }
 
-    session.logEvent('getSessionInfo', 'Attempt', 'api_url', OT.properties.apiURLSSL);
+    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 {
@@ -19096,17 +20162,17 @@ waitForDomReady();
       return {
         code: null,
         message: 'Unknown error: getSessionInfo JSON response was badly formed'
       };
     }
   };
 
   onGetResponseCallback = function(session, onSuccess, rawSessionInfo) {
-    session.logEvent('getSessionInfo', 'Success', 'api_url', OT.properties.apiURLSSL);
+    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'));
@@ -19138,24 +20204,23 @@ waitForDomReady();
    * <code>Session.forceUnpublish()</code> method, the user must have a token that
    * is assigned the role of moderator.
 	 * @property {Number} publish Specifies whether you can publish to the session (1) or not (0).
    * The ability to publish is based on a few factors. To publish, the user must have a token that
    * is assigned a role that supports publishing. There must be a connected camera and microphone.
 	 * @property {Number} subscribe Specifies whether you can subscribe to streams
    * in the session (1) or not (0). Currently, this capability is available for all users on all
    * platforms.
-   * @property {Number} supportsWebRTC Whether the client supports WebRTC (1) or not (0).
 	 */
 	OT.Capabilities = function(permissions) {
 	    this.publish = 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.$.supportsWebRTC() ? 1 : 0;
+	    this.supportsWebRTC = OT.$.hasCapabilities('webrtc') ? 1 : 0;
 
       this.permittedTo = function(action) {
         return this.hasOwnProperty(action) && this[action] === 1;
       };
     };
 
 })(window);
 !(function(window) {
@@ -19324,16 +20389,20 @@ waitForDomReady();
       }
     };
 
     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,
@@ -19562,40 +20631,26 @@ waitForDomReady();
         this.connection.id;
       else if (_connectionId) event.connection_id = _connectionId;
 
       if (options) event = OT.$.extend(options, event);
       _analytics.logEvent(event);
     };
 
  /**
- * Connects to an OpenTok session. Pass your API key as the <code>apiKey</code> parameter.
- * You get an API key when you <a href="https://dashboard.tokbox.com/users/sign_in">sign up</a>
- * for an OpenTok account. Pass a token string as the <code>token</code> parameter. You generate
- * a token using the
- * <a href="/opentok/api/tools/documentation/api/server_side_libraries.html">OpenTok server-side
- * libraries</a> or the <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page. For
- * more information, see <a href="/opentok/tutorials/create-token/">Connection token creation</a>.
+ * Connects to an OpenTok session.
  * <p>
- * Upon a successful connection, the Session object dispatches a <code>sessionConnected</code>
- * event. Call the <code>on()</code> method to set up an event handler to process this event before
- * calling other methods of the Session object.
- *  </p>
+ *  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.
+ * </p>
  *  <p>
- *    The Session object dispatches a <code>connectionCreated</code> event when other clients
- *    create connections to the session.
- *  </p>
- *  <p>
- *    The OT object dispatches an <code>exception</code> event if the session ID,
- *    API key, or token string are invalid. See <a href="ExceptionEvent.html">ExceptionEvent</a>
- *    and <a href="OT.html#on">OT.on()</a>.
- *  </p>
- *  <p>
- *    The application throws an error if the system requirements are not met
- *    (see <a href="OT.html#checkSystemRequirements">OT.checkSystemRequirements()</a>).
+ *    The Session object dispatches a <code>connectionCreated</code> event when any client
+ *    (including your own) connects to to the session.
  *  </p>
  *
  *  <h5>
  *  Example
  *  </h5>
  *  <p>
  *  The following code initializes a session and sets up an event listener for when the session
  *  connects:
@@ -19638,18 +20693,20 @@ waitForDomReady();
  *
   * @param {String} token The session token. You generate a session token using our
   * <a href="/opentok/libraries/server/">server-side libraries</a> or the
   * <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page. For more information, see
   * <a href="/opentok/tutorials/create-token/">Connection token creation</a>.
   *
   * @param {Function} completionHandler (Optional) A function to be called when the call to the
   * <code>connect()</code> method succeeds or fails. This function takes one parameter &mdash;
-  * <code>error</code>. On success, the <code>completionHandler</code> function is not passed any
-  * arguments. On error, the function is passed an <code>error</code> object parameter. The
+  * <code>error</code> (see the <a href="Error.html">Error</a> object).
+  * On success, the <code>completionHandler</code> function is not passed any
+  * arguments. On error, the function is passed an <code>error</code> object parameter
+  * (see the <a href="Error.html">Error</a> object). The
   * <code>error</code> object has two properties: <code>code</code> (an integer) and
   * <code>message</code> (a string), which identify the cause of the failure. The following
   * code adds a <code>completionHandler</code> when calling the <code>connect()</code> method:
   * <pre>
   * session.connect(token, function (error) {
   *   if (error) {
   *       console.log(error.message);
   *   } else {
@@ -19913,17 +20970,18 @@ waitForDomReady();
   * </pre>
   *
   * @param {Publisher} publisher A Publisher object, which you initialize by calling the
   * <a href="OT.html#initPublisher">OT.initPublisher()</a> method.
   *
   * @param {Function} completionHandler (Optional) A function to be called when the call to the
   * <code>publish()</code> method succeeds or fails. This function takes one parameter &mdash;
   * <code>error</code>. On success, the <code>completionHandler</code> function is not passed any
-  * arguments. On error, the function is passed an <code>error</code> object parameter. The
+  * arguments. On error, the function is passed an <code>error</code> object parameter
+  * (see the <a href="Error.html">Error</a> object). The
   * <code>error</code> object has two properties: <code>code</code> (an integer) and
   * <code>message</code> (a string), which identify the cause of the failure. Calling
   * <code>publish()</code> fails if the role assigned to your token is not "publisher" or
   * "moderator"; in this case <code>error.code</code> is set to 1500. Calling
   * <code>publish()</code> also fails the client fails to connect; in this case
   * <code>error.code</code> is set to 1013. The following code adds a
   * <code>completionHandler</code> when calling the <code>publish()</code> method:
   * <pre>
@@ -20200,37 +21258,58 @@ waitForDomReady();
   *         </ul>
   *       </li>
   *
   *   <li>
   *   <code>style</code> (Object) &#151; An object containing properties that define the initial
   *   appearance of user interface controls of the Subscriber. The <code>style</code> object
   *   includes the following properties:
   *     <ul>
-  *       <li><code>backgroundImageURI</code> (String) &mdash; A URI for an image to display as
+  *       <li><code>audioLevelDisplayMode</code> (String) &mdash; How to display the audio level
+  *       indicator. Possible values are: <code>"auto"</code> (the indicator is displayed when the
+  *       video is disabled), <code>"off"</code> (the indicator is not displayed), and
+  *       <code>"on"</code> (the indicator is always displayed).</li>
+  *
+  *       <li><p><code>backgroundImageURI</code> (String) &mdash; 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 <code>subscribeToVideo(false)</code> 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
   *       <code>data</code> URI scheme (instead of http or https) and pass in base-64-encrypted
   *       PNG data, such as that obtained from the
   *       <a href="Subscriber.html#getImgData">Subscriber.getImgData()</a> method. For example,
   *       you could set the property to <code>"data:VBORw0KGgoAA..."</code>, where the portion of
   *       the string after <code>"data:"</code> is the result of a call to
   *       <code>Subscriber.getImgData()</code>. If the URL or the image data is invalid, the
-  *       property is ignored (the attempt to set the image fails silently).</li>
+  *       property is ignored (the attempt to set the image fails silently).
+  *       <p>
+  *       Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+  *       you cannot set the <code>backgroundImageURI</code> style to a string larger than
+  *       32&nbsp;kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+  *       limitation, you cannot set the <code>backgroundImageURI</code> style to a string obtained
+  *       with the <code>getImgData()</code> method.
+  *       </p></li>
   *
   *       <li><code>buttonDisplayMode</code> (String) &mdash; How to display the speaker controls
   *       Possible values are: <code>"auto"</code> (controls are displayed when the stream is first
   *       displayed and when the user mouses over the display), <code>"off"</code> (controls are not
   *       displayed), and <code>"on"</code> (controls are always displayed).</li>
   *
   *       <li><code>nameDisplayMode</code> (String) &#151; Whether to display the stream name.
   *       Possible values are: <code>"auto"</code> (the name is displayed when the stream is first
   *       displayed and when the user mouses over the display), <code>"off"</code> (the name is not
   *       displayed), and <code>"on"</code> (the name is always displayed).</li>
+  *
+  *       <li><code>videoDisabledDisplayMode</code> (String) &#151; 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: <code>"auto"</code> (the icons are automatically when the
+  *       displayed video is disabled or in risk of being disabled due to poor stream quality),
+  *       <code>"off"</code> (do not display the icons), and <code>"on"</code> (display the
+  *       icons). The default setting is <code>"auto"</code></li>
   *   </ul>
   *   </li>
   *
   *       <li><code>subscribeToAudio</code> (Boolean) &#151; Whether to initially subscribe to audio
   *       (if available) for the stream (default: <code>true</code>).</li>
   *
   *       <li><code>subscribeToVideo</code> (Boolean) &#151; Whether to initially subscribe to video
   *       (if available) for the stream (default: <code>true</code>).</li>
@@ -20532,75 +21611,75 @@ waitForDomReady();
 
       streamChannelUpdate: function(stream, channel, attributes) {
         _socket.streamChannelUpdate(stream.id, channel.id, attributes);
       }
     };
 
 
  /**
-  * Sends a signal to each client or specified clients in the session. Specify a
-  * <code>connections</code> property of the <code>signal</code> parameter to limit the
-  * recipients of the signal; otherwise the signal is sent to each client connected to
+  * Sends a signal to each client or a specified client in the session. Specify a
+  * <code>to</code> property of the <code>signal</code> parameter to limit the signal to
+  * be sent to a specific client; otherwise the signal is sent to each client connected to
   * the session.
   * <p>
-  * The following example sends a signal of type "foo" with a specified data payload to all
-  * clients 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:
   * <pre>
   * session.signal({
   *     type: "foo",
   *     data: "hello"
   *   },
   *   function(error) {
   *     if (error) {
-  *       console.log("signal error: " + error.reason);
+  *       console.log("signal error: " + error.message);
   *     } else {
   *       console.log("signal sent");
   *     }
   *   }
   * );
   * </pre>
   * <p>
-  * Calling this method without limiting the set of recipient clients will result in
-  * multiple signals sent (one to each client in the session). For information on charges
-  * for signaling, see the <a href="http://tokbox.com/pricing">OpenTok
-  * pricing</a> page.
+  * Calling this method without specifying a recipient client (by setting the <code>to</code>
+  * property of the <code>signal</code> parameter) results in multiple signals sent (one to each
+  * client in the session). For information on charges for signaling, see the
+  * <a href="http://tokbox.com/pricing">OpenTok pricing</a> page.
   * <p>
-  * The following example sends a signal of type "foo" with a specified data payload to two
-  * specific clients connected to the session:
+  * The following example sends a signal of type "foo" with a data payload ("hello") to a
+  * specific client connected to the session:
   * <pre>
   * session.signal({
   *     type: "foo",
-  *     to: [connection1, connection2]; // connection1 and 2 are Connection objects
+  *     to: recipientConnection; // a Connection object
   *     data: "hello"
   *   },
   *   function(error) {
   *     if (error) {
-  *       console.log("signal error: " + error.reason);
+  *       console.log("signal error: " + error.message);
   *     } else {
   *       console.log("signal sent");
   *     }
   *   }
   * );
   * </pre>
   * <p>
   * Add an event handler for the <code>signal</code> event to listen for all signals sent in
   * the session. Add an event handler for the <code>signal:type</code> event to listen for
   * signals of a specified type only (replace <code>type</code>, in <code>signal:type</code>,
   * with the type of signal to listen for). The Session object dispatches these events. (See
   * <a href="#events">events</a>.)
   *
   * @param {Object} signal An object that contains the following properties defining the signal:
   * <ul>
-  *   <li><code>to</code> &mdash; (Array) An array of <a href="Connection.html">Connection</a>
-  *      objects, corresponding to clients that the message is to be sent to. If you do not
-  *      specify this property, the signal is sent to all clients connected to the session.</li>
   *   <li><code>data</code> &mdash; (String) The data to send. The limit to the length of data
   *     string is 8kB. Do not set the data string to <code>null</code> or
   *     <code>undefined</code>.</li>
+  *   <li><code>to</code> &mdash; (Connection) A <a href="Connection.html">Connection</a>
+  *      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.</li>
   *   <li><code>type</code> &mdash; (String) The type of the signal. You can use the type to
   *     filter signals when setting an event handler for the <code>signal:type</code> event
   *     (where you replace <code>type</code> with the type string). The maximum length of the
   *     <code>type</code> string is 128 characters, and it must contain only letters (A-Z and a-z),
   *     numbers (0-9), '-', '_', and '~'.</li>
   *   </li>
   * </ul>
   *
@@ -20614,40 +21693,37 @@ waitForDomReady();
   * <a href="Error.html">Error</a> class. The <code>error</code> object has the following
   * properties:
   *
   * <ul>
   *   <li><code>code</code> &mdash; (Number) An error code, which can be one of the following:
   *     <table style="width:100%">
   *         <tr>
   *           <td>400</td> <td>One of the signal properties &mdash; data, type, or to &mdash;
-  *                         is invalid. Or the data cannot be parsed as JSON.</td>
+  *                         is invalid.</td>
   *         </tr>
   *         <tr>
-  *           <td>404</td> <td>The to connection does not exist.</td>
+  *           <td>404</td> <td>The client specified by the to property is not connected to
+  *                        the session.</td>
   *         </tr>
   *         <tr>
   *           <td>413</td> <td>The type string exceeds the maximum length (128 bytes),
   *                        or the data string exceeds the maximum size (8 kB).</td>
   *         </tr>
   *         <tr>
   *           <td>500</td> <td>You are not connected to the OpenTok session.</td>
   *         </tr>
   *      </table>
   *   </li>
-  *   <li><code>reason</code> &mdash; (String) A description of the error.</li>
-  *   <li><code>signal</code> &mdash; (Object) An object with properties corresponding to the
-  *     values passed into the <code>signal()</code> method &mdash; <code>data</code>,
-  *     <code>to</code>, and <code>type</code>.
-  *   </li>
+  *   <li><code>message</code> &mdash; (String) A description of the error.</li>
   * </ul>
   *
   * <p>Note that the <code>completionHandler</code> success result (<code>error == null</code>)
   * indicates that the options passed into the <code>Session.signal()</code> method are valid
-  * and the signal was sent. It does <i>not</i> indicate that the signal was sucessfully
+  * and the signal was sent. It does <i>not</i> indicate that the signal was successfully
   * received by any of the intended recipients.
   *
   * @method #signal
   * @memberOf Session
   * @see <a href="#event:signal">signal</a> and <a href="#event:signal:type">signal:type</a> events
   */
     this.signal = function(options, completion) {
       var _options = options,
@@ -20904,17 +21980,17 @@ waitForDomReady();
    * @name archiveStopped
    * @event
    * @memberof Session
    * @see ArchiveEvent
    * @see <a href="http://www.tokbox.com/opentok/tutorials/archiving">Archiving overview</a>.
    */
 
   /**
-   * A new client, other than your own, has connected to the session.
+   * A new client (including your own) has connected to the session.
    * @name connectionCreated
    * @event
    * @memberof Session
    * @see ConnectionEvent
    * @see <a href="OT.html#initSession">OT.initSession()</a>
    */
 
   /**
@@ -20992,25 +22068,33 @@ waitForDomReady();
 	 */
 
 	/**
 	 * A stream has started or stopped publishing audio or video (see
 	 * <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a> and
 	 * <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>); or the
 	 * <code>videoDimensions</code> property of the Stream
 	 * object has changed (see <a href="Stream.html#"videoDimensions>Stream.videoDimensions</a>).
+	 * <p>
+	 * 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 <code>videoDisabled</code>
+	 * and <code>videoEnabled</code> events in all conditions that cause the subscriber's stream
+	 * to be disabled or enabled.
+	 *
 	 * @name streamPropertyChanged
 	 * @event
 	 * @memberof Session
 	 * @see StreamPropertyChangedEvent
 	 * @see <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a>
 	 * @see <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>
 	 * @see <a href="Stream.html#"hasAudio>Stream.hasAudio</a>
 	 * @see <a href="Stream.html#"hasVideo>Stream.hasVideo</a>
 	 * @see <a href="Stream.html#"videoDimensions>Stream.videoDimensions</a>
+	 * @see <a href="Subscriber.html#event:videoDisabled">Subscriber videoDisabled event</a>
+	 * @see <a href="Subscriber.html#event:videoEnabled">Subscriber videoEnabled event</a>
 	 */
 
 	/**
 	 * A signal was received from the session. The <a href="SignalEvent.html">SignalEvent</a>
 	 * class defines this event object. It includes the following properties:
 	 * <ul>
 	 *   <li><code>data</code> &mdash; (String) The data string sent with the signal (if there
 	 *       is one).</li>
@@ -21073,33 +22157,33 @@ waitForDomReady();
 	 * @see <a href="Session.html#signal">Session.signal()</a>
 	 * @see SignalEvent
 	 * @see <a href="#event:signal">signal</a> event
 	 */
   };
 
 })(window);
 (function() {
-  
+
   var txt = function(text) {
     return document.createTextNode(text);
   };
 
   var el = function(attr, children, tagName) {
     var el = OT.$.createElement(tagName || 'div', attr, children);
     el.on = OT.$.bind(OT.$.on, OT.$, el);
     return el;
   };
 
   function DevicePickerController(opts) {
     var destroyExistingPublisher,
         publisher,
         devicesById;
 
-    this.change = function() {
+    this.change = OT.$.bind(function() {
       destroyExistingPublisher();
 
       var settings;
 
       this.pickedDevice = devicesById[opts.selectTag.value];
 
       if(!this.pickedDevice) {
         console.log('No device for', opts.mode, opts.selectTag.value);
@@ -21129,17 +22213,17 @@ waitForDomReady();
         accessAllowed: function() {
         },
         accessDenied: function(event) {
           event.preventDefault();
         }
       });
 
       publisher = pub;
-    }.bind(this);
+    }, this);
 
     this.cleanup = destroyExistingPublisher = function() {
       if(publisher) {
         publisher.destroy();
         publisher = void 0;
       }
     };
 
@@ -21149,27 +22233,27 @@ waitForDomReady();
       opt.setAttribute('disabled', '');
     };
 
     var addDevice = function (device) {
       devicesById[device.deviceId] = device;
       return el({ value: device.deviceId }, txt(device.label), 'option');
     };
 
-    this.setDeviceList = function (devices) {
+    this.setDeviceList = OT.$.bind(function (devices) {
       opts.selectTag.innerHTML = '';
       devicesById = {};
       if(devices.length > 0) {
-        devices.map(addDevice).map(opts.selectTag.appendChild.bind(opts.selectTag));
+        devices.map(addDevice).map(OT.$.bind(opts.selectTag.appendChild, opts.selectTag));
         opts.selectTag.removeAttribute('disabled');
       } else {
         disableSelector(opts.selectTag, 'No devices');
       }
       this.change();
-    }.bind(this);
+    }, this);
 
     this.setLoading = function() {
       disableSelector(opts.selectTag, 'Loading...');
     };
 
     OT.$.on(opts.selectTag, 'change', this.change);
   }
 
@@ -21185,38 +22269,38 @@ waitForDomReady();
     this.audioSource = function() {
       return microphone && microphone.pickedDevice;
     };
 
     this.videoSource = function() {
       return camera && camera.pickedDevice;
     };
 
-    this.destroy = function() {
+    this.destroy = OT.$.bind(function() {
       if(this.is('destroyed')) {
         return;
       }
       if(camera) {
         camera.cleanup();
       }
       if(microphone) {
         microphone.cleanup();
       }
       if(this.is('chooseDevices')) {
         targetElement.parentNode.removeChild(targetElement);
       }
       setState('destroyed');
-    }.bind(this);
+    }, this);
 
     if(targetElement == null) {
       callback(new Error('You must provide a targetElement'));
       return;
     }
 
-    if(!OT.$.canGetMediaDevices()) {
+    if(!OT.$.hasCapabilities('getMediaDevices')) {
       callback(new Error('This browser does not support getMediaDevices APIs'));
       return;
     }
 
     var camSelector,
         camPreview,
         micSelector,
         micPreview,
@@ -21259,17 +22343,17 @@ waitForDomReady();
       selectTag: micSelector,
       previewTag: micPreview,
       mode: 'audioSource'
     });
 
     camera.setLoading();
     microphone.setLoading();
 
-    OT.getDevices(function(error, devices) {
+    OT.getDevices(OT.$.bind(function(error, devices) {
       if (error) {
         callback(error);
         return;
       }
 
       if(this.is('destroyed')) {
         return; // They destroyed us before we got the devices, bail.
       }
@@ -21281,17 +22365,17 @@ waitForDomReady();
       }));
 
       microphone.setDeviceList(devices.filter(function(device) {
         return device.kind === 'audioinput';
       }));
 
       setState('chooseDevices');
 
-    }.bind(this));
+    }, this));
 
     setupDOM = function() {
       var insertMode = options.insertMode;
       if(!(insertMode == null || insertMode === 'replace')) {
         if(insertMode === 'append') {
           targetElement.appendChild(container);
           targetElement = container;
         } else if(insertMode === 'before') {
--- a/browser/components/loop/standalone/Makefile
+++ b/browser/components/loop/standalone/Makefile
@@ -1,14 +1,13 @@
 # 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/.
 
 LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
-LOOP_PENDING_CALL_TIMEOUT := $(shell echo $${LOOP_PENDING_CALL_TIMEOUT-20000})
 NODE_LOCAL_BIN=./node_modules/.bin
 
 install:
 	@npm install
 
 test:
 	@echo "Not implemented yet."
 
@@ -48,9 +47,8 @@ remove_old_config:
 # The services development deployment, however, still wants a static config
 # file, and needs an easy way to generate one.  This target is for folks
 # working with that deployment.
 .PHONY: config
 config:
 	@echo "var loop = loop || {};" > content/config.js
 	@echo "loop.config = loop.config || {};" >> content/config.js
 	@echo "loop.config.serverUrl          = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
-	@echo "loop.config.pendingCallTimeout = `echo $(LOOP_PENDING_CALL_TIMEOUT)`;" >> content/config.js
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -16,19 +16,22 @@ body,
   color: #666;
   text-align: center;
   font-family: Open Sans,sans-serif;
 }
 
 .standalone-header {
   border-radius: 4px;
   background: #fff;
-  padding: 1rem 5rem;
   border: 1px solid #E7E7E7;
   box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.03);
+}
+
+.header-box {
+  padding: 1rem 5rem;
   margin-top: 2rem;
 }
 
 /*
  * Top/Bottom spacing
  **/
 .standalone-footer {
   margin-bottom: 2rem;
@@ -98,26 +101,26 @@ body,
   height: 100px;
   margin: 1rem auto;
   background-image: url("../shared/img/firefox-logo.png");
   background-size: cover;
   background-repeat: no-repeat;
 }
 
 .standalone-header-title,
-.standalone-call-btn-label {
+.standalone-btn-label {
   font-weight: lighter;
 }
 
 .standalone-header-title {
   font-size: 1.8rem;
   line-height: 2.2rem;
 }
 
-.standalone-call-btn-label {
+.standalone-btn-label {
   font-size: 1.2rem;
 }
 
 .light-color-font {
   opacity: .4;
   font-weight: normal;
 }
 
@@ -174,16 +177,20 @@ body,
   .start-audio-only-call:hover {
     background-image: url("../shared/img/audio-inverse-14x14@2x.png");
   }
   .standalone-call-btn-video-icon {
     background-image: url("../shared/img/video-inverse-14x14@2x.png");
   }
 }
 
+.btn-pending-cancel-group > .btn-cancel {
+  flex: 2 1 auto;
+}
+
 .btn-large {
   /* Dimensions from spec
    * https://people.mozilla.org/~dhenein/labs/loop-link-spec/#call-start */
   font-size: 1rem;
   padding: .3em .5rem;
 }
 
   .btn-large + .btn-chevron {
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -115,16 +115,26 @@ loop.webapp = (function($, _, OT, mozL10
           ), 
           PromoteFirefoxView({helper: this.props.helper})
         )
       );
       /* jshint ignore:end */
     }
   });
 
+  var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
+    render: function() {
+      return (
+        React.DOM.h1({className: "standalone-header-title"}, 
+          React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
+        )
+      );
+    }
+  });
+
   var ConversationHeader = React.createClass({displayName: 'ConversationHeader',
     render: function() {
       var cx = React.addons.classSet;
       var conversationUrl = location.href;
 
       var urlCreationDateClasses = cx({
         "light-color-font": true,
         "call-url-date": true, /* Used as a handler in the tests */
@@ -133,20 +143,18 @@ loop.webapp = (function($, _, OT, mozL10
       });
 
       var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
         "call_url_creation_date": this.props.urlCreationDateString
       });
 
       return (
         /* jshint ignore:start */
-        React.DOM.header({className: "standalone-header container-box"}, 
-          React.DOM.h1({className: "standalone-header-title"}, 
-            React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
-          ), 
+        React.DOM.header({className: "standalone-header header-box container-box"}, 
+          ConversationBranding(null), 
           React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), 
           React.DOM.h3({className: "call-url"}, 
             conversationUrl
           ), 
           React.DOM.h4({className: urlCreationDateClasses}, 
             callUrlCreationDateString
           )
         )
@@ -160,16 +168,78 @@ loop.webapp = (function($, _, OT, mozL10
       return (
         React.DOM.div({className: "standalone-footer container-box"}, 
           React.DOM.div({title: "Mozilla Logo", className: "footer-logo"})
         )
       );
     }
   });
 
+  var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
+    getInitialState: function() {
+      return {
+        callState: this.props.callState || "connecting"
+      }
+    },
+
+    propTypes: {
+      websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
+                      .isRequired
+    },
+
+    componentDidMount: function() {
+      this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
+                                    this._handleRingingProgress);
+    },
+
+    _handleRingingProgress: function() {
+      this.setState({callState: "ringing"});
+    },
+
+    _cancelOutgoingCall: function() {
+      this.props.websocket.cancel();
+    },
+
+    render: function() {
+      var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
+      return (
+        /* jshint ignore:start */
+        React.DOM.div({className: "container"}, 
+          React.DOM.div({className: "container-box"}, 
+            React.DOM.header({className: "pending-header header-box"}, 
+              ConversationBranding(null)
+            ), 
+
+            React.DOM.div({id: "cameraPreview"}), 
+
+            React.DOM.div({id: "messages"}), 
+
+            React.DOM.p({className: "standalone-btn-label"}, 
+              callState
+            ), 
+
+            React.DOM.div({className: "btn-pending-cancel-group btn-group"}, 
+              React.DOM.div({className: "flex-padding-1"}), 
+              React.DOM.button({className: "btn btn-large btn-cancel", 
+                      onClick: this._cancelOutgoingCall}, 
+                React.DOM.span({className: "standalone-call-btn-text"}, 
+                  mozL10n.get("initiate_call_cancel_button")
+                )
+              ), 
+              React.DOM.div({className: "flex-padding-1"})
+            )
+          ), 
+
+          ConversationFooter(null)
+        )
+        /* jshint ignore:end */
+      );
+    }
+  });
+
   /**
    * Conversation launcher view. A ConversationModel is associated and attached
    * as a `model` property.
    */
<