Merge mc -> pine draft
authorGregor Wagner <anygregor@gmail.com>
Sat, 09 Aug 2014 17:17:20 -0700
changeset 388157 5637b3c226d85367bb39acb04b16d1f82c8c6171
parent 388156 7cab4c5c56c5d28035ed6b329ff232c3d2c629f6 (current diff)
parent 198777 70be728521e32ff8bfa276fe275c0238c722cf1a (diff)
child 388158 17881d7828e7450d6ff3e0a07213d6ce0aa81092
push id23132
push userbmo:lissyx+mozillians@lissyx.dyndns.org
push dateFri, 15 Jul 2016 10:07:12 +0000
milestone34.0a1
Merge mc -> pine
b2g/app/b2g.js
browser/fuel/public/fuelIApplication.idl
browser/fuel/public/moz.build
browser/fuel/src/fuelApplication.js
browser/fuel/src/fuelApplication.manifest
browser/fuel/src/moz.build
browser/themes/shared/in-content/common.css
browser/themes/shared/incontentprefs/check.png
browser/themes/shared/incontentprefs/check@2x.png
browser/themes/shared/incontentprefs/dropdown-disabled.png
browser/themes/shared/incontentprefs/dropdown-disabled@2x.png
browser/themes/shared/incontentprefs/dropdown.png
browser/themes/shared/incontentprefs/dropdown@2x.png
browser/themes/shared/incontentprefs/help-glyph.png
browser/themes/shared/incontentprefs/help-glyph@2x.png
browser/themes/shared/incontentprefs/sorter.png
browser/themes/shared/incontentprefs/sorter@2x.png
configure.in
dom/ipc/PCOMContentPermissionRequestChild.h
dom/ipc/TabChild.cpp
dom/tests/mochitest/crypto/mochitest-no-legacy.ini
js/src/tests/js1_5/Regress/regress-317533.js
js/src/tests/js1_5/Regress/regress-352197.js
js/src/tests/js1_7/block/regress-347559.js
js/src/vm/Runtime.h
layout/base/tests/marionette/test_touchcaret.py
media/mtransport/test/Makefile.in
media/webrtc/signaling/test/Makefile.in
media/webrtc/webrtc-config.mk
modules/libpref/public/Preferences.h
modules/libpref/public/moz.build
modules/libpref/public/nsIPrefBranch.idl
modules/libpref/public/nsIPrefBranch2.idl
modules/libpref/public/nsIPrefBranchInternal.idl
modules/libpref/public/nsIPrefLocalizedString.idl
modules/libpref/public/nsIPrefService.idl
modules/libpref/public/nsIRelativeFilePref.idl
modules/libpref/src/Makefile.in
modules/libpref/src/Preferences.cpp
modules/libpref/src/init/all.js
modules/libpref/src/moz.build
modules/libpref/src/nsPrefBranch.cpp
modules/libpref/src/nsPrefBranch.h
modules/libpref/src/nsPrefsFactory.cpp
modules/libpref/src/prefapi.cpp
modules/libpref/src/prefapi.h
modules/libpref/src/prefapi_private_data.h
modules/libpref/src/prefread.cpp
modules/libpref/src/prefread.h
profile/dirserviceprovider/public/moz.build
profile/dirserviceprovider/public/nsProfileDirServiceProvider.h
profile/dirserviceprovider/src/moz.build
profile/dirserviceprovider/src/nsProfileDirServiceProvider.cpp
profile/dirserviceprovider/src/nsProfileLock.cpp
profile/dirserviceprovider/src/nsProfileLock.h
profile/dirserviceprovider/src/nsProfileStringTypes.h
profile/public/moz.build
profile/public/notifications.txt
profile/public/nsIProfileUnlocker.idl
widget/gonk/HwcComposer2D.cpp
xpcom/ds/CharTokenizer.h
xpcom/glue/DeadlockDetector.cpp
--- a/Makefile.in
+++ b/Makefile.in
@@ -323,17 +323,18 @@ js/src/export config/host: build/clang-p
 endif
 
 # Interdependencies that moz.build world don't know about yet for compilation.
 # Note some others are hardcoded or "guessed" in recursivemake.py and emitter.py
 ifeq ($(MOZ_WIDGET_TOOLKIT),gtk3)
 toolkit/library/target: widget/gtk/mozgtk/gtk3/target
 endif
 ifdef MOZ_LDAP_XPCOM
-toolkit/library/target: ../ldap/target
+ldap/target: config/external/nss/target mozglue/build/target
+toolkit/library/target: ldap/target
 endif
 ifndef MOZ_FOLD_LIBS
 ifndef MOZ_NATIVE_SQLITE
 config/external/nss/target: db/sqlite3/src/target
 endif
 endif
 ifeq ($(MOZ_REPLACE_MALLOC_LINKAGE),dummy library)
 mozglue/build/target: memory/replace/dummy/target
--- a/accessible/base/DocManager.cpp
+++ b/accessible/base/DocManager.cpp
@@ -32,17 +32,17 @@ using namespace mozilla;
 using namespace mozilla::a11y;
 using namespace mozilla::dom;
 
 ////////////////////////////////////////////////////////////////////////////////
 // DocManager
 ////////////////////////////////////////////////////////////////////////////////
 
 DocManager::DocManager()
-  : mDocAccessibleCache(4)
+  : mDocAccessibleCache(2)
 {
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // DocManager public
 
 DocAccessible*
 DocManager::GetDocAccessible(nsIDocument* aDocument)
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -73,18 +73,18 @@ static const uint32_t kRelationAttrsLen 
 ////////////////////////////////////////////////////////////////////////////////
 // Constructor/desctructor
 
 DocAccessible::
   DocAccessible(nsIDocument* aDocument, nsIContent* aRootContent,
                   nsIPresShell* aPresShell) :
   HyperTextAccessibleWrap(aRootContent, this),
   // XXX aaronl should we use an algorithm for the initial cache size?
-  mAccessibleCache(kDefaultCacheSize),
-  mNodeToAccessibleMap(kDefaultCacheSize),
+  mAccessibleCache(kDefaultCacheLength),
+  mNodeToAccessibleMap(kDefaultCacheLength),
   mDocumentNode(aDocument),
   mScrollPositionChangedTicks(0),
   mLoadState(eTreeConstructionPending), mDocFlags(0), mLoadEventType(0),
   mVirtualCursor(nullptr),
   mPresShell(aPresShell)
 {
   mGenericTypes |= eDocument;
   mStateFlags |= eNotNodeMapEntry;
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -21,17 +21,17 @@
 #include "nsIScrollPositionListener.h"
 #include "nsITimer.h"
 #include "nsIWeakReference.h"
 
 class nsAccessiblePivot;
 
 class nsIScrollableView;
 
-const uint32_t kDefaultCacheSize = 256;
+const uint32_t kDefaultCacheLength = 128;
 
 namespace mozilla {
 namespace a11y {
 
 class DocManager;
 class NotificationController;
 class RelatedAccIterator;
 template<class Class, class Arg>
--- a/accessible/generic/HyperTextAccessible.cpp
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -1541,17 +1541,19 @@ HyperTextAccessible::ScrollSubstringToPo
 
         // avoid divide by zero
         size.width = size.width ? size.width : 1;
         size.height = size.height ? size.height : 1;
 
         int16_t hPercent = offsetPointX * 100 / size.width;
         int16_t vPercent = offsetPointY * 100 / size.height;
 
-        nsresult rv = nsCoreUtils::ScrollSubstringTo(frame, range, vPercent, hPercent);
+        nsresult rv = nsCoreUtils::ScrollSubstringTo(frame, range,
+                                                     nsIPresShell::ScrollAxis(vPercent),
+                                                     nsIPresShell::ScrollAxis(hPercent));
         if (NS_FAILED(rv))
           return;
 
         initialScrolled = true;
       } else {
         // Substring was scrolled to the given point already inside its closest
         // scrollable area. If there are nested scrollable areas then make
         // sure we scroll lower areas to the given point inside currently
--- a/accessible/windows/msaa/nsWinUtils.cpp
+++ b/accessible/windows/msaa/nsWinUtils.cpp
@@ -56,17 +56,17 @@ bool
 nsWinUtils::MaybeStartWindowEmulation()
 {
   // Register window class that'll be used for document accessibles associated
   // with tabs.
   if (Compatibility::IsJAWS() || Compatibility::IsWE() ||
       Compatibility::IsDolphin() ||
       XRE_GetProcessType() == GeckoProcessType_Content) {
     RegisterNativeWindow(kClassNameTabContent);
-    sHWNDCache = new nsRefPtrHashtable<nsPtrHashKey<void>, DocAccessible>(4);
+    sHWNDCache = new nsRefPtrHashtable<nsPtrHashKey<void>, DocAccessible>(2);
     return true;
   }
 
   return false;
 }
 
 void
 nsWinUtils::ShutdownWindowEmulation()
--- a/accessible/xul/XULTreeAccessible.cpp
+++ b/accessible/xul/XULTreeAccessible.cpp
@@ -37,17 +37,17 @@ using namespace mozilla::a11y;
 ////////////////////////////////////////////////////////////////////////////////
 // XULTreeAccessible
 ////////////////////////////////////////////////////////////////////////////////
 
 XULTreeAccessible::
   XULTreeAccessible(nsIContent* aContent, DocAccessible* aDoc,
                     nsTreeBodyFrame* aTreeFrame) :
   AccessibleWrap(aContent, aDoc),
-  mAccessibleCache(kDefaultTreeCacheSize)
+  mAccessibleCache(kDefaultTreeCacheLength)
 {
   mType = eXULTreeType;
   mGenericTypes |= eSelect;
 
   nsCOMPtr<nsITreeView> view = aTreeFrame->GetExistingView();
   mTreeView = view;
 
   mTree = nsCoreUtils::GetTreeBoxObject(aContent);
--- a/accessible/xul/XULTreeAccessible.h
+++ b/accessible/xul/XULTreeAccessible.h
@@ -15,17 +15,17 @@ class nsTreeBodyFrame;
 
 namespace mozilla {
 namespace a11y {
 
 /*
  * A class the represents the XUL Tree widget.
  */
 const uint32_t kMaxTreeColumns = 100;
-const uint32_t kDefaultTreeCacheSize = 256;
+const uint32_t kDefaultTreeCacheLength = 128;
 
 /**
  * Accessible class for XUL tree element.
  */
 
 class XULTreeAccessible : public AccessibleWrap
 {
 public:
--- a/accessible/xul/XULTreeGridAccessible.cpp
+++ b/accessible/xul/XULTreeGridAccessible.cpp
@@ -256,17 +256,17 @@ XULTreeGridAccessible::CreateTreeItemAcc
 // XULTreeGridRowAccessible
 ////////////////////////////////////////////////////////////////////////////////
 
 XULTreeGridRowAccessible::
   XULTreeGridRowAccessible(nsIContent* aContent, DocAccessible* aDoc,
                            Accessible* aTreeAcc, nsITreeBoxObject* aTree,
                            nsITreeView* aTreeView, int32_t aRow) :
   XULTreeItemAccessibleBase(aContent, aDoc, aTreeAcc, aTree, aTreeView, aRow),
-  mAccessibleCache(kDefaultTreeCacheSize)
+  mAccessibleCache(kDefaultTreeCacheLength)
 {
   mGenericTypes |= eTableRow;
 }
 
 XULTreeGridRowAccessible::~XULTreeGridRowAccessible()
 {
 }
 
--- a/addon-sdk/mach_commands.py
+++ b/addon-sdk/mach_commands.py
@@ -21,17 +21,17 @@ from mach.decorators import (
 class JetpackRunner(MozbuildObject):
     """Run jetpack tests."""
     def run_tests(self, **kwargs):
         self._run_make(target='jetpack-tests')
 
 @CommandProvider
 class MachCommands(MachCommandBase):
     @Command('jetpack-test', category='testing',
-        description='Runs the jetpack test suite.')
+        description='Runs the jetpack test suite (Add-on SDK).')
     def run_jetpack_test(self, **params):
         # We should probably have a utility function to ensure the tree is
         # ready to run tests. Until then, we just create the state dir (in
         # case the tree wasn't built with mach).
         self._ensure_state_subdir_exists('.')
 
         jetpack = self._spawn(JetpackRunner)
 
--- a/addon-sdk/source/app-extension/bootstrap.js
+++ b/addon-sdk/source/app-extension/bootstrap.js
@@ -21,16 +21,18 @@ const scriptLoader = Cc['@mozilla.org/mo
 const prefService = Cc['@mozilla.org/preferences-service;1'].
                     getService(Ci.nsIPrefService).
                     QueryInterface(Ci.nsIPrefBranch);
 const appInfo = Cc["@mozilla.org/xre/app-info;1"].
                 getService(Ci.nsIXULAppInfo);
 const vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
            getService(Ci.nsIVersionComparator);
 
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
+
 
 const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable',
                  'install', 'uninstall', 'upgrade', 'downgrade' ];
 
 const bind = Function.call.bind(Function.bind);
 
 let loader = null;
 let unload = null;
--- a/addon-sdk/source/lib/sdk/addon/runner.js
+++ b/addon-sdk/source/lib/sdk/addon/runner.js
@@ -1,49 +1,32 @@
 /* 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/. */
 
 module.metadata = {
   "stability": "experimental"
 };
 
-const { Cc, Ci } = require('chrome');
+const { Cc, Ci, Cu } = require('chrome');
 const { isNative } = require('@loader/options');
 const { descriptor, Sandbox, evaluate, main, resolveURI } = require('toolkit/loader');
 const { once } = require('../system/events');
 const { exit, env, staticArgs } = require('../system');
 const { when: unload } = require('../system/unload');
 const { loadReason } = require('../self');
 const { rootURI, metadata } = require("@loader/options");
 const globals = require('../system/globals');
 const xulApp = require('../system/xul-app');
 const { id } = require('sdk/self');
 const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
                         getService(Ci.nsIAppShellService);
 const { preferences } = metadata;
 
-const NAME2TOPIC = {
-  'Firefox': 'sessionstore-windows-restored',
-  'Fennec': 'sessionstore-windows-restored',
-  'SeaMonkey': 'sessionstore-windows-restored',
-  'Thunderbird': 'mail-startup-done'
-};
-
-// Set 'final-ui-startup' as default topic for unknown applications
-let appStartup = 'final-ui-startup';
-
-// Gets the topic that fit best as application startup event, in according with
-// the current application (e.g. Firefox, Fennec, Thunderbird...)
-for (let name of Object.keys(NAME2TOPIC)) {
-  if (xulApp.is(name)) {
-    appStartup = NAME2TOPIC[name];
-    break;
-  }
-}
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
 
 // Initializes default preferences
 function setDefaultPrefs(prefsURI) {
   const prefs = Cc['@mozilla.org/preferences-service;1'].
                 getService(Ci.nsIPrefService).
                 QueryInterface(Ci.nsIPrefBranch2);
   const branch = prefs.getDefaultBranch('');
   const sandbox = Sandbox({
@@ -69,36 +52,17 @@ function setDefaultPrefs(prefsURI) {
   evaluate(sandbox, prefsURI);
 }
 
 function definePseudo(loader, id, exports) {
   let uri = resolveURI(id, loader.mapping);
   loader.modules[uri] = { exports: exports };
 }
 
-function wait(reason, options) {
-  once(appStartup, function() {
-    startup(null, options);
-  });
-}
-
-function startup(reason, options) {
-  // Try accessing hidden window to guess if we are running during firefox
-  // startup, so that we should wait for session restore event before
-  // running the addon
-  let initialized = false;
-  try {
-    appShellService.hiddenDOMWindow;
-    initialized = true;
-  }
-  catch(e) {}
-  if (reason === 'startup' || !initialized) {
-    return wait(reason, options);
-  }
-
+function startup(reason, options) Startup.onceInitialized.then(() => {
   // Inject globals ASAP in order to have console API working ASAP
   Object.defineProperties(options.loader.globals, descriptor(globals));
 
   // NOTE: Module is intentionally required only now because it relies
   // on existence of hidden window, which does not exists until startup.
   let { ready } = require('../addon/window');
   // Load localization manifest and .properties files.
   // Run the addon even in case of error (best effort approach)
@@ -112,17 +76,17 @@ function startup(reason, options) {
       // Exports data to a pseudo module so that api-utils/l10n/core
       // can get access to it
       definePseudo(options.loader, '@l10n/data', data ? data : null);
       return ready;
     }).then(function() {
       run(options);
     }).then(null, console.exception);
     return void 0; // otherwise we raise a warning, see bug 910304
-}
+});
 
 function run(options) {
   try {
     // Try initializing HTML localization before running main module. Just print
     // an exception in case of error, instead of preventing addon to be run.
     try {
       // Do not enable HTML localization while running test as it is hard to
       // disable. Because unit tests are evaluated in a another Loader who
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/modules/system/Startup.js
@@ -0,0 +1,55 @@
+/* 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"];
+
+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 = {
+  initialized: !appStartupSrv.startingUp
+};
+var exports = Startup;
+
+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) {
+  gOnceInitializedDeferred.resolve()
+}
+else {
+  // Gets the topic that fit best as application startup event, in according with
+  // the current application (e.g. Firefox, Fennec, Thunderbird...)
+  for (let name of Object.keys(NAME2TOPIC)) {
+    if (XulApp.is(name)) {
+      appStartup = NAME2TOPIC[name];
+      break;
+    }
+  }
+
+  let listener = function (subject, topic) {
+    Services.obs.removeObserver(this, topic);
+    Startup.initialized = true;
+    Services.tm.currentThread.dispatch(() => gOnceInitializedDeferred.resolve(),
+                                       Ci.nsIThread.DISPATCH_NORMAL);
+  }
+
+  Services.obs.addObserver(listener, appStartup, false);
+}
--- a/addon-sdk/source/modules/system/moz.build
+++ b/addon-sdk/source/modules/system/moz.build
@@ -1,9 +1,10 @@
 # -*- 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',
 ]
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-system-startup.js
@@ -0,0 +1,19 @@
+/* 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 { Cu } = require("chrome");
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
+
+exports["test startup initialized"] = function(assert) {
+  assert.ok(Startup.initialized, "Startup.initialized is true");
+}
+
+exports["test startup onceInitialized"] = function*(assert) {
+  yield Startup.onceInitialized.then(() => {
+    assert.pass("onceInitialized promise was resolved");
+  }).catch(assert.fail);
+}
+
+require('sdk/test').run(exports);
--- a/b2g/app/Makefile.in
+++ b/b2g/app/Makefile.in
@@ -19,21 +19,16 @@ INSTALL_TARGETS += UA_UPDATE
 
 # Make sure the standalone glue doesn't try to get libxpcom.so from b2g/app.
 NSDISTMODE = copy
 
 include $(topsrcdir)/config/rules.mk
 
 APP_ICON = b2g
 
-source_repo ?= $(call getSourceRepo,$(srcdir)/..)
-ifneq (,$(filter http%,$(source_repo)))
-  DEFINES += -DMOZ_SOURCE_REPO='$(source_repo)'
-endif
-
 ifeq ($(OS_ARCH),WINNT)
 REDIT_PATH = $(LIBXUL_DIST)/bin
 endif
 
 APP_BINARY = $(MOZ_APP_NAME)$(BIN_SUFFIX)
 
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -304,17 +304,20 @@ pref("notification.feature.enabled", tru
 
 // IndexedDB
 pref("dom.indexedDB.warningQuota", 5);
 
 // prevent video elements from preloading too much data
 pref("media.preload.default", 1); // default to preload none
 pref("media.preload.auto", 2);    // preload metadata if preload=auto
 pref("media.cache_size", 4096);    // 4MB media cache
-
+#ifdef MOZ_FMP4
+// Enable/Disable Gonk Decoder Module
+pref("media.fragmented-mp4.gonk.enabled", false);
+#endif
 // The default number of decoded video frames that are enqueued in
 // MediaDecoderReader's mVideoQueue.
 pref("media.video-queue.default-size", 3);
 
 // optimize images' memory usage
 pref("image.mem.decodeondraw", true);
 pref("image.mem.allow_locking_in_content_processes", false); /* don't allow image locking */
 pref("image.mem.min_discard_timeout_ms", 86400000); /* 24h, we rely on the out of memory hook */
@@ -881,16 +884,20 @@ pref("network.sntp.maxRetryCount", 10);
 pref("network.sntp.refreshPeriod", 86400); // In seconds.
 pref("network.sntp.pools", // Servers separated by ';'.
      "0.pool.ntp.org;1.pool.ntp.org;2.pool.ntp.org;3.pool.ntp.org");
 pref("network.sntp.port", 123);
 pref("network.sntp.timeout", 30); // In seconds.
 
 // Enable dataStore
 pref("dom.datastore.enabled", true);
+// When an entry is changed, use two timers to fire system messages in a more
+// moderate pattern.
+pref("dom.datastore.sysMsgOnChangeShortTimeoutSec", 10);
+pref("dom.datastore.sysMsgOnChangeLongTimeoutSec", 60);
 
 // DOM Inter-App Communication API.
 pref("dom.inter-app-communication-api.enabled", true);
 
 // Allow ADB to run for this many hours before disabling
 // (only applies when marionette is disabled)
 // 0 disables the timer.
 pref("b2g.adb.timeout-hours", 12);
--- a/b2g/chrome/content/devtools.js
+++ b/b2g/chrome/content/devtools.js
@@ -549,17 +549,17 @@ let memoryWatcher = {
         // TODO Also count images size (bug #976007).
 
         target.update({name: 'memory', value: total});
       }, err => {
         console.error(err);
       });
     }
 
-    let timer = setTimeout(() => this.measure(target), 300);
+    let timer = setTimeout(() => this.measure(target), 500);
     this._timers.set(target, timer);
   },
 
   trackTarget: function mw_trackTarget(target) {
     target.register('uss');
     target.register('memory');
     this._fronts.set(target, MemoryFront(this._client, target.actor));
     if (this._active) {
--- a/b2g/components/ContentPermissionPrompt.js
+++ b/b2g/components/ContentPermissionPrompt.js
@@ -232,17 +232,16 @@ ContentPermissionPrompt.prototype = {
           permissionSpecificChecker[typesInfo[i].permission](request)) {
         return true;
       }
     }
 
     return false;
   },
 
-  _id: 0,
   prompt: function(request) {
     // Initialize the typesInfo and set the default value.
     let typesInfo = [];
     let perms = request.types.QueryInterface(Ci.nsIArray);
     for (let idx = 0; idx < perms.length; idx++) {
       let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType);
       let tmp = {
         permission: perm.type,
@@ -289,70 +288,68 @@ ContentPermissionPrompt.prototype = {
     }
 
     // prompt PROMPT_ACTION request or request with options.
     typesInfo = typesInfo.filter(function(type) {
       return !type.deny && (type.action == Ci.nsIPermissionManager.PROMPT_ACTION || type.options.length > 0) ;
     });
 
     let frame = request.element;
-    let requestId = this._id++;
 
     if (!frame) {
-      this.delegatePrompt(request, requestId, typesInfo);
+      this.delegatePrompt(request, typesInfo);
       return;
     }
 
     frame = frame.wrappedJSObject;
     var cancelRequest = function() {
       frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
       request.cancel();
     }
 
     var self = this;
     var onVisibilityChange = function(evt) {
       if (evt.detail.visible === true)
         return;
 
-      self.cancelPrompt(request, requestId, typesInfo);
+      self.cancelPrompt(request, typesInfo);
       cancelRequest();
     }
 
     // If the request was initiated from a hidden iframe
     // we don't forward it to content and cancel it right away
     let domRequest = frame.getVisible();
     domRequest.onsuccess = function gv_success(evt) {
       if (!evt.target.result) {
         cancelRequest();
         return;
       }
 
       // Monitor the frame visibility and cancel the request if the frame goes
       // away but the request is still here.
       frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange);
 
-      self.delegatePrompt(request, requestId, typesInfo, function onCallback() {
+      self.delegatePrompt(request, typesInfo, function onCallback() {
         frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
       });
     };
 
     // Something went wrong. Let's cancel the request just in case.
     domRequest.onerror = function gv_error() {
       cancelRequest();
     }
   },
 
-  cancelPrompt: function(request, requestId, typesInfo) {
-    this.sendToBrowserWindow("cancel-permission-prompt", request, requestId,
+  cancelPrompt: function(request, typesInfo) {
+    this.sendToBrowserWindow("cancel-permission-prompt", request,
                              typesInfo);
   },
 
-  delegatePrompt: function(request, requestId, typesInfo, callback) {
-
-    this.sendToBrowserWindow("permission-prompt", request, requestId, typesInfo,
+  delegatePrompt: function(request, typesInfo, callback) {
+    this.sendToBrowserWindow("permission-prompt", request, typesInfo,
                              function(type, remember, choices) {
       if (type == "permission-allow") {
         rememberPermission(typesInfo, request.principal, !remember);
         if (callback) {
           callback();
         }
         request.allow(choices);
         return;
@@ -366,26 +363,36 @@ ContentPermissionPrompt.prototype = {
                                           Ci.nsIPermissionManager.DENY_ACTION);
         } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) {
           Services.perms.addFromPrincipal(request.principal, type.access,
                                           Ci.nsIPermissionManager.DENY_ACTION,
                                           Ci.nsIPermissionManager.EXPIRE_SESSION,
                                           0);
         }
       }
-      typesInfo.forEach(addDenyPermission);
+      try {
+        // This will trow if we are canceling because the remote process died.
+        // Just eat the exception and call the callback that will cleanup the
+        // visibility event listener.
+        typesInfo.forEach(addDenyPermission);
+      } catch(e) { }
 
       if (callback) {
         callback();
       }
-      request.cancel();
+
+      try {
+        request.cancel();
+      } catch(e) { }
     });
   },
 
-  sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) {
+  sendToBrowserWindow: function(type, request, typesInfo, callback) {
+    let requestId = Cc["@mozilla.org/uuid-generator;1"]
+                  .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
     if (callback) {
       SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) {
         let detail = evt.detail;
         if (detail.id != requestId)
           return;
         SystemAppProxy.removeEventListener("mozContentEvent", contentEvent);
 
         callback(detail.type, detail.remember, detail.choices);
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,18 +14,18 @@
   <!--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="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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="227354333a185180b85471f2cc6abfb029e44718"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,18 +12,18 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <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"/>
@@ -128,12 +128,12 @@
   <!-- Emulator specific things -->
   <project name="android-development" path="development" remote="b2g" revision="dab55669da8f48b6e57df95d5af9f16b4a87b0b1"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="197cd9492b9fadaa915c5daf36ff557f8f4a8d1c"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="683623c76338dccd65e698bfb5c4cfee8808d799"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9f28c4faea3b2f01db227b2467b08aeba96d9bec"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="02104803f873a4d5cf9fb611a211b83450e9dfba"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="a7141c4799ac2eb09ac3fe9476bfd066b21285e1"/>
   <project name="android-sdk" path="sdk" remote="b2g" revision="8b1365af38c9a653df97349ee53a3f5d64fd590a"/>
   <project name="darwinstreamingserver" path="system/darwinstreamingserver" remote="b2g" revision="cf85968c7f85e0ec36e72c87ceb4837a943b8af6"/>
 </manifest>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,19 +10,19 @@
   <!--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="7945ca73e687be5edbc7b928dc7fe3a208242144">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- 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"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,18 +14,18 @@
   <!--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="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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="227354333a185180b85471f2cc6abfb029e44718"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,18 +12,18 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <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"/>
@@ -128,25 +128,25 @@
   <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"/>
   <project name="platform/frameworks/av" path="frameworks/av" revision="0f7829661cd7125de9dc2c90eca2fa1dbc68dfbf"/>
-  <project name="platform/frameworks/base" path="frameworks/base" revision="051b29984ff0c23bc6bb31a36495dd1879d036d3"/>
+  <project name="platform/frameworks/base" path="frameworks/base" revision="c1019aa8ff3d7f1aa9e3439d3b26ec5f7ef68205"/>
   <project name="platform/frameworks/native" path="frameworks/native" revision="268d569074237b53617db8211400d4e3c947ae73"/>
   <project name="platform/hardware/libhardware" path="hardware/libhardware" revision="484802559ed106bac4811bd01c024ca64f741e60"/>
   <project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="de4ade568b273781416638fbbce13ff31b636ada"/>
   <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="5e110615212302c5d798a3c223dcee458817651c"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="fa9ffd47948eb24466de227e48fe9c4a7c5e7711"/>
-  <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="5dc48bd46f9589653f8bf297be5d73676f2e2867"/>
+  <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="cd76b19aafd4229ccf83853d02faef8c51ca8b34"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="8a0d0b0d9889ef99c4c6317c810db4c09295f15a"/>
   <project name="platform/hardware/qcom/wlan" path="hardware/qcom/wlan" revision="2208fa3537ace873b8f9ec2355055761c79dfd5f"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="c4e2ac95907a5519a0e09f01a0d8e27fec101af0"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="e1eb226fa3ad3874ea7b63c56a9dc7012d7ff3c2"/>
-  <project name="platform/system/core" path="system/core" revision="b33c9a7b8eefbeaf480f0b8f9af2c6a8a35b0aee"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="02104803f873a4d5cf9fb611a211b83450e9dfba"/>
+  <project name="platform/system/core" path="system/core" revision="adc485d8755af6a61641d197de7cfef667722580"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="a7141c4799ac2eb09ac3fe9476bfd066b21285e1"/>
   <project name="platform/system/qcom" path="system/qcom" revision="1cdab258b15258b7f9657da70e6f06ebd5a2fc25"/>
   <project name="platform/vendor/qcom/msm8610" path="device/qcom/msm8610" revision="4ae5df252123591d5b941191790e7abed1bce5a4"/>
   <project name="platform/vendor/qcom-opensource/wlan/prima" path="vendor/qcom/opensource/wlan/prima" revision="ce18b47b4a4f93a581d672bbd5cb6d12fe796ca9"/>
 </manifest>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "d581b7149d25fcff1786f914ed6b83d499999645", 
+    "revision": "08746241e68aef919d4c8105be87cd3936531ca2", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <!-- 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"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,18 +10,18 @@
   <!--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="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,18 +12,18 @@
   <!--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="3aa6abd313f965a84aa86c6b213dc154e4875139">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <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"/>
@@ -124,17 +124,17 @@
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
   <!-- Nexus 4 specific things -->
   <project name="device-mako" path="device/lge/mako" remote="b2g" revision="78d17f0c117f0c66dd55ee8d5c5dde8ccc93ecba"/>
   <project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="3a9a17613cc685aa232432566ad6cc607eab4ec1"/>
   <project name="device/lge/mako-kernel" path="device/lge/mako-kernel" revision="d1729e53d71d711c8fde25eab8728ff2b9b4df0e"/>
-  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="02104803f873a4d5cf9fb611a211b83450e9dfba"/>
+  <project name="platform_system_nfcd" path="system/nfcd" remote="b2g" revision="a7141c4799ac2eb09ac3fe9476bfd066b21285e1"/>
   <project name="platform/external/libnfc-nci" path="external/libnfc-nci" revision="7d33aaf740bbf6c7c6e9c34a92b371eda311b66b"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="0e56e450367cd802241b27164a2979188242b95f"/>
   <project name="platform/hardware/broadcom/wlan" path="hardware/broadcom/wlan" revision="0e1929fa3aa38bf9d40e9e953d619fab8164c82e"/>
   <project name="platform/hardware/qcom/audio" path="hardware/qcom/audio" revision="b0a528d839cfd9d170d092fe3743b5252b4243a6"/>
   <project name="platform/hardware/qcom/bt" path="hardware/qcom/bt" revision="380945eaa249a2dbdde0daa4c8adb8ca325edba6"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="6f3b0272cefaffeaed2a7d2bb8f633059f163ddc"/>
   <project name="platform/hardware/qcom/keymaster" path="hardware/qcom/keymaster" revision="16da8262c997a5a0d797885788a64a0771b26910"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="689b476ba3eb46c34b81343295fe144a0e81a18e"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,18 +12,18 @@
   <!--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="0d616942c300d9fb142483210f1dda9096c9a9fc">
     <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="54c3c19d439f7dbafda5c6cc3b4850b545a068ba"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6822004d07064ea3ebbc0c94cd2e15be6a02069d"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="db50c8d84054c0286c3ea355feae32ee4a51c82c"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="3bb61a27cd2941b2ba9b616a11aaa44269210396"/>
   <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="71f5a35e3bc1801847413cff1f14fc3b5cd991ca"/>
   <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"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -139,16 +139,17 @@
 @BINPATH@/components/commandlines.xpt
 @BINPATH@/components/composer.xpt
 @BINPATH@/components/content_base.xpt
 @BINPATH@/components/content_events.xpt
 @BINPATH@/components/content_htmldoc.xpt
 @BINPATH@/components/content_html.xpt
 @BINPATH@/components/content_xslt.xpt
 @BINPATH@/components/cookie.xpt
+@BINPATH@/components/devtools_security.xpt
 @BINPATH@/components/directory.xpt
 @BINPATH@/components/diskspacewatcher.xpt
 @BINPATH@/components/docshell.xpt
 @BINPATH@/components/dom.xpt
 @BINPATH@/components/dom_activities.xpt
 @BINPATH@/components/dom_apps.xpt
 @BINPATH@/components/dom_audiochannel.xpt
 @BINPATH@/components/dom_base.xpt
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1406936041000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1407370380000">
   <emItems>
       <emItem  blockID="i454" id="sqlmoz@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                                                 <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                                     <prefs>
               </prefs>
@@ -40,16 +40,23 @@
               </prefs>
     </emItem>
       <emItem  blockID="i646" id="{e1aaa9f8-4500-47f1-9a0a-b02bd60e4076}">
                         <versionRange  minVersion="178.7.0" maxVersion="178.7.0" severity="3">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i684" id="{9edd0ea8-2819-47c2-8320-b007d5996f8a}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                                                    <prefs>
+                  <pref>browser.search.defaultenginename</pref>
+              </prefs>
+    </emItem>
       <emItem  blockID="i107" os="WINNT" id="{ABDE892B-13A8-4d1b-88E6-365A6E755758}">
                         <versionRange  minVersion="0" maxVersion="15.0.5" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i88" id="anttoolbar@ant.com">
                         <versionRange  minVersion="2.4.6.4" maxVersion="2.4.6.4" severity="1">
@@ -58,17 +65,17 @@
               </prefs>
     </emItem>
       <emItem  blockID="i484" id="plugin@getwebcake.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i640" id="jid0-l9BxpNUhx1UUgRfKigWzSfrZqAc@jetpack">
+      <emItem  blockID="i688" id="firefox-extension@mozilla.org">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i65" id="activity@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
@@ -332,18 +339,18 @@
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i47" id="youtube@youtube2.com">
                                                           <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i67" id="youtube2@youtube2.com">
-                        <versionRange  minVersion="0" maxVersion="*">
+      <emItem  blockID="i640" id="jid0-l9BxpNUhx1UUgRfKigWzSfrZqAc@jetpack">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i360" id="ytd@mybrowserbar.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
@@ -483,16 +490,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i447" id="{B18B1E5C-4D81-11E1-9C00-AFEB4824019B}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i668" id="/^(matchersite(pro(srcs?)?)?\@matchersite(pro(srcs?)?)?\.com)|((pro)?sitematcher(_srcs?|pro|site|sitesrc|-generic)?\@(pro)?sitematcher(_srcs?|pro|site|sitesrc|-generic)?\.com)$/">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                                                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i519" id="703db0db-5fe9-44b6-9f53-c6a91a0ad5bd@7314bc82-969e-4d2a-921b-e5edd0b02cf1.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i537" id="rally_toolbar_ff@bulletmedia.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -800,22 +813,16 @@
               </prefs>
     </emItem>
       <emItem  blockID="i370" id="happylyrics@hpyproductions.net">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i668" id="/^(matchersite(pro(src)?)?\@matchersite(pro(src)?)?\.com)|((pro)?sitematcher(_src|pro|site|sitesrc)?\@(pro)?sitematcher(_src|pro|site|sitesrc)?\.com)$/">
-                        <versionRange  minVersion="0" maxVersion="*" severity="1">
-                    </versionRange>
-                                                    <prefs>
-              </prefs>
-    </emItem>
       <emItem  blockID="i440" id="{2d069a16-fca1-4e81-81ea-5d5086dcbd0c}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i396" id="/@(ft|putlocker|clickmovie|m2k|sharerepo|smarter-?)downloader\.com$/">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -974,16 +981,22 @@
                                                                 <versionRange  minVersion="3.3.1" maxVersion="*">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="5.0a1" maxVersion="*" />
                           </targetApplication>
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i682" id="f6682b47-e12f-400b-9bc0-43b3ccae69d1@39d6f481-b198-4349-9ebe-9a93a86f9267.com">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                                                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i471" id="firefox@luckyleap.net">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i495" id="kallow@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
@@ -1106,16 +1119,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i308" id="9518042e-7ad6-4dac-b377-056e28d00c8f@f1cc0a13-4df1-4d66-938f-088db8838882.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i67" id="youtube2@youtube2.com">
+                        <versionRange  minVersion="0" maxVersion="*">
+                    </versionRange>
+                                                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i560" id="adsremoval@adsremoval.net">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i461" id="{8E9E3331-D360-4f87-8803-52DE43566502}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -1761,16 +1780,24 @@
               </prefs>
     </emItem>
       <emItem  blockID="i497" id="{872b5b88-9db5-4310-bdd0-ac189557e5f5}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i686" id="{a7f2cb14-0472-42a1-915a-8adca2280a2c}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                                                    <prefs>
+                  <pref>browser.startup.homepage</pref>
+                  <pref>browser.search.defaultenginename</pref>
+              </prefs>
+    </emItem>
       <emItem  blockID="i580" id="{51c77233-c0ad-4220-8388-47c11c18b355}">
                         <versionRange  minVersion="0" maxVersion="0.1.9999999" severity="1">
                     </versionRange>
                                                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i541" id="/^({988919ff-0cd8-4d0c-bc7e-60d55a49eb64}|{494b9726-9084-415c-a499-68c07e187244}|{55b95864-3251-45e9-bb30-1a82589aaff1}|{eef3855c-fc2d-41e6-8d91-d368f51b3055}|{90a1b331-c2b4-4933-9f63-ba7b84d60d58}|{d2cf9842-af95-48cd-b873-bfbb48cd7f5e})$/">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1694,15 +1694,13 @@ pref("browser.translation.detectLanguage
 pref("browser.translation.neverForLanguages", "");
 // Show the translation UI bits, like the info bar, notification icon and preferences.
 pref("browser.translation.ui.show", false);
 
 // Telemetry experiments settings.
 pref("experiments.enabled", true);
 pref("experiments.manifest.fetchIntervalSeconds", 86400);
 pref("experiments.manifest.uri", "https://telemetry-experiment.cdn.mozilla.net/manifest/v1/firefox/%VERSION%/%CHANNEL%");
-pref("experiments.manifest.certs.1.commonName", "*.cdn.mozilla.net");
-pref("experiments.manifest.certs.1.issuerName", "CN=Cybertrust Public SureServer SV CA,O=Cybertrust Inc");
 // Whether experiments are supported by the current application profile.
 pref("experiments.supported", true);
 
 // Enable the OpenH264 plugin support in the addon manager.
 pref("media.gmp-gmpopenh264.provider.enabled", true);
--- a/browser/base/content/aboutneterror/netError.css
+++ b/browser/base/content/aboutneterror/netError.css
@@ -55,22 +55,14 @@ ul {
   #errorTitleText {
     padding-top: 0;
     background-image: none;
     -moz-padding-start: 0;
     -moz-margin-start: 0;
   }
 }
 
-
-/* common.css overrides */
-
-button {
-  font-size: 1em;
-  min-width: 150px;
-}
-
 /* Pressing the retry button will cause the cursor to flicker from a pointer to
  * not-allowed. Override the disabled cursor behaviour since we will never show
  * the button disabled as the initial state. */
 button:disabled {
   cursor: pointer;
 }
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1041,24 +1041,24 @@ toolbarpaletteitem[dragover] {
 
 #customization-palette-container {
   display: flex;
   flex-direction: column;
 }
 
 #customization-palette:not([hidden]) {
   display: block;
-  flex: 1 1 auto;
+  flex: 1 1 main-size;
   overflow: auto;
   min-height: 3em;
 }
 
 #customization-footer-spacer,
 #customization-spacer {
-  flex: 1 1 auto;
+  flex: 1 1 main-size;
 }
 
 #customization-footer {
   display: flex;
   flex-shrink: 0;
   flex-wrap: wrap;
 }
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -891,19 +891,20 @@
               if (Services.prefs.getBoolPref("browser.chrome.site_icons")) {
                 let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size");
                 if (browser.imageDocument.width <= sz &&
                     browser.imageDocument.height <= sz) {
                   icon = browser.currentURI;
                 }
               }
             }
+
             // Use documentURIObject in the check for shouldLoadFavIcon so that we
             // do the right thing with about:-style error pages.  Bug 453442
-            else if (this.shouldLoadFavIcon(documentURI)) {
+            if (!icon && this.shouldLoadFavIcon(documentURI)) {
               let url = documentURI.prePath + "/favicon.ico";
               if (!this.isFailedIcon(url))
                 icon = url;
             }
             this.setIcon(aTab, icon);
           ]]>
         </body>
       </method>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -294,18 +294,18 @@ skip-if = e10s # Bug 973001 - appears us
 skip-if = e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_favicon_change.js]
 [browser_findbarClose.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (tries to grab an iframe directly from content)
 [browser_fullscreen-window-open.js]
+skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575.
 [browser_fxa_oauth.js]
-skip-if = buildapp == 'mulet' || e10s || os == "linux" # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly. Linux: Intermittent failures - bug 941575.
 [browser_gestureSupport.js]
 skip-if = e10s # Bug 863514 - no gesture support.
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 skip-if = buildapp == 'mulet'
 [browser_identity_UI.js]
 skip-if = e10s # Bug ?????? - this test fails for obscure reasons on non-windows builds only.
@@ -432,18 +432,18 @@ skip-if = e10s # Bug ?????? - test direc
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_visibleTabs_bookmarkAllPages.js]
 skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: bizarre problem with hidden tab having _mouseenter called, via _setPositionalAttributes, and tab not being found resulting in 'candidate is undefined'
 [browser_visibleTabs_bookmarkAllTabs.js]
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_visibleTabs_contextMenu.js]
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_visibleTabs_tabPreview.js]
+skip-if = (os == "win" && !debug) || e10s # Bug 1007418 / Bug 698371 - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.)
 [browser_web_channel.js]
-skip-if = (os == "win" && !debug) || e10s # Bug 1007418 / Bug 698371 - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.)
 [browser_windowopen_reflows.js]
 skip-if = buildapp == 'mulet'
 [browser_wyciwyg_urlbarCopying.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (content.document.getElementById)
 [browser_zbug569342.js]
 skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
 [browser_registerProtocolHandler_notification.js]
 skip-if = e10s # Bug 940206 - nsIWebContentHandlerRegistrar::registerProtocolHandler doesn't work in e10s
--- a/browser/base/content/test/general/browser_urlbarSearchSingleWordNotification.js
+++ b/browser/base/content/test/general/browser_urlbarSearchSingleWordNotification.js
@@ -1,23 +1,37 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+let notificationObserver;
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost");
+  if (notificationObserver) {
+    notificationObserver.disconnect();
+  }
 });
 
 function promiseNotificationForTab(value, expected, tab=gBrowser.selectedTab) {
   let deferred = Promise.defer();
   let notificationBox = gBrowser.getNotificationBox(tab.linkedBrowser);
   if (expected) {
-    waitForCondition(() => notificationBox.getNotificationWithValue(value) !== null,
-                     deferred.resolve, "Were expecting to get a notification");
+    let checkForNotification = function() {
+      if (notificationBox.getNotificationWithValue(value)) {
+        notificationObserver.disconnect();
+        notificationObserver = null;
+        deferred.resolve();
+      }
+    }
+    if (notificationObserver) {
+      notificationObserver.disconnect();
+    }
+    notificationObserver = new MutationObserver(checkForNotification);
+    notificationObserver.observe(notificationBox, {childList: true});
   } else {
     setTimeout(() => {
       is(notificationBox.getNotificationWithValue(value), null, "We are expecting to not get a notification");
       deferred.resolve();
     }, 1000);
   }
   return deferred.promise;
 }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1814,21 +1814,21 @@
             default:
               Cu.reportError(Error("Unexpected blocklist state"));
             }
           }
           this._setupDescription(label, action.pluginName, host);
           this._setupLink(linkLabel, action.detailsLink);
 
           this._primaryButton.label = gNavigatorBundle.getString(button1.label);
-          this._primaryButton.accesskey = gNavigatorBundle.getString(button1.accesskey);
+          this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey);
           this._primaryButton.setAttribute("action", button1.action);
 
           this._secondaryButton.label = gNavigatorBundle.getString(button2.label);
-          this._secondaryButton.accesskey = gNavigatorBundle.getString(button2.accesskey);
+          this._secondaryButton.accessKey = gNavigatorBundle.getString(button2.accesskey);
           this._secondaryButton.setAttribute("action", button2.action);
           if (button1.default) {
             this._primaryButton.setAttribute("default", "true");
           }
           else if (button2.default) {
             this._secondaryButton.setAttribute("default", "true");
           }
         ]]></body>
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -6,31 +6,39 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
-  "resource://gre/modules/MozSocialAPI.jsm");
-
+                                        "resource://gre/modules/MozSocialAPI.jsm");
+XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
+  return Cc["@mozilla.org/xre/app-info;1"]
+           .getService(Ci.nsIXULAppInfo)
+           .QueryInterface(Ci.nsIXULRuntime);
+});
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+                                         "@mozilla.org/widget/clipboardhelper;1",
+                                         "nsIClipboardHelper");
 this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
 
 /**
  * Inject the loop API into the given window.  The caller must be sure the
  * window is a loop content window (eg, a panel, chatwindow, or similar).
  *
  * See the documentation on the individual functions for details of the API.
  *
  * @param {nsIDOMWindow} targetWindow The content window to attach the API.
  */
 function injectLoopAPI(targetWindow) {
   let ringer;
   let ringerStopper;
+  let appVersionInfo;
 
   let api = {
     /**
      * Sets and gets the "do not disturb" mode activation flag.
      */
     doNotDisturb: {
       enumerable: true,
       get: function() {
@@ -220,16 +228,53 @@ function injectLoopAPI(targetWindow) {
         // XXX Should really return a DOM promise here.
         return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
           callback(null, response.body);
         }, (error) => {
           callback(Cu.cloneInto(error, targetWindow));
         });
       }
     },
+
+    /**
+     * Copies passed string onto the system clipboard.
+     *
+     * @param {String} str The string to copy
+     */
+    copyString: {
+      enumerable: true,
+      writable: true,
+      value: function(str) {
+        clipboardHelper.copyString(str);
+      }
+    },
+
+    /**
+     * Returns the app version information for use during feedback.
+     *
+     * @return {Object} An object containing:
+     *   - channel: The update channel the application is on
+     *   - version: The application version
+     *   - OS: The operating system the application is running on
+     */
+    appVersionInfo: {
+      enumerable: true,
+      get: function() {
+        if (!appVersionInfo) {
+          let defaults = Services.prefs.getDefaultBranch(null);
+
+          appVersionInfo = Cu.cloneInto({
+            channel: defaults.getCharPref("app.update.channel"),
+            version: appInfo.version,
+            OS: appInfo.OS
+          }, targetWindow);
+        }
+        return appVersionInfo;
+      }
+    },
   };
 
   let contentObj = Cu.createObjectIn(targetWindow);
   Object.defineProperties(contentObj, api);
   Object.seal(contentObj);
   Cu.makeObjectPropsNormal(contentObj);
 
   targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -243,21 +243,30 @@ loop.conversation = (function(OT, mozL10
     },
 
     /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
       document.title = mozL10n.get("call_has_ended");
 
+      var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+        "feedback.baseUrl");
+
+      var appVersionInfo = navigator.mozLoop.appVersionInfo;
+
+      var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
+        product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+        platform: appVersionInfo.OS,
+        channel: appVersionInfo.channel,
+        version: appVersionInfo.version
+      });
+
       this.loadReactComponent(sharedViews.FeedbackView({
-        feedbackApiClient: new loop.FeedbackAPIClient({
-          baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
-          product: navigator.mozLoop.getLoopCharPref("feedback.product")
-        })
+        feedbackApiClient: feedbackClient
       }));
     }
   });
 
   /**
    * Panel initialisation.
    */
   function init() {
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -243,21 +243,30 @@ loop.conversation = (function(OT, mozL10
     },
 
     /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
       document.title = mozL10n.get("call_has_ended");
 
+      var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
+        "feedback.baseUrl");
+
+      var appVersionInfo = navigator.mozLoop.appVersionInfo;
+
+      var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
+        product: navigator.mozLoop.getLoopCharPref("feedback.product"),
+        platform: appVersionInfo.OS,
+        channel: appVersionInfo.channel,
+        version: appVersionInfo.version
+      });
+
       this.loadReactComponent(sharedViews.FeedbackView({
-        feedbackApiClient: new loop.FeedbackAPIClient({
-          baseUrl: navigator.mozLoop.getLoopCharPref("feedback.baseUrl"),
-          product: navigator.mozLoop.getLoopCharPref("feedback.product")
-        })
+        feedbackApiClient: feedbackClient
       }));
     }
   });
 
   /**
    * Panel initialisation.
    */
   function init() {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -147,21 +147,27 @@ loop.panel = (function(_, mozL10n) {
             this.props.children
           )
         )
       );
     }
   });
 
   var CallUrlResult = React.createClass({displayName: 'CallUrlResult',
+    propTypes: {
+      callUrl:  React.PropTypes.string,
+      notifier: React.PropTypes.object.isRequired,
+      client:   React.PropTypes.object.isRequired
+    },
 
     getInitialState: function() {
       return {
         pending: false,
-        callUrl: ''
+        copied: false,
+        callUrl: this.props.callUrl || ""
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     * @note:
@@ -179,83 +185,100 @@ loop.panel = (function(_, mozL10n) {
                                        this._onCallUrlReceived);
     },
 
     _onCallUrlReceived: function(err, callUrlData) {
       this.props.notifier.clear();
 
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_url");
-        this.setState({pending: false});
+        this.setState(this.getInitialState());
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
           navigator.mozLoop.setLoopCharPref('loopToken', token);
-          this.setState({pending: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false, callUrl: callUrl.href});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
-          this.setState({pending: false});
+          this.setState(this.getInitialState());
         }
       }
     },
 
-    _generateMailto: function() {
+    _generateMailTo: function() {
       return encodeURI([
-        "mailto:?subject=" + __("share_email_subject") + "&",
+        "mailto:?subject=" + __("share_email_subject2") + "&",
         "body=" + __("share_email_body", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
+    handleEmailButtonClick: function(event) {
+      // Note: side effect
+      document.location = event.target.dataset.mailto;
+    },
+
+    handleCopyButtonClick: function(event) {
+      // XXX the mozLoop object should be passed as a prop, to ease testing and
+      //     using a fake implementation in UI components showcase.
+      navigator.mozLoop.copyString(this.state.callUrl);
+      this.setState({copied: true});
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
-      var inputCSSClass = {
-        "pending": this.state.pending,
-        "callUrl": !this.state.pending
-      };
       return (
         PanelLayout({summary: __("share_link_header_text")}, 
           React.DOM.div({className: "invite"}, 
             React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
-                   className: cx(inputCSSClass)}), 
-          React.DOM.a({className: cx({btn: true, hide: !this.state.callUrl}), 
-             href: this._generateMailto()}, 
-           React.DOM.span(null, 
-            __("share_button")
-           )
-          )
+                   className: cx({pending: this.state.pending})}), 
+            React.DOM.p({className: "button-group url-actions"}, 
+              React.DOM.button({className: "btn btn-email", disabled: !this.state.callUrl, 
+                onClick: this.handleEmailButtonClick, 
+                'data-mailto': this._generateMailTo()}, 
+                __("share_button")
+              ), 
+              React.DOM.button({className: "btn btn-copy", disabled: !this.state.callUrl, 
+                onClick: this.handleCopyButtonClick}, 
+                this.state.copied ? __("copied_url_button") :
+                                     __("copy_url_button")
+              )
+            )
           )
         )
       );
     }
   });
 
   /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
-      client: React.PropTypes.object.isRequired
+      client: React.PropTypes.object.isRequired,
+      // Mostly used for UI components showcase and unit tests
+      callUrl: React.PropTypes.string
     },
 
     render: function() {
       return (
         React.DOM.div(null, 
           CallUrlResult({client: this.props.client, 
-                       notifier: this.props.notifier}), 
+                         notifier: this.props.notifier, 
+                         callUrl: this.props.callUrl}), 
           ToSView(null), 
           AvailabilityDropdown(null)
         )
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -147,21 +147,27 @@ loop.panel = (function(_, mozL10n) {
             {this.props.children}
           </div>
         </div>
       );
     }
   });
 
   var CallUrlResult = React.createClass({
+    propTypes: {
+      callUrl:  React.PropTypes.string,
+      notifier: React.PropTypes.object.isRequired,
+      client:   React.PropTypes.object.isRequired
+    },
 
     getInitialState: function() {
       return {
         pending: false,
-        callUrl: ''
+        copied: false,
+        callUrl: this.props.callUrl || ""
       };
     },
 
     /**
     * Returns a random 5 character string used to identify
     * the conversation.
     * XXX this will go away once the backend changes
     * @note:
@@ -179,83 +185,100 @@ loop.panel = (function(_, mozL10n) {
                                        this._onCallUrlReceived);
     },
 
     _onCallUrlReceived: function(err, callUrlData) {
       this.props.notifier.clear();
 
       if (err) {
         this.props.notifier.errorL10n("unable_retrieve_url");
-        this.setState({pending: false});
+        this.setState(this.getInitialState());
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
           navigator.mozLoop.setLoopCharPref('loopToken', token);
-          this.setState({pending: false, callUrl: callUrl.href});
+          this.setState({pending: false, copied: false, callUrl: callUrl.href});
         } catch(e) {
           console.log(e);
           this.props.notifier.errorL10n("unable_retrieve_url");
-          this.setState({pending: false});
+          this.setState(this.getInitialState());
         }
       }
     },
 
-    _generateMailto: function() {
+    _generateMailTo: function() {
       return encodeURI([
-        "mailto:?subject=" + __("share_email_subject") + "&",
+        "mailto:?subject=" + __("share_email_subject2") + "&",
         "body=" + __("share_email_body", {callUrl: this.state.callUrl})
       ].join(""));
     },
 
+    handleEmailButtonClick: function(event) {
+      // Note: side effect
+      document.location = event.target.dataset.mailto;
+    },
+
+    handleCopyButtonClick: function(event) {
+      // XXX the mozLoop object should be passed as a prop, to ease testing and
+      //     using a fake implementation in UI components showcase.
+      navigator.mozLoop.copyString(this.state.callUrl);
+      this.setState({copied: true});
+    },
+
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
-      var inputCSSClass = {
-        "pending": this.state.pending,
-        "callUrl": !this.state.pending
-      };
       return (
         <PanelLayout summary={__("share_link_header_text")}>
           <div className="invite">
             <input type="url" value={this.state.callUrl} readOnly="true"
-                   className={cx(inputCSSClass)} />
-          <a className={cx({btn: true, hide: !this.state.callUrl})}
-             href={this._generateMailto()}>
-           <span>
-            {__("share_button")}
-           </span>
-          </a>
+                   className={cx({pending: this.state.pending})} />
+            <p className="button-group url-actions">
+              <button className="btn btn-email" disabled={!this.state.callUrl}
+                onClick={this.handleEmailButtonClick}
+                data-mailto={this._generateMailTo()}>
+                {__("share_button")}
+              </button>
+              <button className="btn btn-copy" disabled={!this.state.callUrl}
+                onClick={this.handleCopyButtonClick}>
+                {this.state.copied ? __("copied_url_button") :
+                                     __("copy_url_button")}
+              </button>
+            </p>
           </div>
         </PanelLayout>
       );
     }
   });
 
   /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
-      client: React.PropTypes.object.isRequired
+      client: React.PropTypes.object.isRequired,
+      // Mostly used for UI components showcase and unit tests
+      callUrl: React.PropTypes.string
     },
 
     render: function() {
       return (
         <div>
           <CallUrlResult client={this.props.client}
-                       notifier={this.props.notifier} />
+                         notifier={this.props.notifier}
+                         callUrl={this.props.callUrl} />
           <ToSView />
           <AvailabilityDropdown />
         </div>
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -315,16 +315,17 @@
   cursor: pointer;
   padding: 3px 10px;
   display: inline;
   margin-bottom: 1em;
 }
 
 .feedback label {
   display: block;
+  line-height: 1.5em;
 }
 
 .feedback form input[type="radio"] {
   margin-right: .5em;
 }
 
 .feedback form button[type="submit"] {
   width: 100%;
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -81,25 +81,27 @@
   color: #fff;
   width: 50%;
   height: 26px;
   text-align: center;
   font-family: 'Lucida Grande', sans-serif;
   margin-top: 10px;
 }
 
-.share .action .btn:hover {
+.share > .action .btn:hover {
   background-color: #008ACB;
   border: 1px solid #008ACB;
 }
 
-.share .action .btn span {
-  margin-top: 2px;
-  font-size: 12px;
-  display: inline-block;
+.share > .action > .invite > .url-actions {
+  margin: 0 0 5px;
+}
+
+.share > .action > .invite > .url-actions > .btn:first-child {
+  -moz-margin-end: 1em;
 }
 
 /* Specific cases */
 
 .panel #messages .alert {
   margin-bottom: 0;
 }
 
--- a/browser/components/loop/content/shared/js/feedbackApiClient.js
+++ b/browser/components/loop/content/shared/js/feedbackApiClient.js
@@ -1,82 +1,106 @@
 /* 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/. */
 
 /* global loop:true */
 
 var loop = loop || {};
-loop.FeedbackAPIClient = (function($) {
+loop.FeedbackAPIClient = (function($, _) {
   "use strict";
 
   /**
    * Feedback API client. Sends feedback data to an input.mozilla.com compatible
    * API.
    *
-   * Available settings:
-   * - {String} baseUrl Base API url (required)
+   * @param {String} baseUrl  Base API url (required)
+   * @param {Object} defaults Defaults field values for that client.
+   *
+   * Required defaults:
    * - {String} product Product name (required)
    *
-   * @param {Object} settings Settings.
+   * Optional defaults:
+   * - {String} platform   Platform name, eg. "Windows 8", "Android", "Linux"
+   * - {String} version    Product version, eg. "22b2", "1.1"
+   * - {String} channel    Product channel, eg. "stable", "beta"
+   * - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
+   *
    * @link  http://fjord.readthedocs.org/en/latest/api.html
    */
-  function FeedbackAPIClient(settings) {
-    settings = settings || {};
-    if (!settings.hasOwnProperty("baseUrl")) {
-      throw new Error("Missing required baseUrl setting.");
+  function FeedbackAPIClient(baseUrl, defaults) {
+    this.baseUrl = baseUrl;
+    if (!this.baseUrl) {
+      throw new Error("Missing required 'baseUrl' argument.");
     }
-    this._baseUrl = settings.baseUrl;
-    if (!settings.hasOwnProperty("product")) {
-      throw new Error("Missing required product setting.");
+
+    this.defaults = defaults || {};
+    // required defaults checks
+    if (!this.defaults.hasOwnProperty("product")) {
+      throw new Error("Missing required 'product' default.");
     }
-    this._product = settings.product;
   }
 
   FeedbackAPIClient.prototype = {
     /**
-     * Formats Feedback data to match the API spec.
+     * Supported field names by the feedback API.
+     * @type {Array}
+     */
+    _supportedFields: ["happy",
+                       "category",
+                       "description",
+                       "product",
+                       "platform",
+                       "version",
+                       "channel",
+                       "user_agent"],
+
+    /**
+     * Creates a formatted payload object compliant with the Feedback API spec
+     * against validated field data.
      *
-     * @param  {Object} fields Feedback form data.
-     * @return {Object}        Formatted data.
+     * @param  {Object} fields Feedback initial values.
+     * @return {Object}        Formatted payload object.
+     * @throws {Error}         If provided values are invalid
      */
-    _formatData: function(fields) {
-      var formatted = {};
-
+    _createPayload: function(fields) {
       if (typeof fields !== "object") {
         throw new Error("Invalid feedback data provided.");
       }
 
-      formatted.product = this._product;
-      formatted.happy = fields.happy;
-      formatted.category = fields.category;
+      Object.keys(fields).forEach(function(name) {
+        if (this._supportedFields.indexOf(name) === -1) {
+          throw new Error("Unsupported field " + name);
+        }
+      }, this);
+
+      // Payload is basically defaults + fields merged in
+      var payload = _.extend({}, this.defaults, fields);
 
       // Default description field value
       if (!fields.description) {
-        formatted.description = (fields.happy ? "Happy" : "Sad") + " User";
-      } else {
-        formatted.description = fields.description;
+        payload.description = (fields.happy ? "Happy" : "Sad") + " User";
       }
 
-      return formatted;
+      return payload;
     },
 
     /**
      * Sends feedback data.
      *
      * @param  {Object}   fields Feedback form data.
      * @param  {Function} cb     Callback(err, result)
      */
     send: function(fields, cb) {
       var req = $.ajax({
-        url:         this._baseUrl,
+        url:         this.baseUrl,
         method:      "POST",
         contentType: "application/json",
         dataType:    "json",
-        data: JSON.stringify(this._formatData(fields))
+        data: JSON.stringify(this._createPayload(fields))
       });
 
       req.done(function(result) {
         console.info("User feedback data have been submitted", result);
         cb(null, result);
       });
 
       req.fail(function(jqXHR, textStatus, errorThrown) {
@@ -84,9 +108,9 @@ loop.FeedbackAPIClient = (function($) {
         var httpError = jqXHR.status + " " + errorThrown;
         console.error(message, httpError, JSON.stringify(jqXHR.responseJSON));
         cb(new Error(message + ": " + httpError));
       });
     }
   };
 
   return FeedbackAPIClient;
-})(jQuery);
+})(jQuery, _);
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -7,17 +7,16 @@
 /* jshint newcap:false */
 /* global loop:true, React */
 var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = (function(_, OT, l10n) {
   "use strict";
 
   var sharedModels = loop.shared.models;
-  var __ = l10n.get;
   var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
 
   /**
    * L10n view. Translates resulting view DOM fragment once rendered.
    */
   var L10nView = (function() {
     var L10nViewImpl   = Backbone.View.extend(), // Original View constructor
         originalExtend = L10nViewImpl.extend;    // Original static extend fn
@@ -129,17 +128,17 @@ loop.shared.views = (function(_, OT, l10
       classesObj["btn-mute-" + this.props.type] = true;
       return cx(classesObj);
     },
 
     _getTitle: function(enabled) {
       var prefix = this.props.enabled ? "mute" : "unmute";
       var suffix = "button_title";
       var msgId = [prefix, this.props.scope, this.props.type, suffix].join("_");
-      return __(msgId);
+      return l10n.get(msgId);
     },
 
     render: function() {
       return (
         /* jshint ignore:start */
         React.DOM.button({className: this._getClasses(), 
                 title: this._getTitle(), 
                 onClick: this.handleClick})
@@ -179,17 +178,17 @@ loop.shared.views = (function(_, OT, l10
     },
 
     render: function() {
       /* jshint ignore:start */
       return (
         React.DOM.ul({className: "conversation-toolbar"}, 
           React.DOM.li(null, React.DOM.button({className: "btn btn-hangup", 
                       onClick: this.handleClickHangup, 
-                      title: __("hangup_button_title")})), 
+                      title: l10n.get("hangup_button_title")})), 
           React.DOM.li(null, MediaControlButton({action: this.handleToggleVideo, 
                                   enabled: this.props.video.enabled, 
                                   scope: "local", type: "video"})), 
           React.DOM.li(null, MediaControlButton({action: this.handleToggleAudio, 
                                   enabled: this.props.audio.enabled, 
                                   scope: "local", type: "audio"}))
         )
       );
@@ -366,17 +365,17 @@ loop.shared.views = (function(_, OT, l10
       reset: React.PropTypes.func // if not specified, no Back btn is shown
     },
 
     render: function() {
       var backButton = React.DOM.div(null);
       if (this.props.reset) {
         backButton = (
           React.DOM.button({className: "back", type: "button", onClick: this.props.reset}, 
-            "« ", __("feedback_back_button")
+            "« ", l10n.get("feedback_back_button")
           )
         );
       }
       return (
         React.DOM.div({className: "feedback"}, 
           backButton, 
           React.DOM.h3(null, this.props.title), 
           this.props.children
@@ -400,84 +399,106 @@ loop.shared.views = (function(_, OT, l10
     },
 
     getInitialProps: function() {
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
-        audio_quality: __("feedback_category_audio_quality"),
-        video_quality: __("feedback_category_video_quality"),
-        disconnected : __("feedback_category_was_disconnected"),
-        confusing:     __("feedback_category_confusing"),
-        other:         __("feedback_category_other")
+        audio_quality: l10n.get("feedback_category_audio_quality"),
+        video_quality: l10n.get("feedback_category_video_quality"),
+        disconnected : l10n.get("feedback_category_was_disconnected"),
+        confusing:     l10n.get("feedback_category_confusing"),
+        other:         l10n.get("feedback_category_other")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
           React.DOM.label({key: key}, 
             React.DOM.input({type: "radio", ref: "category", name: "category", 
                    value: category, 
-                   onChange: this.handleCategoryChange}), 
+                   onChange: this.handleCategoryChange, 
+                   checked: this.state.category === category}), 
             categories[category]
           )
         );
       }, this);
     },
 
     /**
      * Checks if the form is ready for submission:
-     * - a category (reason) must be chosen
-     * - no feedback submission should be pending
+     *
+     * - no feedback submission should be pending.
+     * - a category (reason) must be chosen;
+     * - if the "other" category is chosen, a custom description must have been
+     *   entered by the end user;
      *
      * @return {Boolean}
      */
     _isFormReady: function() {
-      return this.state.category !== "" && !this.props.pending;
+      if (this.props.pending || !this.state.category) {
+        return false;
+      }
+      if (this.state.category === "other" && !this.state.description) {
+        return false;
+      }
+      return true;
     },
 
     handleCategoryChange: function(event) {
       var category = event.target.value;
-      if (category !== "other") {
-        // resets description text field
-        this.setState({description: ""});
+      this.setState({
+        category: category,
+        description: category == "other" ? "" : this._getCategories()[category]
+      });
+      if (category == "other") {
+        this.refs.description.getDOMNode().focus();
       }
-      this.setState({category: category});
     },
 
-    handleCustomTextChange: function(event) {
+    handleDescriptionFieldChange: function(event) {
       this.setState({description: event.target.value});
     },
 
+    handleDescriptionFieldFocus: function(event) {
+      this.setState({category: "other", description: ""});
+    },
+
     handleFormSubmit: function(event) {
       event.preventDefault();
       this.props.sendFeedback({
         happy: false,
         category: this.state.category,
         description: this.state.description
       });
     },
 
     render: function() {
+      var descriptionDisplayValue = this.state.category === "other" ?
+                                    this.state.description : "";
       return (
-        FeedbackLayout({title: __("feedback_what_makes_you_sad"), 
+        FeedbackLayout({title: l10n.get("feedback_what_makes_you_sad"), 
                         reset: this.props.reset}, 
           React.DOM.form({onSubmit: this.handleFormSubmit}, 
             this._getCategoryFields(), 
-            React.DOM.p(null, React.DOM.input({type: "text", ref: "description", name: "description", 
-                      disabled: this.state.category !== "other", 
-                      onChange: this.handleCustomTextChange, 
-                      value: this.state.description})), 
+            React.DOM.p(null, 
+              React.DOM.input({type: "text", ref: "description", name: "description", 
+                onChange: this.handleDescriptionFieldChange, 
+                onFocus: this.handleDescriptionFieldFocus, 
+                value: descriptionDisplayValue, 
+                placeholder: 
+                  l10n.get("feedback_custom_category_text_placeholder")})
+            ), 
             React.DOM.button({type: "submit", className: "btn btn-success", 
                     disabled: !this._isFormReady()}, 
-              __("feedback_submit_button")
+              l10n.get("feedback_submit_button")
             )
           )
         )
       );
     }
   });
 
   /**
@@ -501,20 +522,21 @@ loop.shared.views = (function(_, OT, l10
     },
 
     render: function() {
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
         window.close();
       }
       return (
-        FeedbackLayout({title: __("feedback_thank_you_heading")}, 
-          React.DOM.p({className: "info thank-you"}, __("feedback_window_will_close_in", {
-            countdown: this.state.countdown
-          }))
+        FeedbackLayout({title: l10n.get("feedback_thank_you_heading")}, 
+          React.DOM.p({className: "info thank-you"}, 
+            l10n.get("feedback_window_will_close_in", {
+              countdown: this.state.countdown
+            }))
         )
       );
     }
   });
 
   /**
    * Feedback view.
    */
@@ -568,17 +590,18 @@ loop.shared.views = (function(_, OT, l10
           return FeedbackReceived(null);
         case "form":
           return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient, 
                                sendFeedback: this.sendFeedback, 
                                reset: this.reset, 
                                pending: this.state.pending});
         default:
           return (
-            FeedbackLayout({title: __("feedback_call_experience_heading")}, 
+            FeedbackLayout({title: 
+              l10n.get("feedback_call_experience_heading")}, 
               React.DOM.div({className: "faces"}, 
                 React.DOM.button({className: "face face-happy", 
                         onClick: this.handleHappyClick}), 
                 React.DOM.button({className: "face face-sad", 
                         onClick: this.handleSadClick})
               )
             )
           );
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -7,17 +7,16 @@
 /* jshint newcap:false */
 /* global loop:true, React */
 var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = (function(_, OT, l10n) {
   "use strict";
 
   var sharedModels = loop.shared.models;
-  var __ = l10n.get;
   var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
 
   /**
    * L10n view. Translates resulting view DOM fragment once rendered.
    */
   var L10nView = (function() {
     var L10nViewImpl   = Backbone.View.extend(), // Original View constructor
         originalExtend = L10nViewImpl.extend;    // Original static extend fn
@@ -129,17 +128,17 @@ loop.shared.views = (function(_, OT, l10
       classesObj["btn-mute-" + this.props.type] = true;
       return cx(classesObj);
     },
 
     _getTitle: function(enabled) {
       var prefix = this.props.enabled ? "mute" : "unmute";
       var suffix = "button_title";
       var msgId = [prefix, this.props.scope, this.props.type, suffix].join("_");
-      return __(msgId);
+      return l10n.get(msgId);
     },
 
     render: function() {
       return (
         /* jshint ignore:start */
         <button className={this._getClasses()}
                 title={this._getTitle()}
                 onClick={this.handleClick}></button>
@@ -179,17 +178,17 @@ loop.shared.views = (function(_, OT, l10
     },
 
     render: function() {
       /* jshint ignore:start */
       return (
         <ul className="conversation-toolbar">
           <li><button className="btn btn-hangup"
                       onClick={this.handleClickHangup}
-                      title={__("hangup_button_title")}></button></li>
+                      title={l10n.get("hangup_button_title")}></button></li>
           <li><MediaControlButton action={this.handleToggleVideo}
                                   enabled={this.props.video.enabled}
                                   scope="local" type="video" /></li>
           <li><MediaControlButton action={this.handleToggleAudio}
                                   enabled={this.props.audio.enabled}
                                   scope="local" type="audio" /></li>
         </ul>
       );
@@ -366,17 +365,17 @@ loop.shared.views = (function(_, OT, l10
       reset: React.PropTypes.func // if not specified, no Back btn is shown
     },
 
     render: function() {
       var backButton = <div />;
       if (this.props.reset) {
         backButton = (
           <button className="back" type="button" onClick={this.props.reset}>
-            &laquo;&nbsp;{__("feedback_back_button")}
+            &laquo;&nbsp;{l10n.get("feedback_back_button")}
           </button>
         );
       }
       return (
         <div className="feedback">
           {backButton}
           <h3>{this.props.title}</h3>
           {this.props.children}
@@ -400,84 +399,106 @@ loop.shared.views = (function(_, OT, l10
     },
 
     getInitialProps: function() {
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
-        audio_quality: __("feedback_category_audio_quality"),
-        video_quality: __("feedback_category_video_quality"),
-        disconnected : __("feedback_category_was_disconnected"),
-        confusing:     __("feedback_category_confusing"),
-        other:         __("feedback_category_other")
+        audio_quality: l10n.get("feedback_category_audio_quality"),
+        video_quality: l10n.get("feedback_category_video_quality"),
+        disconnected : l10n.get("feedback_category_was_disconnected"),
+        confusing:     l10n.get("feedback_category_confusing"),
+        other:         l10n.get("feedback_category_other")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
           <label key={key}>
             <input type="radio" ref="category" name="category"
                    value={category}
-                   onChange={this.handleCategoryChange} />
+                   onChange={this.handleCategoryChange}
+                   checked={this.state.category === category} />
             {categories[category]}
           </label>
         );
       }, this);
     },
 
     /**
      * Checks if the form is ready for submission:
-     * - a category (reason) must be chosen
-     * - no feedback submission should be pending
+     *
+     * - no feedback submission should be pending.
+     * - a category (reason) must be chosen;
+     * - if the "other" category is chosen, a custom description must have been
+     *   entered by the end user;
      *
      * @return {Boolean}
      */
     _isFormReady: function() {
-      return this.state.category !== "" && !this.props.pending;
+      if (this.props.pending || !this.state.category) {
+        return false;
+      }
+      if (this.state.category === "other" && !this.state.description) {
+        return false;
+      }
+      return true;
     },
 
     handleCategoryChange: function(event) {
       var category = event.target.value;
-      if (category !== "other") {
-        // resets description text field
-        this.setState({description: ""});
+      this.setState({
+        category: category,
+        description: category == "other" ? "" : this._getCategories()[category]
+      });
+      if (category == "other") {
+        this.refs.description.getDOMNode().focus();
       }
-      this.setState({category: category});
     },
 
-    handleCustomTextChange: function(event) {
+    handleDescriptionFieldChange: function(event) {
       this.setState({description: event.target.value});
     },
 
+    handleDescriptionFieldFocus: function(event) {
+      this.setState({category: "other", description: ""});
+    },
+
     handleFormSubmit: function(event) {
       event.preventDefault();
       this.props.sendFeedback({
         happy: false,
         category: this.state.category,
         description: this.state.description
       });
     },
 
     render: function() {
+      var descriptionDisplayValue = this.state.category === "other" ?
+                                    this.state.description : "";
       return (
-        <FeedbackLayout title={__("feedback_what_makes_you_sad")}
+        <FeedbackLayout title={l10n.get("feedback_what_makes_you_sad")}
                         reset={this.props.reset}>
           <form onSubmit={this.handleFormSubmit}>
             {this._getCategoryFields()}
-            <p><input type="text" ref="description" name="description"
-                      disabled={this.state.category !== "other"}
-                      onChange={this.handleCustomTextChange}
-                      value={this.state.description} /></p>
+            <p>
+              <input type="text" ref="description" name="description"
+                onChange={this.handleDescriptionFieldChange}
+                onFocus={this.handleDescriptionFieldFocus}
+                value={descriptionDisplayValue}
+                placeholder={
+                  l10n.get("feedback_custom_category_text_placeholder")} />
+            </p>
             <button type="submit" className="btn btn-success"
                     disabled={!this._isFormReady()}>
-              {__("feedback_submit_button")}
+              {l10n.get("feedback_submit_button")}
             </button>
           </form>
         </FeedbackLayout>
       );
     }
   });
 
   /**
@@ -501,20 +522,21 @@ loop.shared.views = (function(_, OT, l10
     },
 
     render: function() {
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
         window.close();
       }
       return (
-        <FeedbackLayout title={__("feedback_thank_you_heading")}>
-          <p className="info thank-you">{__("feedback_window_will_close_in", {
-            countdown: this.state.countdown
-          })}</p>
+        <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
+          <p className="info thank-you">{
+            l10n.get("feedback_window_will_close_in", {
+              countdown: this.state.countdown
+            })}</p>
         </FeedbackLayout>
       );
     }
   });
 
   /**
    * Feedback view.
    */
@@ -568,17 +590,18 @@ loop.shared.views = (function(_, OT, l10
           return <FeedbackReceived />;
         case "form":
           return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
                                sendFeedback={this.sendFeedback}
                                reset={this.reset}
                                pending={this.state.pending} />;
         default:
           return (
-            <FeedbackLayout title={__("feedback_call_experience_heading")}>
+            <FeedbackLayout title={
+              l10n.get("feedback_call_experience_heading")}>
               <div className="faces">
                 <button className="face face-happy"
                         onClick={this.handleHappyClick}></button>
                 <button className="face face-sad"
                         onClick={this.handleSadClick}></button>
               </div>
             </FeedbackLayout>
           );
--- a/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
+++ b/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
@@ -148,35 +148,42 @@
 }
 
 .OT_dialog * {
   font-family: 'Didact Gothic', sans-serif;
 }
 
 .OT_dialog-plugin-prompt {
   margin-left: -350px;
-  margin-top: -141px;
+  margin-top: -127px;
   width: 650px;
-  height: 282px;
+  height: 254px;
 }
 
 .OT_dialog-plugin-reinstall {
   margin-left: -271px;
-  margin-top: -117px;
+  margin-top: -107px;
   width: 542px;
-  height: 234px;
+  height: 214px;
 }
 
 .OT_dialog-plugin-upgrading {
   margin-left: -267px;
   margin-top: -94px;
   width: 514px;
   height: 188px;
 }
 
+.OT_dialog-plugin-upgraded {
+  margin-left: -300px;
+  margin-top: -100px;
+  width: 600px;
+  height: 200px;
+}
+
 .OT_dialog-allow-deny-chrome-first {
   margin-left: -227px;
   margin-top: -122px;
   width: 453px;
   height: 244px;
 }
 
 .OT_dialog-allow-deny-chrome-pre-denied {
@@ -212,34 +219,35 @@
   margin-top: 20px;
   width: 227px;
   height: 94px;
   background-image: url(../images/rtc/access-prompt-chrome.png);
 }
 
 .OT_closeButton {
   top: 15px;
-right: 15px;
-position: absolute;
+  right: 15px;
+  position: absolute;
   font-size: 18px;
   cursor: pointer;
 }
 
 .OT_dialog-messages {
   position: absolute;
   top: 32px;
   left: 32px;
   right: 32px;
   text-align: center;
 }
 
 .OT_dialog-allow-deny-firefox-maybe-denied .OT_dialog-messages {
   top: 45px;
 }
 
+
 .OT_dialog-messages-main {
   font-weight: 300;
   font-size: 18pt;
   line-height: 24px;
 }
 
 .OT_dialog-messages-minor {
   font-weight: 300;
@@ -311,16 +319,22 @@ position: absolute;
   line-height: 50px;
   height: 47px;
   background-color: #29A4DA;
   text-align: center;
   font-size: 16pt;
   cursor: pointer;
 }
 
+.OT_dialog-button.OT_dialog-button-disabled {
+  background-color: #444444;
+  color: #999999;
+  cursor: not-allowed;
+}
+
 .OT_dialog-button.OT_dialog-button-large {
   line-height: 60px;
   height: 58px;
 }
 
 .OT_dialog-progress-bar {
   border: 1px solid #4E4E4E;
   height: 8px;
@@ -444,21 +458,27 @@ position: absolute;
 
 .OT_publisher, .OT_subscriber {
     position: relative;
     min-width: 48px;
     min-height: 48px;
 }
 
 .OT_publisher video,
-.OT_subscriber video {
+.OT_subscriber video,
+.OT_publisher object,
+.OT_subscriber object {
     width: 100%;
     height: 100%;
 }
 
+.OT_publisher object,
+.OT_subscriber object {
+}
+
 /* Styles that are applied when the video element should be mirrored */
 .OT_publisher.OT_mirrored video{
     -webkit-transform: scale(-1, 1);
     -moz-transform:scale(-1,1);
 }
 
 .OT_subscriber_error {
 		background-color: #000;
@@ -531,17 +551,17 @@ position: absolute;
 
 .OT_publisher .OT_archiving-light-box,
 .OT_subscriber .OT_archiving-light-box {
     background: rgba(0, 0, 0, 0.4);
     top: auto;
     bottom: 0;
     right: auto;
     width: 34px;
-    height: 34px;    
+    height: 34px;
 }
 
 .OT_archiving-light {
   width: 7px;
   height: 7px;
   -webkit-border-radius: 30px;
   -moz-border-radius: 30px;
   border-radius: 30px;
@@ -717,17 +737,17 @@ position: absolute;
     box-shadow: 0 0 0px 0px #c70019;
   }
 }
 
 .OT_mini .OT_bar,
 .OT_bar.OT_mode-mini,
 .OT_bar.OT_mode-mini-auto {
     bottom: 0;
-    height: auto;    
+    height: auto;
 }
 
 .OT_mini .OT_name.OT_mode-off,
 .OT_mini .OT_name.OT_mode-on,
 .OT_mini .OT_name.OT_mode-auto,
 .OT_mini:hover .OT_name.OT_mode-auto {
     display: none;
 }
@@ -756,17 +776,17 @@ position: absolute;
     text-align: center;
     text-indent: -9999em;
     background-color: transparent;
     background-repeat: no-repeat;
 }
 
 .OT_publisher .OT_opentok,
 .OT_subscriber .OT_opentok {
-    background: url(../images/rtc/buttons.png) 0 -32px no-repeat;    
+    background: url(../images/rtc/buttons.png) 0 -32px no-repeat;
     cursor: default;
     height: 18px;
     left: 8px;
     line-height: 18px;
     top: 8px;
     width: 16px;
 }
 
@@ -946,28 +966,30 @@ position: absolute;
 }
 
 .OT_publisher.OT_loading .OT_video-loading,
 .OT_subscriber.OT_loading .OT_video-loading {
     display: block;
 }
 
 .OT_publisher.OT_loading video,
-.OT_subscriber.OT_loading video {
+.OT_subscriber.OT_loading video,
+.OT_publisher.OT_loading object,
+.OT_subscriber.OT_loading object {
     display: none;
 }
 
 
 .OT_video-poster {
     width: 100%;
     height: 100%;
     background-position: 50% 50%;
     background-repeat: no-repeat;
     display: none;
 }
 
 .OT_publisher .OT_video-poster {
-    background-image: url(../images/rtc/audioonly-publisher.png);    
+    background-image: url(../images/rtc/audioonly-publisher.png);
 }
 
 .OT_subscriber .OT_video-poster  {
     background-image: url(../images/rtc/audioonly-subscriber.png);
 }
--- a/browser/components/loop/content/shared/libs/sdk.js
+++ b/browser/components/loop/content/shared/libs/sdk.js
@@ -1,25 +1,25 @@
 /**
- * @license  OpenTok JavaScript Library v2.2.6
+ * @license  OpenTok JavaScript Library v2.2.7.2
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: June 24 11:09:07 2014
+ * Date: August 05 08:56:17 2014
  */
 
 (function(window) {
   if (!window.OT) window.OT = {};
 
   OT.properties = {
-    version: 'v2.2.6',         // The current version (eg. v2.0.4) (This is replaced by gradle)
-    build: 'd326ad1',    // The current build hash (This is replaced by gradle)
+    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)
 
     // 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',
@@ -43,24 +43,24 @@
     minimumVersion: {
       firefox: parseFloat('26'),
       chrome: parseFloat('32')
     }
   };
 
 })(window);
 /**
- * @license  Common JS Helpers on OpenTok 0.2.0 1f056b9 master
+ * @license  Common JS Helpers on OpenTok 0.2.0 5c6f145 vib-2.2-node-fixes
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: May 19 04:04:43 2014
+ * Date: July 28 08:28:31 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)
@@ -82,16 +82,19 @@
   var OTHelpers = function(domId) {
     return document.getElementById(domId);
   };
 
   var previousOTHelpers = window.OTHelpers;
 
   window.OTHelpers = OTHelpers;
 
+  // A guard to detect when IE has performed cleans on unload
+  window.___othelpers = true;
+
   OTHelpers.keys = Object.keys || function(object) {
     var keys = [], hasOwnProperty = Object.prototype.hasOwnProperty;
     for(var key in object) {
       if(hasOwnProperty.call(object, key)) {
         keys.push(key);
       }
     }
     return keys;
@@ -1028,75 +1031,333 @@ 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);
+        }
+    };
+
+})(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),
+
+      _loadCallbacks = [],
+      _unloadCallbacks = [],
+      _domUnloaded = false,
+
+      onDomReady = function() {
+        _domReady = true;
+
+        // This is making an assumption about there being only one "window"
+        // that we care about.
+        OTHelpers.on(window, 'unload', onDomUnload);
+
+        OTHelpers.forEach(_loadCallbacks, function(listener) {
+          listener[0].call(listener[1]);
+        });
+
+        _loadCallbacks = [];
+      },
+
+      onDomUnload = function() {
+        _domUnloaded = true;
+
+        OTHelpers.forEach(_unloadCallbacks, function(listener) {
+          listener[0].call(listener[1]);
+        });
+
+        _unloadCallbacks = [];
+      };
+
+
+  OTHelpers.onDOMLoad = function(cb, context) {
+    if (OTHelpers.isReady()) {
+      cb.call(context);
+      return;
+    }
+
+    _loadCallbacks.push([cb, context]);
+  };
+
+  OTHelpers.onDOMUnload = function(cb, context) {
+    if (this.isDOMUnloaded()) {
+      cb.call(context);
+      return;
+    }
+
+    _unloadCallbacks.push([cb, context]);
+  };
+
+  OTHelpers.isReady = function() {
+    return !_domUnloaded && _domReady;
+  };
+
+  OTHelpers.isDOMUnloaded = function() {
+    return _domUnloaded;
+  };
+
+
+  if (_domReady) {
+    onDomReady();
+  } 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();
+      });
+    }
+  }
+
+})(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());
+  //   });
+  //
+  OTHelpers.registerCapability = function(name, callback) {
+    var _name = name.toLowerCase();
+
+    if (capabilities.hasOwnProperty(_name)) {
+      OTHelpers.error('Attempted to register', name, 'capability more than once');
+      return;
+    }
+
+    if (!OTHelpers.isFunction(callback)) {
+      OTHelpers.error('Attempted to register', name,
+                              'capability with a callback that isn\' a function');
+      return;
+    }
+
+    memoriseCapabilityTest(_name, callback);
+  };
+
+  // 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) {
+        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')
+
 (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')
 // tb_require('../vendor/uuid.js')
+// tb_require('./dom_events.js')
 
 (function(window, OTHelpers, undefined) {
 
-  var timeouts = [],
-      messageName = 'OTHelpers.' + OTHelpers.uuid.v4() + '.zero-timeout';
-
-  var handleMessage = function(event) {
-    if (event.data === messageName) {
-      if(OTHelpers.isFunction(event.stopPropagation)) {
-        event.stopPropagation();
-      }
-      event.cancelBubble = true;
-      if (timeouts.length > 0) {
-        var args = timeouts.shift(),
-            fn = args.shift();
-
+  var _callAsync;
+
+  // Is true if window.postMessage is supported.
+  // This is not quite as simple as just looking for
+  // window.postMessage as some older versions of IE
+  // have a broken implementation of it.
+  //
+  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("", "*");
+      window.onmessage = oldOnMessage;
+      return postMessageIsAsynchronous;
+    }
+  })();
+
+  if (supportsPostMessage) {
+    var timeouts = [],
+        messageName = 'OTHelpers.' + OTHelpers.uuid.v4() + '.zero-timeout';
+
+    var removeMessageHandler = function() {
+      timeouts = [];
+
+      if(window.removeEventListener) {
+        window.removeEventListener('message', handleMessage);
+      } else if(window.detachEvent) {
+        window.detachEvent('onmessage', handleMessage);
+      }
+    };
+
+    var handleMessage = function(event) {
+      if (event.source === window &&
+          event.data === messageName) {
+
+        if(OTHelpers.isFunction(event.stopPropagation)) {
+          event.stopPropagation();
+        }
+        event.cancelBubble = true;
+
+        if (!window.___othelpers) {
+          removeMessageHandler();
+          return;
+        }
+
+        if (timeouts.length > 0) {
+          var args = timeouts.shift(),
+              fn = args.shift();
+
+          fn.apply(null, args);
+        }
+      }
+    };
+
+    // Ensure that we don't receive messages after unload
+    // Yes, this seems to really happen in IE sometimes, usually
+    // when iFrames are involved.
+    OTHelpers.on(window, 'unload', removeMessageHandler);
+
+    if(window.addEventListener) {
+      window.addEventListener('message', handleMessage, true);
+    } else if(window.attachEvent) {
+      window.attachEvent('onmessage', handleMessage);
+    }
+
+    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
+      timeouts.push(Array.prototype.slice.call(arguments));
+      window.postMessage(messageName, '*');
+    };
+  }
+  else {
+    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
+      var args = Array.prototype.slice.call(arguments),
+          fn = args.shift();
+
+      setTimeout(function() {
         fn.apply(null, args);
-      }
-    }
-  };
-
-  if(window.addEventListener) {
-    window.addEventListener('message', handleMessage, true);
-  } else if(window.attachEvent) {
-    window.attachEvent('onmessage', handleMessage);
-  }
+      }, 0);
+    };
+  }
+
 
   // Calls the function +fn+ asynchronously with the current execution.
   // This is most commonly used to execute something straight after
   // the current function.
   //
   // Any arguments in addition to +fn+ will be passed to +fn+ when it's
   // called.
   //
   // You would use this inplace of setTimeout(fn, 0) type constructs. callAsync
   // is preferable as it executes in a much more predictable time window,
   // unlike setTimeout which could execute anywhere from 2ms to several thousand
   // depending on the browser/context.
   //
-  OTHelpers.callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
-    timeouts.push(Array.prototype.slice.call(arguments));
-    window.postMessage(messageName, '*');
-  };
+  // It does this using window.postMessage, although if postMessage won't
+  // work it will fallback to setTimeout.
+  //
+  OTHelpers.callAsync = _callAsync;
 
 
   // Wraps +handler+ in a function that will execute it asynchronously
   // so that it doesn't interfere with it's exceution context if it raises
   // an exception.
   OTHelpers.createAsyncHandler = function(handler) {
     return function() {
       var args = Array.prototype.slice.call(arguments);
@@ -1239,29 +1500,37 @@ OTHelpers.roundFloat = function(value, p
     var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) {
       var listener = {handler: handler};
       if (context) listener.context = context;
       if (closure) listener.closure = closure;
 
       OTHelpers.forEach(eventNames, function(name) {
         if (!_events[name]) _events[name] = [];
         _events[name].push(listener);
+        var addedListener = name + ':added';
+        if (_events[addedListener]) {
+          executeListeners(addedListener, [_events[name].length]);
+        }
       });
     }, self);
 
 
     var removeListeners = function (eventNames, handler, context) {
       function filterHandlerAndContext(listener) {
         return !(listener.handler === handler && listener.context === context);
       }
 
       OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) {
         if (_events[name]) {
           _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext);
           if (_events[name].length === 0) delete _events[name];
+          var removedListener = name + ':removed';
+          if (_events[ removedListener]) {
+            executeListeners(removedListener, [_events[name] ? _events[name].length : 0]);
+          }
         }
       }, self));
 
     };
 
     // Execute any listeners bound to the +event+ Event.
     //
     // Each handler will be executed async. On completion the defaultAction
@@ -1658,18 +1927,16 @@ OTHelpers.roundFloat = function(value, p
     // @depreciated will become a private helper function in the future.
     self.removeEventListener = function(eventName, handler, context) {
       OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.');
       removeListeners([eventName], handler, context);
     };
 
 
 
-
-
     return self;
   };
 
   OTHelpers.eventing.Event = function() {
 
     return function (type, cancelable) {
       this.type = type;
       this.cancelable = cancelable !== undefined ? cancelable : true;
@@ -1796,45 +2063,16 @@ OTHelpers.createButton = function(innerH
         }
 
         button._boundEvents = events;
     }
 
     return button;
 };
 
-// Helper function for adding event listeners to dom elements.
-// WARNING: This doesn't preserve event types, your handler could be getting all kinds of different
-// parameters depending on the browser. You also may have different scopes depending on the browser
-// and bubbling and cancelable are not supported.
-OTHelpers.on = function(element, eventName,  handler) {
-    if (element.addEventListener) {
-        element.addEventListener(eventName, handler, false);
-    } else if (element.attachEvent) {
-        element.attachEvent("on" + eventName, handler);
-    } else {
-        var oldHandler = element["on"+eventName];
-        element["on"+eventName] = function() {
-          handler.apply(this, arguments);
-          if (oldHandler) oldHandler.apply(this, arguments);
-        };
-    }
-    return element;
-};
-
-// Helper function for removing event listeners from dom elements.
-OTHelpers.off = function(element, eventName, handler) {
-    if (element.removeEventListener) {
-        element.removeEventListener (eventName, handler,false);
-    }
-    else if (element.detachEvent) {
-        element.detachEvent("on" + eventName, handler);
-    }
-};
-
 
 // Detects when an element is not part of the document flow because it or one of it's ancesters has display:none.
 OTHelpers.isDisplayNone = function(element) {
     if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return true;
     if (element.parentNode && element.parentNode.style) return OTHelpers.isDisplayNone(element.parentNode);
     return false;
 };
 
@@ -1900,17 +2138,17 @@ OTHelpers.observeStyleChanges = function
 
         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;
                 }
@@ -2467,16 +2705,34 @@ 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);
+
+})(window, window.OTHelpers);
+
+// AJAX helpers
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+(function(window, OTHelpers, undefined) {
+
   function formatPostData(data) { //, contentType
     // If it's a string, we assume it's properly encoded
     if (typeof(data) === 'string') return data;
 
     var queryString = [];
 
     for (var key in data) {
       queryString.push(
@@ -2580,29 +2836,29 @@ OTHelpers.centerElement = function(eleme
       callback(new Error('No HTTP method specified in options'));
       return;
     }
 
     // Setup callbacks to correctly respond to success and error callbacks. This includes
     // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore
     // by default.
     if(callback) {
-      request.addEventListener('load', function(event) {
+      OTHelpers.on(request, 'load', function(event) {
         var status = event.target.status;
 
         // We need to detect things that XMLHttpRequest considers a success,
         // but we consider to be failures.
         if ( status >= 200 && status < 300 || status === 304 ) {
           callback(null, event);
         } else {
           callback(event);
         }
-      }, false);
-
-      request.addEventListener('error', callback, false);
+      });
+
+      OTHelpers.on(request, 'error', callback);
     }
 
     request.open(options.method, url, true);
 
     if (!_options.headers) _options.headers = {};
 
     for (var name in _options.headers) {
       request.setRequestHeader(name, _options.headers[name]);
@@ -2626,20 +2882,159 @@ OTHelpers.centerElement = function(eleme
     if(_options.xdomainrequest) {
       OTHelpers.xdomainRequest(url, _options, callback);
     } else {
       OTHelpers.request(url, _options, callback);
     }
   };
 
 })(window, window.OTHelpers);
+!(function(window) {
+
+  /* global OTHelpers */
+
+  if (!window.OT) window.OT = {};
+
+  // Bring OTHelpers in as OT.$
+  OT.$ = OTHelpers.noConflict();
+
+  // Allow events to be bound on OT
+  OT.$.eventing(OT);
+
+  // REMOVE THIS POST IE MERGE
+
+  OT.$.defineGetters = function(self, getters, enumerable) {
+    var propsDefinition = {};
+
+    if (enumerable === void 0) enumerable = false;
+
+    for (var key in getters) {
+      if(!getters.hasOwnProperty(key)) {
+        continue;
+      }
+      propsDefinition[key] = {
+        get: getters[key],
+        enumerable: enumerable
+      };
+    }
+
+    Object.defineProperties(self, propsDefinition);
+  };
+
+  // STOP REMOVING HERE
+
+  // OT.$.Modal was OT.Modal before the great common-js-helpers move
+  OT.Modal = OT.$.Modal;
+
+  // Add logging methods
+  OT.$.useLogHelpers(OT);
+
+  var _debugHeaderLogged = false,
+      _setLogLevel = OT.setLogLevel;
+
+  // On the first time log level is set to DEBUG (or higher) show version info.
+  OT.setLogLevel = function(level) {
+    // Set OT.$ to the same log level
+    OT.$.setLogLevel(level);
+    var retVal = _setLogLevel.call(OT, level);
+    if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) {
+      OT.debug('OpenTok JavaScript library ' + OT.properties.version);
+      OT.debug('Release notes: ' + OT.properties.websiteURL +
+        '/opentok/webrtc/docs/js/release-notes.html');
+      OT.debug('Known issues: ' + OT.properties.websiteURL +
+        '/opentok/webrtc/docs/js/release-notes.html#knownIssues');
+      _debugHeaderLogged = true;
+    }
+    OT.debug('OT.setLogLevel(' + retVal + ')');
+    return retVal;
+  };
+
+  OT.setLogLevel(OT.properties.debug ? OT.DEBUG : OT.ERROR);
+
+  OT.$.userAgent = function() {
+    var userAgent = navigator.userAgent;
+    if (TBPlugin.isInstalled()) userAgent += '; TBPlugin ' + TBPlugin.version();
+    return userAgent;
+  };
+
+  /**
+  * Sets the API log level.
+  * <p>
+  * Calling <code>OT.setLogLevel()</code> sets the log level for runtime log messages that
+  * are the OpenTok library generates. The default value for the log level is <code>OT.ERROR</code>.
+  * </p>
+  * <p>
+  * The OpenTok JavaScript library displays log messages in the debugger console (such as
+  * Firebug), if one exists.
+  * </p>
+  * <p>
+  * The following example logs the session ID to the console, by calling <code>OT.log()</code>.
+  * The code also logs an error message when it attempts to publish a stream before the Session
+  * object dispatches a <code>sessionConnected</code> event.
+  * </p>
+  * <pre>
+  * OT.setLogLevel(OT.LOG);
+  * session = OT.initSession(sessionId);
+  * OT.log(sessionId);
+  * publisher = OT.initPublisher("publishContainer");
+  * session.publish(publisher);
+  * </pre>
+  *
+  * @param {Number} logLevel The degree of logging desired by the developer:
+  *
+  * <p>
+  * <ul>
+  *   <li>
+  *     <code>OT.NONE</code> &#151; API logging is disabled.
+  *   </li>
+  *   <li>
+  *     <code>OT.ERROR</code> &#151; Logging of errors only.
+  *   </li>
+  *   <li>
+  *     <code>OT.WARN</code> &#151; Logging of warnings and errors.
+  *   </li>
+  *   <li>
+  *     <code>OT.INFO</code> &#151; Logging of other useful information, in addition to
+  *     warnings and errors.
+  *   </li>
+  *   <li>
+  *     <code>OT.LOG</code> &#151; Logging of <code>OT.log()</code> messages, in addition
+  *     to OpenTok info, warning,
+  *     and error messages.
+  *   </li>
+  *   <li>
+  *     <code>OT.DEBUG</code> &#151; Fine-grained logging of all API actions, as well as
+  *     <code>OT.log()</code> messages.
+  *   </li>
+  * </ul>
+  * </p>
+  *
+  * @name OT.setLogLevel
+  * @memberof OT
+  * @function
+  * @see <a href="#log">OT.log()</a>
+  */
+
+  /**
+  * Sends a string to the the debugger console (such as Firebug), if one exists.
+  * However, the function only logs to the console if you have set the log level
+  * to <code>OT.LOG</code> or <code>OT.DEBUG</code>,
+  * by calling <code>OT.setLogLevel(OT.LOG)</code> or <code>OT.setLogLevel(OT.DEBUG)</code>.
+  *
+  * @param {String} message The string to log.
+  *
+  * @name OT.log
+  * @memberof OT
+  * @function
+  * @see <a href="#setLogLevel">OT.setLogLevel()</a>
+  */
+
+})(window);
 !(function() {
 
-  OT.Dialogs = {};
-
   var addCss = function(document, url, callback) {
     var head = document.head || document.getElementsByTagName('head')[0];
     var cssTag = OT.$.createElement('link', {
       type: 'text/css',
       media: 'screen',
       rel: 'stylesheet',
       href: url
     });
@@ -2660,26 +3055,55 @@ OTHelpers.centerElement = function(eleme
       addCss(document, stylesheetUrl, function() {
         if(--remainingStylesheets <= 0) {
           callback();
         }
       });
     });
 
   };
-  
+
   var templateElement = function(classes, children, tagName) {
     var el = OT.$.createElement(tagName || 'div', { 'class': classes }, children, this);
     el.on = OT.$.bind(OT.$.on, OT.$, el);
+    el.off = OT.$.bind(OT.$.off, OT.$, el);
     return el;
   };
 
-  OT.Dialogs.AllowDeny = {};
-  OT.Dialogs.AllowDeny.Chrome = {};
-  OT.Dialogs.AllowDeny.Firefox = {};
+  var checkBoxElement = function (classes, nameAndId, onChange) {
+    var checkbox = templateElement.call(this, '', null, 'input').on('change', onChange);
+
+    if (OT.$.browser() === 'ie' && OT.$.browserVersion() <= 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');
+
+    return checkbox;
+  };
+
+  var linkElement = function(children, href, classes) {
+    var link = templateElement.call(this, classes || '', children, 'a');
+    link.setAttribute('href', href);
+    return link;
+  };
+
+  OT.Dialogs = {};
+
+  OT.Dialogs.AllowDeny = {
+    Chrome: {},
+    Firefox: {}
+  };
 
   OT.Dialogs.AllowDeny.Chrome.initialPrompt = function() {
     var modal = new OT.$.Modal(function(window, document) {
 
       var el = templateElement.bind(document),
           close, root;
 
       close = el('OT_closeButton', '&times;')
@@ -2822,17 +3246,16 @@ OTHelpers.centerElement = function(eleme
       addDialogCSS(document, [], function() {
         document.body.appendChild(root);
       });
 
     });
     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'),
           root,
           refreshButton;
 
@@ -2853,193 +3276,297 @@ OTHelpers.centerElement = function(eleme
         ])
       );
 
       addDialogCSS(document, [], function() {
         document.body.appendChild(root);
       });
 
     });
+
     return modal;
   };
 
+  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'),
+          acceptEULA,
+          checkbox,
+          close,
+          root;
+
+      function onDownload() {
+        modal.trigger('download');
+      }
+
+      function onRefresh() {
+        modal.trigger('refresh');
+      }
+
+      function onToggleEULA() {
+        if (checkbox.checked) {
+          enableButtons();
+        }
+        else {
+          disableButtons();
+        }
+      }
+
+      function enableButtons() {
+        OT.$.removeClass(downloadButton, 'OT_dialog-button-disabled');
+        downloadButton.on('click', onDownload);
+
+        OT.$.removeClass(refreshButton, 'OT_dialog-button-disabled');
+        refreshButton.on('click', onRefresh);
+      }
+
+      function disableButtons() {
+        OT.$.addClass(downloadButton, 'OT_dialog-button-disabled');
+        downloadButton.off('click', onDownload);
+
+        OT.$.addClass(refreshButton, 'OT_dialog-button-disabled');
+        refreshButton.off('click', onRefresh);
+      }
+
+
+      close = el('OT_closeButton', '&times;')
+        .on('click', function() {
+          modal.close();
+        });
+
+      acceptEULA = linkElement.call(document,
+                                    'End-user license agreement',
+                                    'http://tokbox.com/support/ie-eula');
+
+      checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA);
+
+      root = el('OT_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-button-pair', [
+          el('OT_dialog-button-with-title', [
+            el('OT_dialog-button-title', [
+              el('', 'Step 1', 'strong'),
+              checkbox,
+              (function() {
+                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', [
+            el('OT_dialog-button-title', [
+              el('', 'Step 2', 'strong'),
+              'Reload this page after installation'
+            ]),
+            refreshButton
+          ])
+        ])
+      ]);
+
+      addDialogCSS(document, [], function() {
+        document.body.appendChild(root);
+      });
+
+    });
+    return modal;
+  };
+
+  OT.Dialogs.Plugin.promptToReinstall = function() {
+    var modal = new OT.$.Modal(function(window, document) {
+
+      var el = templateElement.bind(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.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 ' +
+            'enable real-time video communication for Internet Explorer.')
+        ]),
+        el('OT_dialog-single-button', okayButton)
+      ]);
+
+      addDialogCSS(document, [], function() {
+        document.body.appendChild(root);
+      });
+
+    });
+
+    return modal;
+  };
+
+  OT.Dialogs.Plugin.updateInProgress = function() {
+
+    var progressBar,
+        progressText,
+        progressValue = 0;
+
+    var modal = new OT.$.Modal(function(window, document) {
+
+      var el = templateElement.bind(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', [
+          el('OT_dialog-messages-main', [
+            'One moment please... ',
+            progressText
+          ]),
+          el('OT_dialog-progress-bar', progressBar),
+          el('OT_dialog-messages-minor', 'Please wait while the OpenTok plugin is updated')
+        ])
+      ]);
+
+      addDialogCSS(document, [], function() {
+        document.body.appendChild(root);
+        if(progressValue != null) {
+          modal.setUpdateProgress(progressValue);
+        }
+      });
+    });
+
+    modal.setUpdateProgress = function(newProgress) {
+      if(progressBar && progressText) {
+        if(newProgress > 99) {
+          OT.$.css(progressBar, 'width', '');
+          progressText.innerHTML = '100%';
+        } else if(newProgress < 1) {
+          OT.$.css(progressBar, 'width', '0%');
+          progressText.innerHTML = '0%';
+        } else {
+          OT.$.css(progressBar, 'width', newProgress + '%');
+          progressText.innerHTML = newProgress + '%';
+        }
+      } else {
+        progressValue = newProgress;
+      }
+    };
+
+    return modal;
+  };
+
+  OT.Dialogs.Plugin.updateComplete = function(error) {
+    var modal = new OT.$.Modal(function(window, document) {
+      var el = templateElement.bind(document),
+          reloadButton,
+          root;
+
+      reloadButton = el('OT_dialog-button', 'Reload').on('click', function() {
+        modal.trigger('reload');
+      });
+
+      var msgs;
+
+      if(error) {
+        msgs = ['Update Failed.', error + '' || 'NO ERROR'];
+      } else {
+        msgs = ['Update Complete.',
+          'The OpenTok plugin has been succesfully updated. ' +
+          'Please reload your browser.'];
+      }
+
+      root = el('OT_root OT_dialog OT_dialog-plugin-upgraded', [
+        el('OT_dialog-messages', [
+          el('OT_dialog-messages-main', msgs[0]),
+          el('OT_dialog-messages-minor', msgs[1])
+        ]),
+        el('OT_dialog-single-button', reloadButton)
+      ]);
+
+      addDialogCSS(document, [], function() {
+        document.body.appendChild(root);
+      });
+
+    });
+
+    return modal;
+
+  };
+
+
 })();
 !(function(window) {
 
-  /* global OTHelpers */
-
-  if (!window.OT) window.OT = {};
-
-  // Bring OTHelpers in as OT.$
-  OT.$ = OTHelpers.noConflict();
-
-  // Allow events to be bound on OT
-  OT.$.eventing(OT);
-
-  // REMOVE THIS POST IE MERGE
-
-  OT.$.defineGetters = function(self, getters, enumerable) {
-    var propsDefinition = {};
-
-    if (enumerable === void 0) enumerable = false;
-
-    for (var key in getters) {
-      if(!getters.hasOwnProperty(key)) {
-        continue;
-      }
-      propsDefinition[key] = {
-        get: getters[key],
-        enumerable: enumerable
-      };
-    }
-
-    Object.defineProperties(self, propsDefinition);
-  };
-
-  // STOP REMOVING HERE
-
-  // OT.$.Modal was OT.Modal before the great common-js-helpers move
-  OT.Modal = OT.$.Modal;
-
-  // Add logging methods
-  OT.$.useLogHelpers(OT);
-
-  var _debugHeaderLogged = false,
-      _setLogLevel = OT.setLogLevel;
-
-  // On the first time log level is set to DEBUG (or higher) show version info.
-  OT.setLogLevel = function(level) {
-    // Set OT.$ to the same log level
-    OT.$.setLogLevel(level);
-    var retVal = _setLogLevel.call(OT, level);
-    if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) {
-      OT.debug('OpenTok JavaScript library ' + OT.properties.version);
-      OT.debug('Release notes: ' + OT.properties.websiteURL +
-        '/opentok/webrtc/docs/js/release-notes.html');
-      OT.debug('Known issues: ' + OT.properties.websiteURL +
-        '/opentok/webrtc/docs/js/release-notes.html#knownIssues');
-      _debugHeaderLogged = true;
-    }
-    OT.debug('OT.setLogLevel(' + retVal + ')');
-    return retVal;
-  };
-
-  OT.setLogLevel(OT.properties.debug ? OT.DEBUG : OT.ERROR);
-
-  /**
-  * Sets the API log level.
-  * <p>
-  * Calling <code>OT.setLogLevel()</code> sets the log level for runtime log messages that
-  * are the OpenTok library generates. The default value for the log level is <code>OT.ERROR</code>.
-  * </p>
-  * <p>
-  * The OpenTok JavaScript library displays log messages in the debugger console (such as
-  * Firebug), if one exists.
-  * </p>
-  * <p>
-  * The following example logs the session ID to the console, by calling <code>OT.log()</code>.
-  * The code also logs an error message when it attempts to publish a stream before the Session
-  * object dispatches a <code>sessionConnected</code> event.
-  * </p>
-  * <pre>
-  * OT.setLogLevel(OT.LOG);
-  * session = OT.initSession(sessionId);
-  * OT.log(sessionId);
-  * publisher = OT.initPublisher("publishContainer");
-  * session.publish(publisher);
-  * </pre>
-  *
-  * @param {Number} logLevel The degree of logging desired by the developer:
-  *
-  * <p>
-  * <ul>
-  *   <li>
-  *     <code>OT.NONE</code> &#151; API logging is disabled.
-  *   </li>
-  *   <li>
-  *     <code>OT.ERROR</code> &#151; Logging of errors only.
-  *   </li>
-  *   <li>
-  *     <code>OT.WARN</code> &#151; Logging of warnings and errors.
-  *   </li>
-  *   <li>
-  *     <code>OT.INFO</code> &#151; Logging of other useful information, in addition to
-  *     warnings and errors.
-  *   </li>
-  *   <li>
-  *     <code>OT.LOG</code> &#151; Logging of <code>OT.log()</code> messages, in addition
-  *     to OpenTok info, warning,
-  *     and error messages.
-  *   </li>
-  *   <li>
-  *     <code>OT.DEBUG</code> &#151; Fine-grained logging of all API actions, as well as
-  *     <code>OT.log()</code> messages.
-  *   </li>
-  * </ul>
-  * </p>
-  *
-  * @name OT.setLogLevel
-  * @memberof OT
-  * @function
-  * @see <a href="#log">OT.log()</a>
-  */
-
-  /**
-  * Sends a string to the the debugger console (such as Firebug), if one exists.
-  * However, the function only logs to the console if you have set the log level
-  * to <code>OT.LOG</code> or <code>OT.DEBUG</code>,
-  * by calling <code>OT.setLogLevel(OT.LOG)</code> or <code>OT.setLogLevel(OT.DEBUG)</code>.
-  *
-  * @param {String} message The string to log.
-  *
-  * @name OT.log
-  * @memberof OT
-  * @function
-  * @see <a href="#setLogLevel">OT.setLogLevel()</a>
-  */
-
-})(window);
-!(function(window) {
-
   // IMPORTANT This file should be included straight after helpers.js
   if (!window.OT) window.OT = {};
 
   if (!OT.properties) {
     throw new Error('OT.properties does not exist, please ensure that you include a valid ' +
       'properties file.');
   }
 
+  OT.useSSL = function () {
+    return OT.properties.supportSSL && (window.location.protocol.indexOf('https') >= 0 ||
+          window.location.protocol.indexOf('chrome-extension') >= 0);
+  };
+
   // Consumes and overwrites OT.properties. Makes it better and stronger!
   OT.properties = function(properties) {
     var props = OT.$.clone(properties);
 
     props.debug = properties.debug === 'true' || properties.debug === true;
     props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
 
-    if (props.supportSSL && (window.location.protocol.indexOf('https') >= 0 ||
-      window.location.protocol.indexOf('chrome-extension') >= 0)) {
-      props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version;
-    } else {
-      props.assetURL = props.cdnURL + '/webrtc/' + props.version;
-    }
-
-    props.configURL = props.assetURL + '/js/dynamic_config.min.js';
-    props.cssURL = props.assetURL + '/css/ot.min.css';
-    
     if (window.OTProperties) {
       // Allow window.OTProperties to override cdnURL, configURL, assetURL and cssURL
       if (window.OTProperties.cdnURL) props.cdnURL = window.OTProperties.cdnURL;
+      if (window.OTProperties.cdnURLSSL) props.cdnURLSSL = window.OTProperties.cdnURLSSL;
       if (window.OTProperties.configURL) props.configURL = window.OTProperties.configURL;
       if (window.OTProperties.assetURL) props.assetURL = window.OTProperties.assetURL;
       if (window.OTProperties.cssURL) props.cssURL = window.OTProperties.cssURL;
     }
 
+    if (!props.assetURL) {
+      if (OT.useSSL()) {
+        props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version;
+      } else {
+        props.assetURL = props.cdnURL + '/webrtc/' + props.version;
+      }
+    }
+
+    if (!props.configURL) props.configURL = props.assetURL + '/js/dynamic_config.min.js';
+    if (!props.cssURL) props.cssURL = props.assetURL + '/css/ot.min.css';
+
     return props;
   }(OT.properties);
-
 })(window);
 !(function() {
 
 //--------------------------------------
 // JS Dynamic Config
 //--------------------------------------
 
 
@@ -3107,17 +3634,17 @@ OTHelpers.centerElement = function(eleme
         if (!configUrl) throw new Error('You must pass a valid configUrl to Config.load');
 
         _loaded = false;
 
         setTimeout(function() {
           _script = document.createElement( 'script' );
           _script.async = 'async';
           _script.src = configUrl;
-          _script.onload = _script.onreadystatechange = _onLoad.bind(this);
+          _script.onload = _script.onreadystatechange = OT.$.bind(_onLoad, this);
           _head.appendChild(_script);
         },1);
 
         _loadTimer = setTimeout(function() {
           _this._onLoadTimeout();
         }, this.loadTimeout);
       },
 
@@ -3167,30 +3694,1600 @@ OTHelpers.centerElement = function(eleme
     };
 
     OT.$.eventing(_this);
 
     return _this;
   })();
 
 })(window);
+/**
+ * @license  TB Plugin 0.4.0.7 9425efe 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
+ *
+ */
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: false,
+          trailing: true, browser: true, smarttabs:true */
+/* global scope:true, OT:true */
+/* exported TBPlugin */
+
+/* jshint ignore:start */
+(function(scope) {
+/* jshint ignore:end */
+
+// If we've already be setup, bail
+if (scope.TBPlugin !== void 0) return;
+
+// TB must exist first, otherwise we can't do anything
+if (scope.OT === void 0) return;
+
+// Establish the environment that we're running in
+var env = OT.$.browserVersion(),
+    isSupported = env.browser === 'IE' && env.version >= 8,
+    pluginReady = false;
+
+var TBPlugin = {
+  isSupported: function () { return isSupported; },
+  isReady: function() { return pluginReady; }
+};
+
+
+scope.TBPlugin = TBPlugin;
+
+// We only support IE, version 10 or above right now
+if (!TBPlugin.isSupported()) {
+  TBPlugin.isInstalled = function isInstalled () { return false; };
+  return;
+}
+
+// tb_require('./header.js')
+
+/* exported shim */
+
+// Shims for various missing things from JS
+// Applied only after init is called to prevent unnecessary polution
+var shim = function shim () {
+  if (!Function.prototype.bind) {
+    Function.prototype.bind = function (oThis) {
+      if (typeof this !== 'function') {
+        // closest thing possible to the ECMAScript 5 internal IsCallable function
+        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+      }
+
+      var aArgs = Array.prototype.slice.call(arguments, 1),
+          fToBind = this,
+          FNOP = function () {},
+          fBound = function () {
+            return fToBind.apply(this instanceof FNOP && oThis ?
+                          this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
+          };
+
+      FNOP.prototype = this.prototype;
+      fBound.prototype = new FNOP();
+
+      return fBound;
+    };
+  }
+
+  if(!Array.isArray) {
+    Array.isArray = function (vArg) {
+      return Object.prototype.toString.call(vArg) === '[object Array]';
+    };
+  }
+
+  if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (searchElement, fromIndex) {
+      var i,
+          pivot = (fromIndex) ? fromIndex : 0,
+          length;
+
+      if (!this) {
+        throw new TypeError();
+      }
+
+      length = this.length;
+
+      if (length === 0 || pivot >= length) {
+        return -1;
+      }
+
+      if (pivot < 0) {
+        pivot = length - Math.abs(pivot);
+      }
+
+      for (i = pivot; i < length; i++) {
+        if (this[i] === searchElement) {
+          return i;
+        }
+      }
+      return -1;
+    };
+  }
+
+  if (!Array.prototype.map)
+  {
+    Array.prototype.map = function(fun /*, thisArg */)
+    {
+      'use strict';
+
+      if (this === void 0 || this === null)
+        throw new TypeError();
+
+      var t = Object(this);
+      var len = t.length >>> 0;
+      if (typeof fun !== 'function') {
+        throw new TypeError();
+      }
+
+      var res = new Array(len);
+      var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
+      for (var i = 0; i < len; i++)
+      {
+        // NOTE: Absolute correctness would demand Object.defineProperty
+        //       be used.  But this method is fairly new, and failure is
+        //       possible only if Object.prototype or Array.prototype
+        //       has a property |i| (very unlikely), so use a less-correct
+        //       but more portable alternative.
+        if (i in t)
+          res[i] = fun.call(thisArg, t[i], i, t);
+      }
+
+      return res;
+    };
+  }
+};
+// tb_require('./header.js')
+// tb_require('./shims.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global OT:true, TBPlugin:true, pluginInfo:true, debug:true, scope:true,
+          _document:true */
+/* exported createMediaCaptureController:true, createPeerController:true,
+            injectObject:true, plugins:true, mediaCaptureObject:true,
+            removeAllObjects:true, curryCallAsync:true */
+
+var objectTimeouts = {},
+    mediaCaptureObject,
+    plugins = {};
+
+var curryCallAsync = function curryCallAsync (fn) {
+  return function() {
+    var args = Array.prototype.slice.call(arguments);
+    args.unshift(fn);
+    OT.$.callAsync.apply(OT.$, args);
+  };
+};
+
+var generatePluginUuid = function generatePluginUuid () {
+  return OT.$.uuid().replace(/\-+/g, '');
+};
+
+
+var clearObjectLoadTimeout = function clearObjectLoadTimeout (callbackId) {
+  if (!callbackId) return;
+
+  if (objectTimeouts[callbackId]) {
+    clearTimeout(objectTimeouts[callbackId]);
+    delete objectTimeouts[callbackId];
+  }
+
+  if (scope[callbackId]) {
+    try {
+      delete scope[callbackId];
+    } catch (err) {
+      scope[callbackId] = void 0;
+    }
+  }
+};
+
+var removeObjectFromDom = function removeObjectFromDom (object) {
+  clearObjectLoadTimeout(object.getAttribute('tb_callbackId'));
+
+  if (mediaCaptureObject && mediaCaptureObject.id === object.id) {
+    mediaCaptureObject = null;
+  }
+  else if (plugins.hasOwnProperty(object.id)) {
+    delete plugins[object.id];
+  }
+
+  object.parentNode.removeChild(object);
+};
+
+// @todo bind destroy to unload, may need to coordinate with TB
+// jshint -W098
+var removeAllObjects = function removeAllObjects () {
+  if (mediaCaptureObject) mediaCaptureObject.destroy();
+
+  for (var id in plugins) {
+    if (plugins.hasOwnProperty(id)) {
+      plugins[id].destroy();
+    }
+  }
+};
+
+// Reference counted wrapper for a plugin object
+var PluginObject = function PluginObject (plugin) {
+  var _plugin = plugin,
+      _liveObjects = [];
+
+  this._ = _plugin;
+
+  this.addRef = function(ref) {
+    _liveObjects.push(ref);
+    return this;
+  };
+
+  this.removeRef = function(ref) {
+    if (_liveObjects.length === 0) return;
+
+    var index = _liveObjects.indexOf(ref);
+    if (index !== -1) {
+      _liveObjects.splice(index, 1);
+    }
+
+    if (_liveObjects.length === 0) {
+      this.destroy();
+    }
+
+    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);
+    return this;
+  };
+
+  this.onReady = function(readyCallback) {
+    // 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);
+    }
+  };
+
+  this.destroy = function() {
+    while (_liveObjects.length) {
+      _liveObjects.shift().destroy();
+    }
+
+    removeObjectFromDom(_plugin);
+    _plugin = null;
+  };
+
+  this.setStream = function(stream, completion) {
+    if (completion) {
+      if (stream.hasVideo()) {
+        this.once('renderingStarted', completion);
+      }
+      else {
+        // TODO Investigate whether there is a good way to detect
+        // when the audio is ready. Does it even matter?
+        completion();
+      }
+    }
+    _plugin.setStream(stream);
+  };
+};
+
+// Stops and cleans up after the plugin object load timeout.
+var injectObject = function injectObject (mimeType, isVisible, params, completion) {
+  var callbackId = 'TBPlugin_loaded_' + generatePluginUuid();
+  params.onload = callbackId;
+  params.userAgent = window.navigator.userAgent.toLowerCase();
+
+  scope[callbackId] = function() {
+    clearObjectLoadTimeout(callbackId);
+
+    o.setAttribute('id', 'tb_plugin_' + o.uuid);
+    o.removeAttribute('tb_callbackId');
+
+    pluginRefCounted.uuid = o.uuid;
+    pluginRefCounted.id = o.id;
+
+    pluginRefCounted.onReady(function(err) {
+      if (err) {
+        OT.error('Error while starting up plugin ' + o.uuid + ': ' + err);
+        return;
+      }
+
+      debug('Plugin ' + o.id + ' is loaded');
+
+      if (completion && OT.$.isFunction(completion)) {
+        completion.call(TBPlugin, null, pluginRefCounted);
+      }
+    });
+  };
+
+  var tmpContainer = document.createElement('div'),
+      objBits = [],
+      extraAttributes = ['width="0" height="0"'],
+      pluginRefCounted,
+      o;
+
+  if (isVisible !== true) {
+    extraAttributes.push('visibility="hidden"');
+  }
+
+  objBits.push('<object type="' + mimeType + '" ' + extraAttributes.join(' ') + '>');
+
+  for (var name in params) {
+    if (params.hasOwnProperty(name)) {
+      objBits.push('<param name="' + name + '" value="' + params[name] + '" />');
+    }
+  }
+
+  objBits.push('</object>');
+  tmpContainer.innerHTML = objBits.join('');
+
+  _document.body.appendChild(tmpContainer);
+
+  function firstElementChild(element) {
+    if(element.firstElementChild) {
+      return element.firstElementChild;
+    }
+    for(var i = 0, len = element.childNodes.length; i < len; ++i) {
+      if(element.childNodes[i].nodeType === 1) {
+        return element.childNodes[i];
+      }
+    }
+    return null;
+  }
+
+  o = firstElementChild(tmpContainer);
+  o.setAttribute('tb_callbackId', callbackId);
+
+  pluginRefCounted = new PluginObject(o);
+
+  _document.body.appendChild(o);
+  _document.body.removeChild(tmpContainer);
+
+  objectTimeouts[callbackId] = setTimeout(function() {
+    clearObjectLoadTimeout(callbackId);
+
+    completion.call(TBPlugin, 'The object with the mimeType of ' +
+                                mimeType + ' timed out while loading.');
+
+    _document.body.removeChild(o);
+  }, 3000);
+
+  return pluginRefCounted;
+};
+
+
+// Creates the Media Capture controller. This exposes selectSources and is
+// used in the private API.
+//
+// Only one Media Capture controller can exist at once, calling this method
+// more than once will raise an exception.
+//
+var createMediaCaptureController = function createMediaCaptureController (completion) {
+  if (mediaCaptureObject) {
+    throw new Error('TBPlugin.createMediaCaptureController called multiple times!');
+  }
+
+  mediaCaptureObject = injectObject(pluginInfo.mimeType, false, {windowless: false}, completion);
+
+  mediaCaptureObject.selectSources = function() {
+    return this._.selectSources.apply(this._, arguments);
+  };
+
+  return mediaCaptureObject;
+};
+
+// Create an instance of the publisher/subscriber/peerconnection object.
+// Many of these can exist at once, but the +id+ of each must be unique
+// within a single instance of scope (window or window-like thing).
+//
+var createPeerController = function createPeerController (completion) {
+  var o = injectObject(pluginInfo.mimeType, true, {windowless: true}, function(err, plugin) {
+    if (err) {
+      completion.call(TBPlugin, err);
+      return;
+    }
+
+    plugins[plugin.id] = plugin;
+    completion.call(TBPlugin, null, plugin);
+  });
+
+  return o;
+};
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./plugin_object.js')
+
+/* 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() {
+
+  var autoUpdaterController,
+      updaterMimeType,        // <- cached version, use getInstallerMimeType instead
+      installedVersion = -1;  // <- cached version, use getInstallerMimeType instead
+
+
+  var versionGreaterThan = function versionGreaterThan (version1,version2) {
+    if (version1 === version2) return false;
+
+    var v1 = version1.split('.'),
+        v2 = version2.split('.');
+
+    v1 = parseFloat(parseInt(v1.shift(), 10) + '.' +
+                      v1.map(function(vcomp) { return parseInt(vcomp, 10); }).join(''));
+
+    v2 = parseFloat(parseInt(v2.shift(), 10) + '.' +
+                      v2.map(function(vcomp) { return parseInt(vcomp, 10); }).join(''));
+
+
+    return v1 > v2;
+  };
+
+
+  // Work out the full mimeType (including the currently installed version)
+  // of the installer.
+  var findMimeTypeAndVersion = function findMimeTypeAndVersion () {
+
+    if (updaterMimeType !== void 0) {
+      return updaterMimeType;
+    }
+
+    var activeXControlId = 'TokBox.otiePluginInstaller',
+        unversionedMimeType = 'application/x-otieplugininstaller',
+        plugin = navigator.plugins[activeXControlId];
+
+    installedVersion = -1;
+
+
+    if (plugin) {
+      // Look through the supported mime-types for the version
+      // There should only be one mime-type in our use case, and
+      // if there's more than one they should all have the same
+      // version.
+      var numMimeTypes = plugin.length,
+          extractVersion = new RegExp(unversionedMimeType.replace('-', '\\-') +
+                                                            ',version=([0-9]+)', 'i'),
+          mimeType,
+          bits;
+
+      for (var i=0; i<numMimeTypes; ++i) {
+        mimeType = plugin[i];
+
+        // Look through the supported mimeTypes and find
+        // the newest one.
+        if (mimeType && mimeType.enabledPlugin &&
+            (mimeType.enabledPlugin.name === plugin.name) &&
+            mimeType.type.indexOf(unversionedMimeType) !== -1) {
+
+          bits = extractVersion.exec(mimeType.type);
+
+          if (bits !== null && versionGreaterThan(bits[1], installedVersion)) {
+            installedVersion = bits[1];
+          }
+        }
+      }
+    }
+    else {
+      // This may mean that the installer plugin is not installed.
+      // Although it could also mean that we're on IE 9 and below,
+      // which does not support navigator.plugins. Fallback to
+      // using 'ActiveXObject' instead.
+      try {
+        plugin = new ActiveXObject(activeXControlId);
+        installedVersion = plugin.getMasterVersion();
+      } catch(e) {
+      }
+    }
+
+    updaterMimeType = installedVersion !== -1 ?
+                              unversionedMimeType + ',version=' + installedVersion :
+                              null;
+  };
+
+  var getInstallerMimeType = function getInstallerMimeType () {
+    if (updaterMimeType === void 0) {
+      findMimeTypeAndVersion();
+    }
+
+    return updaterMimeType;
+  };
+
+  var getInstalledVersion = function getInstalledVersion () {
+    if (installedVersion === void 0) {
+      findMimeTypeAndVersion();
+    }
+
+    return installedVersion;
+  };
+
+  // Version 0.4.0.4 autoupdate was broken. We want to prompt
+  // for install on 0.4.0.4 or earlier. We're also including
+  // earlier versions just in case...
+  var hasBrokenUpdater = function () {
+    var _broken = !versionGreaterThan(getInstalledVersion(), '0.4.0.4');
+
+    hasBrokenUpdater = function() { return _broken; };
+    return _broken;
+  };
+
+
+  AutoUpdater = function (plugin) {
+
+    // Returns true if the version of the plugin installed on this computer
+    // does not match the one expected by this version of TBPlugin.
+    this.isOutOfDate = function () {
+      return versionGreaterThan(pluginInfo.version, getInstalledVersion());
+    };
+
+    this.autoUpdate = function () {
+      var modal = OT.Dialogs.Plugin.updateInProgress(),
+          analytics = new OT.Analytics(),
+        payload = {
+          ieVersion: OT.$.browserVersion().version,
+          pluginOldVersion: TBPlugin.installedVersion(),
+          pluginNewVersion: TBPlugin.version()
+        };
+
+      var success = curryCallAsync(function() {
+            analytics.logEvent({
+              action: 'OTPluginAutoUpdate',
+              variation: 'Success',
+              partnerId: OT.APIKEY,
+              payload: JSON.stringify(payload)
+            });
+
+            plugin.destroy();
+
+            modal.close();
+            OT.Dialogs.Plugin.updateComplete().on({
+              reload: function() {
+                window.location.reload();
+              }
+            });
+          }),
+
+          error = curryCallAsync(function(errorCode, errorMessage, systemErrorCode) {
+            payload.errorCode = errorCode;
+            payload.systemErrorCode = systemErrorCode;
+
+            analytics.logEvent({
+              action: 'OTPluginAutoUpdate',
+              variation: 'Failure',
+              partnerId: OT.APIKEY,
+              payload: JSON.stringify(payload)
+            });
+
+            plugin.destroy();
+
+            modal.close();
+            var updateMessage = errorMessage + ' (' + errorCode +
+                                      '). Please restart your browser and try again.';
+
+            modal = OT.Dialogs.Plugin.updateComplete(updateMessage).on({
+              'reload': function() {
+                modal.close();
+              }
+            });
+
+            OT.error('autoUpdate failed: ' + errorMessage + ' (' + errorCode +
+                                      '). Please restart your browser and try again.');
+            // TODO log client event
+          }),
+
+          progress = curryCallAsync(function(progress) {
+            modal.setUpdateProgress(progress.toFixed());
+            // modalBody.innerHTML = 'Updating...' + progress.toFixed() + '%';
+          });
+
+      plugin._.updatePlugin(TBPlugin.pathToInstaller(), success, error, progress);
+    };
+
+    this.destroy = function() {
+      plugin.destroy();
+    };
+  };
+
+  AutoUpdater.get = function (completion) {
+    if (autoUpdaterController) {
+      completion.call(null, void 0, autoUpdaterController);
+      return;
+    }
+
+    if (!this.isinstalled()) {
+      completion.call(null, 'Plugin was not installed');
+      return;
+    }
+
+    injectObject(getInstallerMimeType(), false, {windowless: false}, function(err, plugin) {
+      if (plugin) autoUpdaterController = new AutoUpdater(plugin);
+      completion.call(null, err, autoUpdaterController);
+    });
+  };
+
+  AutoUpdater.isinstalled = function () {
+    return getInstallerMimeType() !== null && !hasBrokenUpdater();
+  };
+
+  AutoUpdater.installedVersion = function () {
+    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) {
+  this.id = options.id;
+  this.kind = options.kind;
+  this.label = options.label;
+  this.enabled = OT.$.castToBoolean(options.enabled);
+  this.streamId = mediaStreamId;
+
+  this.setEnabled = function (enabled) {
+    this.enabled = OT.$.castToBoolean(enabled);
+
+    if (this.enabled) {
+      plugin._.enableMediaStreamTrack(mediaStreamId, this.id);
+    }
+    else {
+      plugin._.disableMediaStreamTrack(mediaStreamId, this.id);
+    }
+  };
+};
+
+var MediaStream = function MediaStream (options, plugin) {
+  var audioTracks = [],
+      videoTracks = [];
+
+  this.id = options.id;
+  plugin.addRef(this);
+
+  // TODO
+  // this.ended =
+  // this.onended =
+
+  if (options.videoTracks) {
+    options.videoTracks.map(function(track) {
+      videoTracks.push( new MediaStreamTrack(options.id, track, plugin) );
+    });
+  }
+
+  if (options.audioTracks) {
+    options.audioTracks.map(function(track) {
+      audioTracks.push( new MediaStreamTrack(options.id, track, plugin) );
+    });
+  }
+
+  var hasTracksOfType = function (type) {
+    var tracks = type === 'video' ? videoTracks : audioTracks;
+
+    return OT.$.some(tracks, function(track) {
+      return track.enabled;
+    });
+  };
+
+  this.getVideoTracks = function () { return videoTracks; };
+  this.getAudioTracks = function () { return audioTracks; };
+
+  this.getTrackById = function (id) {
+    videoTracks.concat(audioTracks).forEach(function(track) {
+      if (track.id === id) return track;
+    });
+
+    return null;
+  };
+
+  this.hasVideo = function () {
+    return hasTracksOfType('video');
+  };
+
+  this.hasAudio = function () {
+    return hasTracksOfType('audio');
+  };
+
+  this.addTrack = function (/* MediaStreamTrack */) {
+    // TODO
+  };
+
+  this.removeTrack = function (/* MediaStreamTrack */) {
+    // TODO
+  };
+
+  this.stop = function() {
+    plugin._.stopMediaStream(this.id);
+    plugin.removeRef(this);
+  };
+
+  this.destroy = function() {
+    this.stop();
+  };
+
+  // Private MediaStream API
+  this._ = {
+    plugin: plugin,
+
+    // Get a VideoContainer to render the stream in.
+    render: OT.$.bind(function() {
+      return new VideoContainer(plugin, this);
+    }, this)
+  };
+};
+
+
+MediaStream.fromJson = function (json, plugin) {
+  if (!json) return null;
+  return new MediaStream( JSON.parse(json), plugin );
+};
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+
+/* global OT:true */
+/* exported PluginRumorSocket */
+
+var PluginRumorSocket = function(plugin, server) {
+  var connected = false,
+      rumorID;
+
+  try {
+    rumorID = plugin._.RumorInit(server, '');
+  }
+  catch(e) {
+    OT.error('Error creating the Rumor Socket: ', e.message);
+  }
+
+  if(!rumorID) {
+    throw new Error('Could not initialise plugin rumor connection');
+  }
+
+  var socket = {
+    open: function() {
+      connected = true;
+      plugin._.RumorOpen(rumorID);
+    },
+
+    close: function(code, reason) {
+      if (!connected) return;
+      connected = false;
+
+      plugin._.RumorClose(rumorID, code, reason);
+      plugin.removeRef(this);
+    },
+
+    destroy: function() {
+      this.close();
+    },
+
+    send: function(msg) {
+      plugin._.RumorSend(rumorID, msg.type, msg.toAddress,
+        JSON.parse(JSON.stringify(msg.headers)), msg.data);
+    },
+
+    onOpen: function(callback) {
+      plugin._.SetOnRumorOpen(rumorID, callback);
+    },
+
+    onClose: function(callback) {
+      plugin._.SetOnRumorClose(rumorID, callback);
+    },
+
+    onError: function(callback) {
+      plugin._.SetOnRumorError(rumorID, callback);
+    },
+
+    onMessage: function(callback) {
+      plugin._.SetOnRumorMessage(rumorID, callback);
+    }
+  };
+
+  plugin.addRef(socket);
+  return socket;
+
+};
+
+// 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 */
+/* exported MediaConstraints */
+
+var MediaConstraints = function(userConstraints) {
+  var constraints = OT.$.clone(userConstraints);
+
+  this.hasVideo = constraints.video !== void 0 && constraints.video !== false;
+  this.hasAudio = constraints.audio !== void 0 && constraints.audio !== false;
+
+  if (constraints.video === true) constraints.video = {};
+  if (constraints.audio === true)  constraints.audio = {};
+
+  if (this.hasVideo && !constraints.video.mandatory) {
+    constraints.video.mandatory = {};
+  }
+
+  if (this.hasAudio && !constraints.audio.mandatory) {
+    constraints.audio.mandatory = {};
+  }
+
+  this.screenSharing = this.hasVideo &&
+                ( constraints.video.mandatory.chromeMediaSource === 'screen' ||
+                  constraints.video.mandatory.chromeMediaSource === 'window' );
+
+  this.audio = constraints.audio;
+  this.video = constraints.video;
+
+  this.setVideoSource = function(sourceId) {
+    if (sourceId !== void 0) constraints.video.mandatory.sourceId =  sourceId;
+    else delete constraints.video;
+  };
+
+  this.setAudioSource = function(sourceId) {
+    if (sourceId !== void 0) constraints.audio.mandatory.sourceId =  sourceId;
+    else delete constraints.audio;
+  };
+
+  this.toHash = function() {
+    return constraints;
+  };
+};
+
+// 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 */
+/* exported RTCStatsReport */
+
+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 */
+/* exported PeerConnection */
+
+// Our RTCPeerConnection shim, it should look like a normal PeerConection
+// from the outside, but it actually delegates to our plugin.
+//
+var PeerConnection = function PeerConnection (iceServers, options, plugin) {
+  var id = OT.$.uuid(),
+      hasLocalDescription = false,
+      hasRemoteDescription = false,
+      candidates = [];
+
+  plugin.addRef(this);
+
+  var onAddIceCandidate = function onAddIceCandidate () {/* success */},
+
+      onAddIceCandidateFailed = function onAddIceCandidateFailed (err) {
+        OT.error('Failed to process candidate');
+        OT.error(err);
+      },
+
+      processPendingCandidates = function processPendingCandidates () {
+        for (var i=0; i<candidates.length; ++i) {
+          plugin._.addIceCandidate(id, candidates[i], onAddIceCandidate, onAddIceCandidateFailed);
+        }
+      },
+
+      callAsync = function callAsync (/* fn, [arg1, arg2, ..., argN] */) {
+        var args = Array.prototype.slice.call(arguments),
+            fn = args.shift();
+
+        setTimeout(function() {
+          return fn.apply(null, args);
+        }, 0);
+      }/*,
+
+      attachEvent = function attachEvent (name, callback) {
+        if (plugin._.attachEvent) {
+          plugin._.attachEvent('on'+name, callback.bind(this));
+        } else {
+          plugin._.addEventListener(name, callback.bind(this), false);
+        }
+      }.bind(this)*/;
+
+  this.createOffer = function (success, error, constraints) {
+    OT.debug('createOffer', constraints);
+    plugin._.createOffer(id, function(type, sdp) {
+      success(new TBPlugin.RTCSessionDescription({
+        type: type,
+        sdp: sdp
+      }));
+    }, error, constraints || {});
+  };
+
+  this.createAnswer = function (success, error, constraints) {
+    OT.debug('createAnswer', constraints);
+    plugin._.createAnswer(id, function(type, sdp) {
+      success(new TBPlugin.RTCSessionDescription({
+        type: type,
+        sdp: sdp
+      }));
+    }, error, constraints || {});
+  };
+
+  this.setLocalDescription = function (description, success, error) {
+    OT.debug('setLocalDescription');
+
+    plugin._.setLocalDescription(id, description, function() {
+      hasLocalDescription = true;
+
+      if (hasRemoteDescription) processPendingCandidates();
+
+      if (success) success.call(null);
+    }, error);
+  };
+
+  this.setRemoteDescription = function (description, success, error) {
+    OT.debug('setRemoteDescription');
+
+    plugin._.setRemoteDescription(id, description, function() {
+      hasRemoteDescription = true;
+
+      if (hasLocalDescription) processPendingCandidates();
+      if (success) success.call(null);
+    }, error);
+  };
+
+  this.addIceCandidate = function (candidate) {
+    OT.debug('addIceCandidate');
+
+    if (hasLocalDescription && hasRemoteDescription) {
+      plugin._.addIceCandidate(id, candidate, onAddIceCandidate, onAddIceCandidateFailed);
+    }
+    else {
+      candidates.push(candidate);
+    }
+  };
+
+  this.addStream = function (stream) {
+    var constraints = {};
+    plugin._.addStream(id, stream, constraints);
+  };
+
+  this.removeStream = function (stream) {
+    plugin._.removeStream(id, stream);
+  };
+
+  this.getRemoteStreams = function () {
+    return plugin._.getRemoteStreams(id).map(function(stream) {
+      return MediaStream.fromJson(stream, plugin);
+    });
+  };
+
+  this.getLocalStreams = function () {
+    return plugin._.getLocalStreams(id).map(function(stream) {
+      return MediaStream.fromJson(stream, plugin);
+    });
+  };
+
+  this.getStreamById = function (streamId) {
+    return MediaStream.fromJson(plugin._.getStreamById(id, streamId), plugin);
+  };
+
+  this.getStats = function (mediaStreamTrack, success, error) {
+    plugin._.getStats(id, mediaStreamTrack || null, function(statsReportJson) {
+      var report = new RTCStatsReport(JSON.parse(statsReportJson));
+      callAsync(success, report);
+    }, error);
+  };
+
+  this.close = function () {
+    plugin._.destroyPeerConnection(id);
+    plugin.removeRef(this);
+  };
+
+  this.destroy = function () {
+    this.close();
+  };
+
+  // I want these to appear to be null, instead of undefined, if no
+  // callbacks are assigned. This more closely matches how the native
+  // objects appear and allows 'if (pc.onsignalingstatechange)' type
+  // feature detection to work.
+  this.onaddstream = null;
+  this.onremovestream = null;
+  this.onicecandidate = null;
+  this.onsignalingstatechange = null;
+  this.oniceconnectionstatechange = null;
+
+  // Both username and credential must exist, otherwise the plugin throws an error
+  OT.$.forEach(iceServers.iceServers, function(iceServer) {
+    if (!iceServer.username) iceServer.username = '';
+    if (!iceServer.credential) iceServer.credential = '';
+  });
+
+  if (!plugin._.initPeerConnection(id, iceServers, options)) {
+    OT.error('Failed to initialise PeerConnection');
+    // TODO: something sensible here
+    return;
+  }
+
+  plugin._.on(id, {
+    addStream: function(streamJson) {
+      setTimeout(function() {
+        if (this.onaddstream && OT.$.isFunction(this.onaddstream)) {
+          var stream = MediaStream.fromJson(streamJson, plugin);
+          callAsync(this.onaddstream, {stream: stream});
+        }
+      }.bind(this), 3000);
+    }.bind(this),
+
+    removeStream: function(streamJson) {
+      if (this.onremovestream && OT.$.isFunction(this.onremovestream)) {
+        var stream = MediaStream.fromJson(streamJson, plugin);
+        callAsync(this.onremovestream, {stream: stream});
+      }
+    }.bind(this),
+
+    iceCandidate: function(candidateSdp, sdpMid, sdpMLineIndex) {
+      if (this.onicecandidate && OT.$.isFunction(this.onicecandidate)) {
+
+        var candidate = new TBPlugin.RTCIceCandidate({
+          candidate: candidateSdp,
+          sdpMid: sdpMid,
+          sdpMLineIndex: sdpMLineIndex
+        });
+
+        callAsync(this.onicecandidate, {candidate: candidate});
+      }
+    }.bind(this),
+
+    signalingStateChange: function(state) {
+      if (this.onsignalingstatechange && OT.$.isFunction(this.onsignalingstatechange)) {
+        callAsync(this.onsignalingstatechange, state);
+      }
+    }.bind(this),
+
+    iceConnectionChange: function(state) {
+      if (this.oniceconnectionstatechange && OT.$.isFunction(this.oniceconnectionstatechange)) {
+        callAsync(this.oniceconnectionstatechange, state);
+      }
+    }.bind(this)
+  });
+};
+
+
+
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./plugin_object.js')
+// tb_require('./auto_updater.js')
+// tb_require('./media_constraints.js')
+// tb_require('./peer_connection.js')
+// tb_require('./media_stream.js')
+// tb_require('./video_container.js')
+// tb_require('./rumor.js')
+
+/* jshint globalstrict: true, strict: false, undef: true,
+          unused: true, trailing: true, browser: true, smarttabs:true */
+/* global ActiveXObject, OT, TBPlugin, scope, shim,
+          shimMutationObservers, PeerConnection, VideoContainer,
+          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'
+  },
+  _document = scope.document,
+  readyCallbacks = [];
+
+var debug = function (message, object) {
+  if (object) {
+    scope.OT.info('TB Plugin - ' + message + ' => ', object);
+  }
+  else {
+    scope.OT.info('TB Plugin - ' + message);
+  }
+};
+
+
+/// Private API
+
+var isDomReady = function isDomReady () {
+      return (_document.readyState === 'complete' ||
+             (_document.readyState === 'interactive' && _document.body));
+    },
+
+    onDomReady = function onDomReady () {
+      var callCompletionHandlers = function(err) {
+        var callback;
+
+        while ( (callback = readyCallbacks.pop()) && OT.$.isFunction(callback) ) {
+          callback.call(TBPlugin, err);
+        }
+      };
+
+      AutoUpdater.get(function(err, updater) {
+        if (err) {
+          OT.error('Error while loading the AutoUpdater: ' + err);
+          callCompletionHandlers('Error while loading the AutoUpdater: ' + err);
+          return;
+        }
+
+        // If the plugin is out of date then we kick off the
+        // auto update process and then bail out.
+        if (updater.isOutOfDate()) {
+          updater.autoUpdate();
+          return;
+        }
+
+        // Inject the controller object into the page, wait for it to load or timeout...
+        createMediaCaptureController(function(err) {
+          if (!err && (mediaCaptureObject && !mediaCaptureObject.isValid())) {
+            err = 'The TB Plugin failed to load properly';
+          }
+
+          pluginReady = true;
+          callCompletionHandlers(err);
+
+          OT.onUnload(destroy);
+        });
+      });
+    },
+
+    waitForDomReady = function waitForDomReady () {
+      if (isDomReady()) {
+        onDomReady();
+      }
+      else if (_document.addEventListener) {
+        _document.addEventListener('DOMContentLoaded', onDomReady, false);
+      } else if (_document.attachEvent) {
+        _document.attachEvent('onreadystatechange', function() {
+          if (_document.readyState === 'complete') onDomReady();
+        });
+      }
+    },
+
+    // @todo bind destroy to unload, may need to coordinate with TB
+    // jshint -W098
+    destroy = function destroy () {
+      removeAllObjects();
+    };
+
+
+/// Public API
+
+TBPlugin.isInstalled = function isInstalled () {
+  if (!this.isSupported()) return false;
+  return AutoUpdater.isinstalled();
+};
+
+TBPlugin.version = function version () {
+  return pluginInfo.version;
+};
+
+TBPlugin.installedVersion = function installedVersion () {
+  return AutoUpdater.installedVersion();
+};
+
+// Returns a URI to the TBPlugin installer that is paired with
+// this version of TBPlugin.js.
+TBPlugin.pathToInstaller = function pathToInstaller () {
+  return 'https://s3.amazonaws.com/otplugin.tokbox.com/v' +
+                    pluginInfo.version + '/otiePluginMain.msi';
+};
+
+// Trigger +callback+ when the plugin is ready
+//
+// Most of the public API cannot be called until
+// the plugin is ready.
+//
+TBPlugin.ready = function ready (callback) {
+  if (TBPlugin.isReady()) {
+    var err;
+
+    if (!mediaCaptureObject || !mediaCaptureObject.isValid()) {
+      err = 'The TB Plugin failed to load properly';
+    }
+
+    callback.call(TBPlugin, err);
+  }
+  else {
+    readyCallbacks.push(callback);
+  }
+};
+
+// Helper function for TBPlugin.getUserMedia
+var _getUserMedia = function _getUserMedia(mediaConstraints, success, error) {
+  createPeerController(function(err, plugin) {
+    if (err) {
+      error.call(TBPlugin, err);
+      return;
+    }
+
+    plugin._.getUserMedia(mediaConstraints.toHash(), function(streamJson) {
+      success.call(TBPlugin, MediaStream.fromJson(streamJson, plugin));
+    }, error);
+  });
+};
+
+// Equivalent to: window.getUserMedia(constraints, success, error);
+//
+// Except that the constraints won't be identical
+TBPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) {
+  var constraints = new MediaConstraints(userConstraints);
+
+  if (constraints.screenSharing) {
+    _getUserMedia(constraints, success, error);
+  }
+  else {
+    var sources = [];
+    if (constraints.hasVideo) sources.push('video');
+    if (constraints.hasAudio) sources.push('audio');
+
+    mediaCaptureObject.selectSources(sources, function(captureDevices) {
+      for (var key in captureDevices) {
+        if (captureDevices.hasOwnProperty(key)) {
+          OT.debug(key + ' Capture Device: ' + captureDevices[key]);
+        }
+      }
+
+      // Use the sources to acquire the hardware and start rendering
+      constraints.setVideoSource(captureDevices.video);
+      constraints.setAudioSource(captureDevices.audio);
+
+      _getUserMedia(constraints, success, error);
+    }, error);
+  }
+};
+
+TBPlugin.initRumorSocket = function(messagingURL, completion) {
+  TBPlugin.ready(function(error) {
+    if(error) {
+      completion(error);
+    } else {
+      completion(null, new PluginRumorSocket(mediaCaptureObject, messagingURL));
+    }
+  });
+};
+
+
+// Equivalent to: var pc = new window.RTCPeerConnection(iceServers, options);
+//
+// Except that it is async and takes a completion handler
+TBPlugin.initPeerConnection = function initPeerConnection (iceServers,
+                                                           options,
+                                                           localStream,
+                                                           completion) {
+
+  var gotPeerObject = function(err, plugin) {
+    if (err) {
+      completion.call(TBPlugin, err);
+      return;
+    }
+
+    debug('Got PeerConnection for ' + plugin.id);
+    var peerConnection = new PeerConnection(iceServers, options, plugin);
+
+    completion.call(TBPlugin, null, peerConnection);
+  };
+
+  // @fixme this is nasty and brittle. We need some way to use the same Object
+  // for the PeerConnection that was used for the getUserMedia call (in the case
+  // of publishers). We don't really have a way of implicitly associating them though.
+  // Hence, publishers will have to pass through their localStream (if they have one)
+  // and we will look up the original Object and use that. Otherwise we generate
+  // a new one.
+  if (localStream && localStream._.plugin) {
+    gotPeerObject(null, localStream._.plugin);
+  }
+  else {
+    createPeerController(gotPeerObject);
+  }
+};
+
+// A RTCSessionDescription like object exposed for native WebRTC compatability
+TBPlugin.RTCSessionDescription = function RTCSessionDescription (options) {
+  this.type = options.type;
+  this.sdp = options.sdp;
+};
+
+// A RTCIceCandidate like object exposed for native WebRTC compatability
+TBPlugin.RTCIceCandidate = function RTCIceCandidate (options) {
+  this.sdpMid = options.sdpMid;
+  this.sdpMLineIndex = parseInt(options.sdpMLineIndex, 10);
+  this.candidate = options.candidate;
+};
+
+
+// Make this available for now
+TBPlugin.debug = debug;
+
+shim();
+
+waitForDomReady();
+
+// tb_require('./tb_plugin.js')
+/* jshint ignore:start */
+})(this);
+/* jshint ignore:end */
+
 !(function() {
 /*global OT:true */
 
   var defaultAspectRatio = 4.0/3.0,
       miniWidth = 128,
       miniHeight = 128,
       microWidth = 64,
       microHeight = 64;
 
   // This code positions the video element so that we don't get any letterboxing.
   // It will take into consideration aspect ratios other than 4/3 but only when
   // the video element is first created. If the aspect ratio changes at a later point
   // this calculation will become incorrect.
   function fixAspectRatio(element, width, height, desiredAspectRatio, rotated) {
+
+    if (TBPlugin.isInstalled()) {
+      // The plugin will sort out it's own aspect ratio, so we
+      // only need to tell the container to expand to fit it's parent.
+
+      OT.$.css(element, {
+        width: '100%',
+        height: '100%',
+        left: 0,
+        top: 0
+      });
+
+      return;
+    }
+
     if (!width) width = parseInt(OT.$.width(element.parentNode), 10);
     else width = parseInt(width, 10);
 
     if (!height) height = parseInt(OT.$.height(element.parentNode), 10);
     else height = parseInt(height, 10);
 
     if (width === 0 || height === 0) return;
 
@@ -3359,58 +5456,60 @@ OTHelpers.centerElement = function(eleme
 
     posterContainer = document.createElement('div');
     OT.$.addClass(posterContainer, 'OT_video-poster');
     videoContainer.appendChild(posterContainer);
 
     oldContainerStyles.width = container.offsetWidth;
     oldContainerStyles.height = container.offsetHeight;
 
-    // Observe changes to the width and height and update the aspect ratio
-    dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'],
-      function(changeSet) {
-      var width = changeSet.width ? changeSet.width[1] : container.offsetWidth,
-          height = changeSet.height ? changeSet.height[1] : container.offsetHeight;
-      fixMini(container, width, height);
-      fixAspectRatio(videoContainer, width, height, videoElement ?
-        videoElement.aspectRatio : null);
-    });
-
-
-    // @todo observe if the video container or the video element get removed
-    // if they do we should do some cleanup
-    videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) {
-      if (!videoElement) return;
-
-      // This assumes a video element being removed is the main video element. This may
-      // not be the case.
-      var videoRemoved = removedNodes.some(function(node) {
-        return node === videoContainer || node.nodeName === 'VIDEO';
-      });
-
-      if (videoRemoved) {
-        videoElement.destroy();
-        videoElement = null;
-      }
-
-      if (videoContainer) {
-        OT.$.removeElement(videoContainer);
-        videoContainer = null;
-      }
-
-      if (dimensionsObserver) {
-        dimensionsObserver.disconnect();
-        dimensionsObserver = null;
-      }
-
-      if (videoObserver) {
-        videoObserver.disconnect();
-        videoObserver = null;
-      }
-    });
+    if (!TBPlugin.isInstalled()) {
+      // Observe changes to the width and height and update the aspect ratio
+      dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'],
+        function(changeSet) {
+        var width = changeSet.width ? changeSet.width[1] : container.offsetWidth,
+            height = changeSet.height ? changeSet.height[1] : container.offsetHeight;
+        fixMini(container, width, height);
+        fixAspectRatio(videoContainer, width, height, videoElement ?
+          videoElement.aspectRatio() : null);
+      });
+
+
+      // @todo observe if the video container or the video element get removed
+      // if they do we should do some cleanup
+      videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) {
+        if (!videoElement) return;
+
+        // This assumes a video element being removed is the main video element. This may
+        // not be the case.
+        var videoRemoved = OT.$.some(removedNodes, function(node) {
+          return node === videoContainer || node.nodeName === 'VIDEO';
+        });
+
+        if (videoRemoved) {
+          videoElement.destroy();
+          videoElement = null;
+        }
+
+        if (videoContainer) {
+          OT.$.removeElement(videoContainer);
+          videoContainer = null;
+        }
+
+        if (dimensionsObserver) {
+          dimensionsObserver.disconnect();
+          dimensionsObserver = null;
+        }
+
+        if (videoObserver) {
+          videoObserver.disconnect();
+          videoObserver = null;
+        }
+      });
+    }
 
     this.destroy = function() {
       if (dimensionsObserver) {
         dimensionsObserver.disconnect();
         dimensionsObserver = null;
       }
 
       if (videoObserver) {
@@ -3424,24 +5523,81 @@ OTHelpers.centerElement = function(eleme
       }
 
       if (container) {
         OT.$.removeElement(container);
         container = null;
       }
     };
 
-    Object.defineProperties(this, {
-
+
+
+    this.bindVideo = function(webRTCStream, options, completion) {
+      // remove the old video element if it exists
+      // @todo this might not be safe, publishers/subscribers use this as well...
+      if (videoElement) {
+        videoElement.destroy();
+        videoElement = null;
+      }
+
+      var onError = options && options.error ? options.error : void 0;
+      delete options.error;
+
+      var video = new OT.VideoElement({ attributes: options }, onError);
+
+      // Initialize the audio volume
+      if (options.audioVolume) video.setAudioVolume(options.audioVolume);
+
+      // makes the incoming audio streams take priority (will impact only FF OS for now)
+      video.audioChannelType('telephony');
+
+      video.appendTo(videoContainer).bindToStream(webRTCStream, function(err) {
+        if (err) {
+          video.destroy();
+          completion(err);
+          return;
+        }
+
+        videoElement = video;
+
+        videoElement.on({
+          orientationChanged: function(){
+            fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight,
+              videoElement.aspectRatio(), videoElement.isRotated());
+          }
+        });
+
+        var fix = function() {
+          fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight,
+            videoElement ? videoElement.aspectRatio() : null,
+            videoElement ? videoElement.isRotated() : null);
+        };
+
+        if(isNaN(videoElement.aspectRatio())) {
+          videoElement.on('streamBound', fix);
+        } else {
+          fix();
+        }
+
+        completion(null, video);
+      });
+
+      return video;
+    };
+
+    this.video = function() { return videoElement; };
+
+
+    OT.$.defineProperties(this, {
       showPoster: {
         get: function() {
           return !OT.$.isDisplayNone(posterContainer);
         },
-        set: function(shown) {
-          if(shown) {
+        set: function(newValue) {
+          if(newValue) {
             OT.$.show(posterContainer);
           } else {
             OT.$.hide(posterContainer);
           }
         }
       },
 
       poster: {
@@ -3461,56 +5617,24 @@ OTHelpers.centerElement = function(eleme
           if (loading) {
             OT.$.addClass(container, 'OT_loading');
           } else {
             OT.$.removeClass(container, 'OT_loading');
           }
         }
       },
 
-      video: {
-        get: function() { return videoElement; },
-        set: function(video) {
-          // remove the old video element if it exists
-          // @todo this might not be safe, publishers/subscribers use this as well...
-          if (videoElement) videoElement.destroy();
-
-          video.appendTo(videoContainer);
-          videoElement = video;
-
-          videoElement.on({
-            orientationChanged: function(){
-              fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight,
-                videoElement.aspectRatio, videoElement.isRotated);
-            }
-          });
-
-          if (videoElement) {
-            var fix = function() {
-              fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight,
-                videoElement ? videoElement.aspectRatio : null,
-                videoElement ? videoElement.isRotated : null);
-            };
-            if(isNaN(videoElement.aspectRatio)) {
-              videoElement.on('streamBound', fix);
-            } else {
-              fix();
-            }
-          }
-        }
-      },
-
-      domElement: {
-        get: function() { return container; }
-      },
 
       domId: {
         get: function() { return container.getAttribute('id'); }
       }
-    });
+
+    });
+
+    this.domElement = container;
 
     this.addError = function(errorMsg, helpMsg, classNames) {
       container.innerHTML = '<p>' + errorMsg +
         (helpMsg ? ' <span class="ot-help-message">' + helpMsg + '</span>' : '') +
         '</p>';
       OT.$.addClass(container, classNames || 'OT_subscriber_error');
       if(container.querySelector('p').offsetHeight > container.offsetHeight) {
         container.querySelector('span').style.display = 'none';
@@ -3529,24 +5653,28 @@ OTHelpers.centerElement = function(eleme
       gumNamesToMessages,
       mapVendorErrorName,
       parseErrorEvent,
       areInvalidConstraints;
 
   // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth
   nativeGetUserMedia = (function() {
     if (navigator.getUserMedia) {
-      return navigator.getUserMedia.bind(navigator);
+      return OT.$.bind(navigator.getUserMedia, navigator);
     } else if (navigator.mozGetUserMedia) {
-      return navigator.mozGetUserMedia.bind(navigator);
+      return OT.$.bind(navigator.mozGetUserMedia, navigator);
     } else if (navigator.webkitGetUserMedia) {
-      return navigator.webkitGetUserMedia.bind(navigator);
+      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() {
         return this.videoTracks;
       };
@@ -3598,16 +5726,27 @@ OTHelpers.centerElement = function(eleme
     // object (a wrapped native object I think).
     // if (!window.mozRTCPeerConnection.prototype.getRemoteStreams) {
     //     window.mozRTCPeerConnection.prototype.getRemoteStreams = function() {
     //         return this.remoteStreams;
     //     };
     // }
   }
 
+  // The setEnabled method on MediaStreamTracks is a TBPlugin
+  // construct. In this particular instance it's easier to bring
+  // all the good browsers down to IE's level than bootstrap it up.
+  if (typeof window.MediaStreamTrack !== 'undefined') {
+    if (!window.MediaStreamTrack.prototype.setEnabled) {
+      window.MediaStreamTrack.prototype.setEnabled = function (enabled) {
+        this.enabled = OT.$.castToBoolean(enabled);
+      };
+    }
+  }
+
 
   // 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',
@@ -3718,16 +5857,18 @@ OTHelpers.centerElement = function(eleme
       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;
   };
@@ -3747,36 +5888,38 @@ OTHelpers.centerElement = function(eleme
     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';
+    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 bundle
+  // * Chrome support rtcp mux
+  // * OT Plugin supports rtcp mux
   //
   OT.$.supportsRtcpMux = function() {
-    return OT.$.supportsWebRTC() && OT.$.browser() === 'Chrome';
+    return OT.$.supportsWebRTC() && (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled());
   };
 
   OT.$.shouldAskForDevices = function(callback) {
     var memoiseReply = function(audio, video) {
       OT.$.shouldAskForDevices = function(callback) {
-        setTimeout(callback.bind(null, { video: video, audio: audio }));
+        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';
@@ -3817,16 +5960,45 @@ OTHelpers.centerElement = function(eleme
   // @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;
     }
@@ -3903,317 +6075,559 @@ OTHelpers.centerElement = function(eleme
       // accessDialogOpened event.
       triggerOpenedTimer = setTimeout(triggerOpened, 100);
 
     } else {
       // wait a second and then trigger accessDialogOpened
       triggerOpenedTimer = setTimeout(triggerOpened, 500);
     }
   };
-  
-  OT.$.createPeerConnection = function (config, options) {
-    var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection);
-    return new NativeRTCPeerConnection(config, options);
-  };
+
+  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);
+    }
+  };
+
 
 })(window);
-!(function(window) {
-
-  var _videoErrorCodes = {},
-      VideoOrientationTransforms;
-
-  VideoOrientationTransforms = {
+(function(window) {
+
+  var VideoOrientationTransforms = {
     0: 'rotate(0deg)',
     270: 'rotate(90deg)',
     90: 'rotate(-90deg)',
     180: 'rotate(180deg)'
   };
 
   OT.VideoOrientation = {
     ROTATED_NORMAL: 0,
     ROTATED_LEFT: 270,
     ROTATED_RIGHT: 90,
     ROTATED_UPSIDE_DOWN: 180
   };
 
+  var DefaultAudioVolume = 50;
+
+  var DEGREE_TO_RADIANS = Math.PI * 2 / 360;
+
+  //
+  //
   //   var _videoElement = new OT.VideoElement({
   //     fallbackText: 'blah'
-  //   });
-  //
-  //   _videoElement.on({
-  //     streamBound: function() {...},
-  //     loadError: function() {...},
-  //     error: function() {...}
-  //   });
-  //
-  //   _videoElement.bindToStream(webRtcStream);      // => VideoElement
-  //   _videoElement.appendTo(DOMElement)             // => VideoElement
-  //
-  //   _videoElement.stream                           // => Web RTC stream
+  //   }, errorHandler);
+  //
+  //   _videoElement.bindToStream(webRtcStream, completion);      // => VideoElement
+  //   _videoElement.appendTo(DOMElement)                         // => VideoElement
+  //
   //   _videoElement.domElement                       // => DomNode
-  //   _videoElement.parentElement                    // => DomNode
   //
   //   _videoElement.imgData                          // => PNG Data string
   //
   //   _videoElement.orientation = OT.VideoOrientation.ROTATED_LEFT;
   //
   //   _videoElement.unbindStream();
   //   _videoElement.destroy()                        // => Completely cleans up and
-  //                                                  // removes the video element
-  //
-  //
-  OT.VideoElement = function(options) {
-    var _stream,
-        _domElement,
-        _parentElement,
-        _streamBound = false,
-        _videoElementMovedWarning = false,
-        _options,
-        _onVideoError,
-        _onStreamBound,
-        _onStreamBoundError,
-        _playVideoOnPause;
-
-    _options = OT.$.defaults(options || {}, {
-      fallbackText: 'Sorry, Web RTC is not available in your browser'
-    });
+  //                                                        removes the video element
+  //
+  //
+  OT.VideoElement = function(/* optional */ options/*, optional errorHandler*/) {
+    var _options = OT.$.defaults( options && !OT.$.isFunction(options) ? options : {}, {
+        fallbackText: 'Sorry, Web RTC is not available in your browser'
+      }),
+
+      errorHandler = OT.$.isFunction(arguments[arguments.length-1]) ?
+                                    arguments[arguments.length-1] : void 0,
+
+      orientationHandler = OT.$.bind(function(orientation) {
+        this.trigger('orientationChanged', orientation);
+      }, this),
+
+      _videoElement = TBPlugin.isInstalled() ?
+                            new PluginVideoElement(_options, errorHandler, orientationHandler) :
+                            new NativeDOMVideoElement(_options, errorHandler, orientationHandler),
+      _streamBound = false,
+      _stream,
+      _preInitialisedVolue;
 
     OT.$.eventing(this);
 
-    /// Private API
-    _onVideoError = function(event) {
-      var reason = 'There was an unexpected problem with the Video Stream: ' +
-        videoElementErrorCodeToStr(event.target.error.code);
-      this.trigger('error', null, reason, this, 'VideoElement');
-    }.bind(this);
-
-    _onStreamBound = function() {
-      _streamBound = true;
-      _domElement.addEventListener('error', _onVideoError, false);
-      this.trigger('streamBound', this);
-    }.bind(this);
-
-    _onStreamBoundError = function(reason) {
-      this.trigger('loadError', OT.ExceptionCodes.P2P_CONNECTION_FAILED, reason, this,
-        'VideoElement');
-    }.bind(this);
-
-    // The video element pauses itself when it's reparented, this is
-    // unfortunate. This function plays the video again and is triggered
-    // on the pause event.
-    _playVideoOnPause = function() {
-      if(!_videoElementMovedWarning) {
-        OT.warn('Video element paused, auto-resuming. If you intended to do this, use ' +
-          'publishVideo(false) or subscribeToVideo(false) instead.');
-        _videoElementMovedWarning = true;
-      }
-      _domElement.play();
-    };
-
-
-    _domElement = createVideoElement(_options.fallbackText, _options.attributes);
-
-    _domElement.addEventListener('pause', _playVideoOnPause);
-
-    /// Public Properties
-    Object.defineProperties(this, {
-      stream: {
-        get: function() {return _stream; }
+    // Public Properties
+    OT.$.defineProperties(this, {
+
+      domElement: {
+        get: function() {
+          return _videoElement.domElement();
+        }
+      },
+
+      videoWidth: {
+        get: function() {
+          return _videoElement['video' + (this.isRotated() ? 'Height' : 'Width')]();
+        }
+      },
+
+      videoHeight: {
+        get: function() {
+          return _videoElement['video' + (this.isRotated() ? 'Width' : 'Height')]();
+        }
+      },
+
+      aspectRatio: {
+        get: function() {
+          return (this.videoWidth() + 0.0) / this.videoHeight();
+        }
+      },
+
+      isRotated: {
+        get: function() {
+          return _videoElement.isRotated();
+        }
+      },
+
+      orientation: {
+        get: function() {
+          return _videoElement.orientation();
+        },
+        set: function(orientation) {
+          _videoElement.orientation(orientation);
+        }
       },
-      domElement: {
-        get: function() {return _domElement; }
-      },
-      parentElement: {
-        get: function() {return _parentElement; }
-      },
-      isBoundToStream: {
-        get: function() { return _streamBound; }
-      },
-      poster: {
+
+      audioChannelType: {
         get: function() {
-          return _domElement.getAttribute('poster');
-        },
-        set: function(src) {
-          _domElement.setAttribute('poster', src);
-        }
-      }
-    });
-
+          return _videoElement.audioChannelType();
+        },
+        set: function(type) {
+          _videoElement.audioChannelType(type);
+        }
+      }
+    });
+
+    // Public Methods
+
+    this.imgData = function() {
+      return _videoElement.imgData();
+    };
+
+    this.appendTo = function(parentDomElement) {
+      _videoElement.appendTo(parentDomElement);
+      return this;
+    };
+
+    this.bindToStream = function(webRtcStream, completion) {
+      _streamBound = false;
+      _stream = webRtcStream;
+
+      _videoElement.bindToStream(webRtcStream, OT.$.bind(function(err) {
+        if (err) {
+          completion(err);
+          return;
+        }
+
+        _streamBound = true;
+
+        if (_preInitialisedVolue) {
+          this.setAudioVolume(_preInitialisedVolue);
+          _preInitialisedVolue = null;
+        }
+
+        completion(null);
+      }, this));
+
+      return this;
+    };
+
+    this.unbindStream = function() {
+      if (!_stream) return this;
+
+      _stream = null;
+      _videoElement.unbindStream();
+      return this;
+    };
+
+    this.setAudioVolume = function (value) {
+      if (_streamBound) _videoElement.setAudioVolume( OT.$.roundFloat(value / 100, 2) );
+      else _preInitialisedVolue = value;
+
+      return this;
+    };
+
+    this.getAudioVolume = function () {
+      if (_streamBound) return parseInt(_videoElement.getAudioVolume() * 100, 10);
+      else return _preInitialisedVolue || 50;
+    };
+
+
+    this.whenTimeIncrements = function (callback, context) {
+      _videoElement.whenTimeIncrements(callback, context);
+      return this;
+    };
+
+    this.destroy = function () {
+      // unbind all events so they don't fire after the object is dead
+      this.off();
+
+      _videoElement.destroy();
+      return void 0;
+    };
+  };
+
+  var PluginVideoElement = function PluginVideoElement (options,
+                                                        errorHandler,
+                                                        orientationChangedHandler) {
+    var _videoProxy,
+        _parentDomElement;
+
+    canBeOrientatedMixin(this,
+                          function() { return _videoProxy.domElement; },
+                          orientationChangedHandler);
 
     /// Public methods
 
+    this.domElement = function() {
+      return _videoProxy ? _videoProxy.domElement : void 0;
+    };
+
+    this.videoWidth = function() {
+      return _videoProxy ? _videoProxy.getVideoWidth() : void 0;
+    };
+
+    this.videoHeight = function() {
+      return _videoProxy ? _videoProxy.getVideoHeight() : void 0;
+    };
+
+    this.imgData = function() {
+      return _videoProxy ? _videoProxy.getImgData() : null;
+    };
+
     // Append the Video DOM element to a parent node
     this.appendTo = function(parentDomElement) {
-      _parentElement = parentDomElement;
-      _parentElement.appendChild(_domElement);
-
+      _parentDomElement = parentDomElement;
       return this;
     };
 
     // Bind a stream to the video element.
-    this.bindToStream = function(webRtcStream) {
-      _streamBound = false;
-      _stream = webRtcStream;
-
-      bindStreamToVideoElement(_domElement, _stream, _onStreamBound, _onStreamBoundError);
+    this.bindToStream = function(webRtcStream, completion) {
+      if (!_parentDomElement) {
+        completion('The VideoElement must attached to a DOM node before a stream can be bound');
+        return;
+      }
+
+      _videoProxy = webRtcStream._.render();
+      _videoProxy.appendTo(_parentDomElement);
+      _videoProxy.show(completion);
 
       return this;
     };
 
     // Unbind the currently bound stream from the video element.
     this.unbindStream = function() {
-      if (!_stream) return this;
-
-      if (_domElement) {
-        if (!navigator.mozGetUserMedia) {
-          // The browser would have released this on unload anyway, but
-          // we're being a good citizen.
-          window.URL.revokeObjectURL(_domElement.src);
-        } else {
-          _domElement.mozSrcObject = null;
-        }
-      }
-
-      _stream = null;
+      // TODO: some way to tell TBPlugin to release that stream and controller
+
+      if (_videoProxy) {
+        _videoProxy.destroy();
+        _parentDomElement = null;
+        _videoProxy = null;
+      }
 
       return this;
     };
 
     this.setAudioVolume = function(value) {
-      if (_domElement) _domElement.volume = OT.$.roundFloat(value / 100, 2);
+      if (_videoProxy) _videoProxy.setVolume(value);
     };
 
     this.getAudioVolume = function() {
       // Return the actual volume of the DOM element
-      if (_domElement) return parseInt(_domElement.volume * 100, 10);
-      return 50;
+      if (_videoProxy) return _videoProxy.getVolume();
+      return DefaultAudioVolume;
+    };
+
+    // see https://wiki.mozilla.org/WebAPI/AudioChannels
+    // The audioChannelType is not currently supported in the plugin.
+    this.audioChannelType = function(/* type */) {
+      return 'unknown';
+    };
+
+    this.whenTimeIncrements = function(callback, context) {
+      // exists for compatibility with NativeVideoElement
+      OT.$.callAsync(OT.$.bind(callback, context));
+    };
+
+    this.destroy = function() {
+      this.unbindStream();
+
+      return void 0;
+    };
+  };
+
+
+  var NativeDOMVideoElement = function NativeDOMVideoElement (options,
+                                                              errorHandler,
+                                                              orientationChangedHandler) {
+    var _domElement,
+        _videoElementMovedWarning = false;
+
+
+    /// Private API
+    var _onVideoError = OT.$.bind(function(event) {
+          var reason = 'There was an unexpected problem with the Video Stream: ' +
+                        videoElementErrorCodeToStr(event.target.error.code);
+
+          errorHandler.call(null, null, reason, this, 'VideoElement');
+        }, this),
+
+        // The video element pauses itself when it's reparented, this is
+        // unfortunate. This function plays the video again and is triggered
+        // on the pause event.
+        _playVideoOnPause = function() {
+          if(!_videoElementMovedWarning) {
+            OT.warn('Video element paused, auto-resuming. If you intended to do this, ' +
+                      'use publishVideo(false) or subscribeToVideo(false) instead.');
+
+            _videoElementMovedWarning = true;
+          }
+
+          _domElement.play();
+        };
+
+
+    _domElement = createNativeVideoElement(options.fallbackText, options.attributes);
+
+    _domElement.addEventListener('pause', _playVideoOnPause);
+
+    canBeOrientatedMixin(this, function() { return _domElement; }, orientationChangedHandler);
+
+    /// Public methods
+
+    this.domElement = function() {
+      return _domElement;
+    };
+
+    this.videoWidth = function() {
+      return _domElement.videoWidth;
+    };
+
+    this.videoHeight = function() {
+      return _domElement.videoHeight;
+    };
+
+    this.imgData = function() {
+      var canvas = OT.$.createElement('canvas', {
+        width: _domElement.videoWidth,
+        height: _domElement.videoHeight,
+        style: { display: 'none' }
+      });
+
+      document.body.appendChild(canvas);
+      try {
+        canvas.getContext('2d').drawImage(_domElement, 0, 0, canvas.width, canvas.height);
+      } catch(err) {
+        OT.warn('Cannot get image data yet');
+        return null;
+      }
+      var imgData = canvas.toDataURL('image/png');
+
+      OT.$.removeElement(canvas);
+
+      return OT.$.trim(imgData.replace('data:image/png;base64,', ''));
+    };
+
+    // Append the Video DOM element to a parent node
+    this.appendTo = function(parentDomElement) {
+      parentDomElement.appendChild(_domElement);
+      return this;
+    };
+
+    // Bind a stream to the video element.
+    this.bindToStream = function(webRtcStream, completion) {
+      bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) {
+        if (err) {
+          completion(err);
+          return;
+        }
+
+        _domElement.addEventListener('error', _onVideoError, false);
+        completion(null);
+      });
+
+      return this;
+    };
+
+
+    // Unbind the currently bound stream from the video element.
+    this.unbindStream = function() {
+      if (_domElement) {
+        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;
+        }
+      }
+
+      return this;
+    };
+
+    this.setAudioVolume = function(value) {
+      if (_domElement) _domElement.volume = value;
+    };
+
+    this.getAudioVolume = function() {
+      // Return the actual volume of the DOM element
+      if (_domElement) return _domElement.volume;
+      return DefaultAudioVolume;
+    };
+
+    // see https://wiki.mozilla.org/WebAPI/AudioChannels
+    // The audioChannelType is currently only available in Firefox. This property returns
+    // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel"
+    this.audioChannelType = function(type) {
+      if (type !== void 0) {
+        _domElement.mozAudioChannelType = type;
+      }
+
+      if ('mozAudioChannelType' in _domElement) {
+        return _domElement.mozAudioChannelType;
+      } else {
+        return 'unknown';
+      }
     };
 
     this.whenTimeIncrements = function(callback, context) {
       if(_domElement) {
         var lastTime, handler;
-        handler = function() {
+        handler = OT.$.bind(function() {
           if(!lastTime || lastTime >= _domElement.currentTime) {
             lastTime = _domElement.currentTime;
           } else {
             _domElement.removeEventListener('timeupdate', handler, false);
             callback.call(context, this);
           }
-        }.bind(this);
+        }, this);
         _domElement.addEventListener('timeupdate', handler, false);
       }
     };
 
     this.destroy = function() {
-      // unbind all events so they don't fire after the object is dead
-      this.off();
-
       this.unbindStream();
 
       if (_domElement) {
         // Unbind this first, otherwise it will trigger when the
         // video element is removed from the DOM.
         _domElement.removeEventListener('pause', _playVideoOnPause);
 
         OT.$.removeElement(_domElement);
         _domElement = null;
       }
 
-      _parentElement = null;
-
-      return undefined;
-    };
-  };
-
-  // Checking for window.defineProperty for IE compatibility,
-  // just so we don't throw exceptions when the script is included
-  if (OT.$.canDefineProperty) {
-    // Extracts a snapshot from a video element and returns it's as a PNG Data string.
-    Object.defineProperties(OT.VideoElement.prototype, {
-      imgData: {
-        get: function() {
-          var canvas,
-              imgData;
-
-          canvas = OT.$.createElement('canvas', {
-            width: this.domElement.videoWidth,
-            height: this.domElement.videoHeight,
-            style: {
-              display: 'none'
-            }
-          });
-
-          document.body.appendChild(canvas);
-
-          try {
-            canvas.getContext('2d').drawImage(this.domElement, 0, 0, canvas.width, canvas.height);
-          } catch(err) {
-            OT.warn('Cannot get image data yet');
-            return null;
-          }
-
-          imgData = canvas.toDataURL('image/png');
-
-          OT.$.removeElement(canvas);
-
-          return imgData.replace('data:image/png;base64,', '').trim();
-        }
-      },
-
-      videoWidth: {
-        get: function() {
-          return this.domElement['video' + (this.isRotated ? 'Height' : 'Width')];
-        }
-      },
-
-      videoHeight: {
-        get: function() {
-          return this.domElement['video' + (this.isRotated ? 'Width' : 'Height')];
-        }
-      },
-
-      aspectRatio: {
-        get: function() {
-          return (this.videoWidth + 0.0) / this.videoHeight;
-        }
-      },
-
+      return void 0;
+    };
+  };
+
+/// Private Helper functions
+
+  // A mixin to create the orientation API implementation on +self+
+  // +getDomElementCallback+ is a function that the mixin will call when it wants to
+  // get the native Dom element for +self+.
+  //
+  // +initialOrientation+ sets the initial orientation (shockingly), it's currently unused
+  // so the initial value is actually undefined.
+  //
+  var canBeOrientatedMixin = function canBeOrientatedMixin (self,
+                                                            getDomElementCallback,
+                                                            orientationChangedHandler,
+                                                            initialOrientation) {
+    var _orientation = initialOrientation;
+
+    OT.$.defineProperties(self, {
       isRotated: {
         get: function() {
-          return this._orientation && (
-            this._orientation.videoOrientation === 270 ||
-            this._orientation.videoOrientation === 90
-          );
+          return this.orientation() &&
+                    (this.orientation().videoOrientation === 270 ||
+                     this.orientation().videoOrientation === 90);
         }
       },
 
       orientation: {
-        get: function() { return this._orientation; },
+        get: function() { return _orientation; },
         set: function(orientation) {
+          _orientation = orientation;
+
           var transform = VideoOrientationTransforms[orientation.videoOrientation] ||
-              VideoOrientationTransforms.ROTATED_NORMAL;
-
-          this._orientation = orientation;
+                          VideoOrientationTransforms.ROTATED_NORMAL;
 
           switch(OT.$.browser()) {
             case 'Chrome':
             case 'Safari':
-              this.domElement.style.webkitTransform = transform;
+              getDomElementCallback().style.webkitTransform = transform;
               break;
 
             case 'IE':
-              this.domElement.style.msTransform = transform;
+              if (OT.$.browserVersion().version >= 9) {
+                getDomElementCallback().style.msTransform = transform;
+              }
+              else {
+                // So this basically defines matrix that represents a rotation
+                // of a single vector in a 2d basis.
+                //
+                //    R =  [cos(Theta) -sin(Theta)]
+                //         [sin(Theta)  cos(Theta)]
+                //
+                // Where Theta is the number of radians to rotate by
+                //
+                // Then to rotate the vector v:
+                //    v' = Rv
+                //
+                // We then use IE8 Matrix filter property, which takes
+                // a 2x2 rotation matrix, to rotate our DOM element.
+                //
+                var radians = orientation.videoOrientation * DEGREE_TO_RADIANS,
+                    element = getDomElementCallback(),
+                    costheta = Math.cos(radians),
+                    sintheta = Math.sin(radians);
+
+                // element.filters.item(0).M11 = costheta;
+                // element.filters.item(0).M12 = -sintheta;
+                // element.filters.item(0).M21 = sintheta;
+                // element.filters.item(0).M22 = costheta;
+
+                element.style.filter = 'progid:DXImageTransform.Microsoft.Matrix(' +
+                                          'M11='+costheta+',' +
+                                          'M12='+(-sintheta)+',' +
+                                          'M21='+sintheta+',' +
+                                          'M22='+costheta+',SizingMethod=\'auto expand\')';
+              }
+
+
               break;
 
             default:
               // The standard version, just Firefox, Opera, and IE > 9
-              this.domElement.style.transform = transform;
-          }
-
-          this.trigger('orientationChanged');
+              getDomElementCallback().style.transform = transform;
+          }
+
+          orientationChangedHandler(_orientation);
+
         }
       },
 
       // see https://wiki.mozilla.org/WebAPI/AudioChannels
       // The audioChannelType is currently only available in Firefox. This property returns
       // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel"
       audioChannelType: {
         get: function() {
@@ -4225,21 +6639,19 @@ OTHelpers.centerElement = function(eleme
         },
         set: function(type) {
           if ('mozAudioChannelType' in this.domElement) {
             this.domElement.mozAudioChannelType = type;
           }
         }
       }
     });
-  }
-
-/// Private Helper functions
-
-  function createVideoElement(fallbackText, attributes) {
+  };
+
+  function createNativeVideoElement(fallbackText, attributes) {
     var videoElement = document.createElement('video');
     videoElement.setAttribute('autoplay', '');
     videoElement.innerHTML = fallbackText;
 
     if (attributes) {
       if (attributes.muted === true) {
         delete attributes.muted;
         videoElement.muted = 'true';
@@ -4253,16 +6665,18 @@ OTHelpers.centerElement = function(eleme
       }
     }
 
     return videoElement;
   }
 
 
   // See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes
+  var _videoErrorCodes = {};
+
   // Checking for window.MediaError for IE compatibility, just so we don't throw
   // exceptions when the script is included
   if (window.MediaError) {
     _videoErrorCodes[window.MediaError.MEDIA_ERR_ABORTED] = 'The fetching process for the media ' +
       'resource was aborted by the user agent at the user\'s request.';
     _videoErrorCodes[window.MediaError.MEDIA_ERR_NETWORK] = 'A network error of some description ' +
       'caused the user agent to stop fetching the media resource, after the resource was ' +
       'established to be usable.';
@@ -4272,79 +6686,84 @@ OTHelpers.centerElement = function(eleme
     _videoErrorCodes[window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = 'The media resource ' +
       'indicated by the src attribute was not suitable.';
   }
 
   function videoElementErrorCodeToStr(errorCode) {
     return _videoErrorCodes[parseInt(errorCode, 10)] || 'An unknown error occurred.';
   }
 
-  function bindStreamToVideoElement(videoElement, webRTCStream, onStreamBound, onStreamBoundError) {
+  function bindStreamToNativeVideoElement(videoElement, webRtcStream, completion) {
     var cleanup,
         onLoad,
         onError,
         onStoppedLoading,
         timeout;
 
     // Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938
-    if (navigator.mozGetUserMedia || (
-      webRTCStream.getVideoTracks().length > 0 && webRTCStream.getVideoTracks()[0].enabled)) {
+    // After version 36 it will fire if the video track is disabled.
+    var browser = OT.$.browserVersion(),
+        needsDisabledAudioProtection = browser.browser === 'Chrome' && browser.version < 36;
+
+    if (navigator.mozGetUserMedia || !(needsDisabledAudioProtection &&
+        (webRtcStream.getVideoTracks().length > 0 && webRtcStream.getVideoTracks()[0].enabled))) {
 
       cleanup = function cleanup () {
         clearTimeout(timeout);
         videoElement.removeEventListener('loadedmetadata', onLoad, false);
         videoElement.removeEventListener('error', onError, false);
-        webRTCStream.onended = null;
+        webRtcStream.onended = null;
       };
 
       onLoad = function onLoad () {
         cleanup();
-        onStreamBound();
+        completion(null);
       };
 
       onError = function onError (event) {
         cleanup();
-        onStreamBoundError('There was an unexpected problem with the Video Stream: ' +
+        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();
-        onStreamBoundError('Stream ended while trying to bind it to a video element.');
+        completion('Stream ended while trying to bind it to a video element.');
       };
 
       // Timeout if it takes too long
-      timeout = setTimeout(function() {
+      timeout = setTimeout(OT.$.bind(function() {
         if (videoElement.currentTime === 0) {
-          onStreamBoundError('The video stream failed to connect. Please notify the site ' +
+          cleanup();
+          completion('The video stream failed to connect. Please notify the site ' +
             'owner if this continues to happen.');
         } else {
           // This should never happen
           OT.warn('Never got the loadedmetadata event but currentTime > 0');
-          onStreamBound();
-        }
-      }.bind(this), 30000);
+          onLoad(null);
+        }
+      }, this), 30000);
 
 
       videoElement.addEventListener('loadedmetadata', onLoad, false);
       videoElement.addEventListener('error', onError, false);
-      webRTCStream.onended = onStoppedLoading;
+      webRtcStream.onended = onStoppedLoading;
     } else {
-      onStreamBound();
+      OT.$.callAsync(completion, null);
     }
 
     // The official spec way is 'srcObject', we are slowly converging there.
     if (videoElement.srcObject !== void 0) {
-      videoElement.srcObject = webRTCStream;
+      videoElement.srcObject = webRtcStream;
     } else if (videoElement.mozSrcObject !== void 0) {
-      videoElement.mozSrcObject = webRTCStream;
+      videoElement.mozSrcObject = webRtcStream;
     } else {
-      videoElement.src = window.URL.createObjectURL(webRTCStream);
+      videoElement.src = window.URL.createObjectURL(webRtcStream);
     }
 
     videoElement.play();
   }
 
 })(window);
 !(function(window) {
 
@@ -4358,19 +6777,22 @@ OTHelpers.centerElement = function(eleme
     var endPoint = OT.properties.loggingURL + '/logging/ClientEvent',
         endPointQos = OT.properties.loggingURL + '/logging/ClientQos',
 
         reportedErrors = {},
 
         // Map of camel-cased keys to underscored
         camelCasedKeys,
 
+        browser = OT.$.browserVersion(),
+
         send = function(data, isQos, callback) {
-          OT.$.post(isQos ? endPointQos : endPoint, {
+          OT.$.post((isQos ? endPointQos : endPoint) + '?_=' + OT.$.uuid.v4(), {
             body: data,
+            xdomainrequest: (browser.browser === 'IE' & browser.version < 10),
             headers: {
               'Content-Type': 'application/x-www-form-urlencoded'
             }
           }, callback);
         },
 
         throttledPost = function() {
           // Throttle logs so that they only happen 1 at a time
@@ -4384,16 +6806,17 @@ OTHelpers.centerElement = function(eleme
               queueRunning = false;
               throttledPost();
             };
 
             if (curr) {
               send(curr.data, curr.isQos, function(err) {
                 if(err) {
                   OT.debug('Failed to send ClientEvent, moving on to the next item.');
+                  // There was an error, move onto the next item
                 } else {
                   curr.onComplete();
                 }
                 setTimeout(processNextItem, 50);
               });
             }
           }
         },
@@ -4436,19 +6859,19 @@ OTHelpers.centerElement = function(eleme
     // Log an error via ClientEvents.
     //
     // @param [String] code
     // @param [String] type
     // @param [String] message
     // @param [Hash] details additional error details
     //
     // @param [Hash] options the options to log the client event with.
-    // @option options [String] action The name of the Event that we are logging. E.g. 
+    // @option options [String] action The name of the Event that we are logging. E.g.
     //  'TokShowLoaded'. Required.
-    // @option options [String] variation Usually used for Split A/B testing, when you 
+    // @option options [String] variation Usually used for Split A/B testing, when you
     //  have multiple variations of the +_action+.
     // @option options [String] payloadType A text description of the payload. Required.
     // @option options [String] payload The payload. Required.
     // @option options [String] sessionId The active OpenTok session, if there is one
     // @option options [String] connectionId The active OpenTok connectionId, if there is one
     // @option options [String] partnerId
     // @option options [String] guid ...
     // @option options [String] widgetId ...
@@ -4465,26 +6888,26 @@ OTHelpers.centerElement = function(eleme
       if (!options) options = {};
       var partnerId = options.partnerId;
 
       if (OT.Config.get('exceptionLogging', 'enabled', partnerId) !== true) {
         return;
       }
 
       if (shouldThrottleError(code, type, partnerId)) {
-        //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' + 
+        //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' +
         // code + ' for partner ' + (partnerId || 'No Partner Id'));
         return;
       }
 
       var errKey = [partnerId, type, code].join('_'),
 
       payload = this.escapePayload(OT.$.extend(details || {}, {
         message: payload,
-        userAgent: navigator.userAgent
+        userAgent: OT.$.userAgent()
       }));
 
 
       reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ?
         reportedErrors[errKey] + 1 : 1;
 
       return this.logEvent(OT.$.extend(options, {
         action: type + '.' + code,
@@ -4500,19 +6923,19 @@ OTHelpers.centerElement = function(eleme
     //      action: 'foo',
     //      payload_type: 'foo's payload',
     //      payload: 'bar',
     //      session_id: sessionId,
     //      connection_id: connectionId
     //  })
     //
     // @param [Hash] options the options to log the client event with.
-    // @option options [String] action The name of the Event that we are logging. 
+    // @option options [String] action The name of the Event that we are logging.
     //  E.g. 'TokShowLoaded'. Required.
-    // @option options [String] variation Usually used for Split A/B testing, when 
+    // @option options [String] variation Usually used for Split A/B testing, when
     //  you have multiple variations of the +_action+.
     // @option options [String] payloadType A text description of the payload. Required.
     // @option options [String] payload The payload. Required.
     // @option options [String] session_id The active OpenTok session, if there is one
     // @option options [String] connection_id The active OpenTok connectionId, if there is one
     // @option options [String] partner_id
     // @option options [String] guid ...
     // @option options [String] widget_id ...
@@ -4727,29 +7150,39 @@ OTHelpers.centerElement = function(eleme
 *       <code>targetElement</code> value does not exist in the HTML DOM.
 * </p>
 *
 * @param {Object} properties (Optional) This object contains the following properties (each of which
 * are optional):
 * </p>
 * <ul>
 * <li>
+*   <strong>audioSource</strong> (String) &#151; The ID of the audio input device (such as a
+*    microphone) to be used by the publisher. You can obtain a list of available devices, including
+*    audio input devices, by calling the <a href="#getDevices">OT.getDevices()</a> method. Each
+*    device listed by the method has a unique device ID. If you pass in a device ID that does not
+*    match an existing audio input device, the call to <code>OT.initPublisher()</code> fails with an
+*    error (error code 1500, "Unable to Publish") passed to the completion handler function.
+* </li>
+* <li>
 *   <strong>frameRate</strong> (Number) &#151; The desired frame rate, in frames per second,
 *   of the video. Valid values are 30, 15, 7, and 1. The published stream will use the closest
 *   value supported on the publishing client. The frame rate can differ slightly from the value
 *   you set, depending on the browser of the client. And the video will only use the desired
 *   frame rate if the client configuration supports it.
 *   <br><br><p>If the publisher specifies a frame rate, the actual frame rate of the video stream
 *   is set as the <code>frameRate</code> property of the Stream object, though the actual frame rate
 *   will vary based on changing network and system conditions. If the developer does not specify a
 *   frame rate, this property is undefined.
 *   <p>
-*   For OpenTok cloud-enabled sessions, lowering the frame rate or lowering the resolution reduces
-*   the maximum bandwidth the stream can use. However, in peer-to-peer sessions, lowering the frame 
-*   rate or resolution may not reduce the stream's bandwidth.
+*   For 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, lowering the frame rate or lowering the resolution reduces
+*   the maximum bandwidth the stream can use. However, in sessions with the media mode set to
+*   relayed, lowering the frame rate or resolution may not reduce the stream's bandwidth.
 *   </p>
 *   <p>
 *   You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate
 *   a Subscriber, call the <code>restrictFrameRate()</code> method of the subscriber, passing in
 *   <code>true</code>.
 *   (See <a href="Subscriber.html#restrictFrameRate">Subscriber.restrictFrameRate()</a>.)
 *   </p>
 * </li>
@@ -4806,23 +7239,25 @@ OTHelpers.centerElement = function(eleme
 *   <code>"320x240"</code>. The published video will only use the desired resolution if the
 *   client configuration supports it.
 *   <br><br><p>
 *   The requested resolution of a video stream is set as the <code>videoDimensions.width</code> and
 *   <code>videoDimensions.height</code> properties of the Stream object.
 *   </p>
 *   <p>
 *   The default resolution for a stream (if you do not specify a resolution) is 640x480 pixels.
-*   If the client system cannot support the resolution you requested, the the stream will use the 
+*   If the client system cannot support the resolution you requested, the the stream will use the
 *   next largest setting supported.
 *   </p>
 *   <p>
-*   For OpenTok cloud-enabled sessions, lowering the frame rate or lowering the resolution reduces
-*   the maximum bandwidth the stream can use. However, in peer-to-peer sessions, lowering the frame
-*   rate or resolution may not reduce the stream's bandwidth.
+*   For 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, lowering the frame rate or lowering the resolution reduces the maximum bandwidth
+*   the stream can use. However, in sessions that have the media mode set to relayed, lowering the
+*   frame rate or resolution may not reduce the stream's bandwidth.
 *   </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
@@ -4844,16 +7279,24 @@ OTHelpers.centerElement = function(eleme
 *
 *       <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>
 *   </ul>
 * </li>
 * <li>
+*   <strong>videoSource</strong> (String) &#151; The ID of the video input device (such as a
+*    camera) to be used by the publisher. You can obtain a list of available devices, including
+*    video input devices, by calling the <a href="#getDevices">OT.getDevices()</a> method. Each
+*    device listed by the method has a unique device ID. If you pass in a device ID that does not
+*    match an existing video input device, the call to <code>OT.initPublisher()</code> fails with an
+*    error (error code 1500, "Unable to Publish") passed to the completion handler function.
+* </li>
+* <li>
 *   <strong>width</strong> (Number) &#151; The desired width, in pixels, of the
 *   displayed Publisher video stream (default: 264). <i>Note:</i> Use the
 *   <code>height</code> and <code>width</code> properties to set the dimensions
 *   of the publisher video; do not set the height and width of the DOM element
 *   (using CSS).
 * </li>
 * </ul>
 * @param {Function} completionHandler (Optional) A function to be called when the method succeeds
@@ -4882,25 +7325,26 @@ OTHelpers.centerElement = function(eleme
 * @returns {Publisher} The Publisher object.
 * @see <a href="Session#publish>Session.publish()</a>
 * @method OT.initPublisher
 * @memberof OT
 */
   OT.initPublisher = function(targetElement, properties, completionHandler) {
     OT.debug('OT.initPublisher('+targetElement+')');
 
-    if(targetElement != null && !(
-      (typeof targetElement === 'object' && targetElement.nodeType === Node.ELEMENT_NODE) ||
+    // To support legacy (apikey, targetElement, properties) users
+    // we check to see if targetElement is actually an apikey. Which we ignore.
+    if(targetElement != null && !(OT.$.isElementNode(targetElement) ||
       (typeof targetElement === 'string' && document.getElementById(targetElement))) &&
       typeof targetElement !== 'function') {
       targetElement = properties;
       properties = completionHandler;
       completionHandler = arguments[3];
     }
-    
+
     if(typeof targetElement === 'function') {
       completionHandler = targetElement;
       properties = undefined;
       targetElement = undefined;
     }
 
     if(typeof properties === 'function') {
       completionHandler = properties;
@@ -4935,30 +7379,75 @@ OTHelpers.centerElement = function(eleme
     publisher.once('initSuccess', removeInitSuccessAndCallComplete);
     publisher.once('publishComplete', removeHandlersAndCallComplete);
 
     publisher.publish(targetElement, properties);
 
     return publisher;
   };
 
+  /**
+  * Enumerates the audio input devices (such as microphones) and video input devices
+  * (cameras) available to the browser.
+  * <p>
+  * The array of devices is passed in as the <code>devices</code> parameter of
+  * the <code>callback</code> function passed into the method.
+  *
+  * @param callback {Function} The callback function invoked when the list of devices
+  * devices is available. This function takes two parameters:
+  * <ul>
+  *   <li><code>error</code> &mdash; This is set to an error object when
+  *   there is an error in calling this method; it is set to <code>null</code>
+  *   when the call succeeds.</li>
+  *
+  *   <li><p><code>devices</code> &mdash; An array of objects corresponding to
+  *   available microphones and cameras. Each object has three properties: <code>kind</code>,
+  *   <code>deviceId</code>, and <code>label</code>, each of which are strings.
+  *   <p>
+  *   The <code>kind</code> property is set to <code>"audioInput"</code> for audio input
+  *   devices or <code>"videoInput"</code> for video input devices.
+  *   <p>
+  *   The <code>deviceId</code> property is a unique ID for the device. You can pass
+  *   the <code>deviceId</code> in as the <code>audioSource</code> or <code>videoSource</code>
+  *   property of the the <code>options</code> parameter of the
+  *   <a href="#initPublisher">OT.initPublisher()</a> method.
+  *   <p>
+  *   The <code>label</code> property identifies the device. The <code>label</code>
+  *   property is set to an empty string if the user has not previously granted access to
+  *   a camera and microphone. In HTTP, the user must have granted access to a camera and
+  *   microphone in the current page (for example, in response to a call to
+  *   <code>OT.initPublisher()</code>). In HTTPS, the user must have previously granted access
+  *   to the camera and microphone in the current page or in a page previously loaded from the
+  *   domain.</li>
+  * </ul>
+  *
+  * @see <a href="#initPublisher">OT.initPublisher()</a>
+  * @method OT.getDevices
+  * @memberof OT
+  */
+  OT.getDevices = function(callback) {
+    OT.$.getMediaDevices(callback);
+  };
 
 
 /**
 * Checks if the system supports OpenTok for WebRTC.
 * @return {Number} Whether the system supports OpenTok for WebRTC (1) or not (0).
 * @see <a href="#upgradeSystemRequirements">OT.upgradeSystemRequirements()</a>
 * @method OT.checkSystemRequirements
 * @memberof OT
 */
   OT.checkSystemRequirements = function() {
     OT.debug('OT.checkSystemRequirements()');
 
-    var systemRequirementsMet = OT.$.supportsWebSockets() && OT.$.supportsWebRTC() ?
-      this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS;
+    // Try native support first, then TBPlugin...
+    var systemRequirementsMet = (OT.$.supportsWebSockets() && OT.$.supportsWebRTC());
+
+    systemRequirementsMet = systemRequirementsMet ?
+                                      this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS;
 
     OT.checkSystemRequirements = function() {
       OT.debug('OT.checkSystemRequirements()');
       return systemRequirementsMet;
     };
 
     return systemRequirementsMet;
   };
@@ -5001,22 +7490,24 @@ OTHelpers.centerElement = function(eleme
           // but we just make the background of the iframe completely transparent.
           d.style.backgroundColor = 'transparent';
           d.setAttribute('allowTransparency', 'true');
         }
         d.setAttribute('frameBorder', '0');
         d.frameBorder = '0';
         d.scrolling = 'no';
         d.setAttribute('scrolling', 'no');
+
         var browser = OT.$.browserVersion(),
             isSupportedButOld = OT.properties.minimumVersion[browser.browser.toLowerCase()];
         d.src = OT.properties.assetURL + '/html/upgrade.html#' +
                           encodeURIComponent(isSupportedButOld ? 'true' : 'false') + ',' +
                           encodeURIComponent(JSON.stringify(OT.properties.minimumVersion)) + '|' +
                           encodeURIComponent(document.location.href);
+
         return d;
       })());
 
       // Now we need to listen to the event handler if the user closes this dialog.
       // Since this is from an IFRAME within another domain we are going to listen to hash
       // changes. The best cross browser solution is to poll for a change in the hashtag.
       if (_intervalId) clearInterval(_intervalId);
       _intervalId = setInterval(function(){
@@ -5058,17 +7549,17 @@ OTHelpers.centerElement = function(eleme
     return m ? m[1] : '';
   })();
 
   OT.HAS_REQUIREMENTS = 1;
   OT.NOT_HAS_REQUIREMENTS = 0;
 
 /**
 * This method is deprecated. Use <a href="#on">on()</a> or <a href="#once">once()</a> instead.
-* 
+*
 * <p>
 * Registers a method as an event listener for a specific event.
 * </p>
 *
 * <p>
 * The OT object dispatches one type of event &#151; an <code>exception</code> event. The
 * following code adds an event listener for the <code>exception</code> event:
 * </p>
@@ -5095,17 +7586,17 @@ OTHelpers.centerElement = function(eleme
 * @see <a href="#on">on()</a>
 * @see <a href="#once">once()</a>
 * @memberof OT
 * @method addEventListener
 */
 
 /**
 * This method is deprecated. Use <a href="#off">off()</a> instead.
-* 
+*
 * <p>
 * Removes an event listener for a specific event.
 * </p>
 *
 * <p>
 *   Throws an exception if the <code>listener</code> name is invalid.
 * </p>
 *
@@ -5118,28 +7609,28 @@ OTHelpers.centerElement = function(eleme
 * @method removeEventListener
 */
 
 
 /**
 * Adds an event handler function for one or more events.
 *
 * <p>
-* The OT object dispatches one type of event &#151; an <code>exception</code> event. The following 
+* The OT object dispatches one type of event &#151; an <code>exception</code> event. The following
 * code adds an event
 * listener for the <code>exception</code> event:
 * </p>
 *
 * <pre>
 * OT.on("exception", function (event) {
 *   // This is the event handler.
 * });
 * </pre>
 *
-* <p>You can also pass in a third <code>context</code> parameter (which is optional) to define the 
+* <p>You can also pass in a third <code>context</code> parameter (which is optional) to define the
 * value of
 * <code>this</code> in the handler method:</p>
 *
 * <pre>
 * OT.on("exception",
 *   function (event) {
 *     // This is the event handler.
 *   }),
@@ -5149,82 +7640,82 @@ OTHelpers.centerElement = function(eleme
 *
 * <p>
 * If you do not add a handler for an event, the event is ignored locally.
 * </p>
 *
 * @param {String} type The string identifying the type of event.
 * @param {Function} handler The handler function to process the event. This function takes the event
 * object as a parameter.
-* @param {Object} context (Optional) Defines the value of <code>this</code> in the event handler 
+* @param {Object} context (Optional) Defines the value of <code>this</code> in the event handler
 * function.
 *
 * @memberof OT
 * @method on
 * @see <a href="#off">off()</a>
 * @see <a href="#once">once()</a>
 * @see <a href="#events">Events</a>
 */
 
 /**
-* Adds an event handler function for an event. Once the handler is called, the specified handler 
+* Adds an event handler function for an event. Once the handler is called, the specified handler
 * method is
 * removed as a handler for this event. (When you use the <code>OT.on()</code> method to add an event
 * handler, the handler
-* is <i>not</i> removed when it is called.) The <code>OT.once()</code> method is the equivilent of 
+* is <i>not</i> removed when it is called.) The <code>OT.once()</code> method is the equivilent of
 * calling the <code>OT.on()</code>
 * method and calling <code>OT.off()</code> the first time the handler is invoked.
 *
 * <p>
 * The following code adds a one-time event handler for the <code>exception</code> event:
 * </p>
 *
 * <pre>
 * OT.once("exception", function (event) {
 *   console.log(event);
 * }
 * </pre>
 *
-* <p>You can also pass in a third <code>context</code> parameter (which is optional) to define the 
+* <p>You can also pass in a third <code>context</code> parameter (which is optional) to define the
 * value of
 * <code>this</code> in the handler method:</p>
 *
 * <pre>
 * OT.once("exception",
 *   function (event) {
 *     // This is the event handler.
 *   },
 *   session
 * );
 * </pre>
 *
 * <p>
-* The method also supports an alternate syntax, in which the first parameter is an object that is a 
+* The method also supports an alternate syntax, in which the first parameter is an object that is a
 * hash map of
-* event names and handler functions and the second parameter (optional) is the context for this in 
+* event names and handler functions and the second parameter (optional) is the context for this in
 * each handler:
 * </p>
 * <pre>
 * OT.once(
 *   {exeption: function (event) {
 *     // This is the event handler.
 *     }
 *   },
 *   session
 * );
 * </pre>
 *
-* @param {String} type The string identifying the type of event. You can specify multiple event 
+* @param {String} type The string identifying the type of event. You can specify multiple event
 * names in this string,
-* separating them with a space. The event handler will process the first occurence of the events. 
+* separating them with a space. The event handler will process the first occurence of the events.
 * After the first event,
 * the handler is removed (for all specified events).
 * @param {Function} handler The handler function to process the event. This function takes the event
 * object as a parameter.
-* @param {Object} context (Optional) Defines the value of <code>this</code> in the event handler 
+* @param {Object} context (Optional) Defines the value of <code>this</code> in the event handler
 * function.
 *
 * @memberof OT
 * @method once
 * @see <a href="#on">on()</a>
 * @see <a href="#once">once()</a>
 * @see <a href="#events">Events</a>
 */
@@ -5232,42 +7723,42 @@ OTHelpers.centerElement = function(eleme
 
 /**
 * Removes an event handler.
 *
 * <p>Pass in an event name and a handler method, the handler is removed for that event:</p>
 *
 * <pre>OT.off("exceptionEvent", exceptionEventHandler);</pre>
 *
-* <p>If you pass in an event name and <i>no</i> handler method, all handlers are removed for that 
+* <p>If you pass in an event name and <i>no</i> handler method, all handlers are removed for that
 * events:</p>
 *
 * <pre>OT.off("exceptionEvent");</pre>
 *
 * <p>
-* The method also supports an alternate syntax, in which the first parameter is an object that is a 
+* The method also supports an alternate syntax, in which the first parameter is an object that is a
 * hash map of
-* event names and handler functions and the second parameter (optional) is the context for matching 
+* event names and handler functions and the second parameter (optional) is the context for matching
 * handlers:
 * </p>
 * <pre>
 * OT.off(
 *   {
 *     exceptionEvent: exceptionEventHandler
 *   },
 *   this
 * );
 * </pre>
 *
-* @param {String} type (Optional) The string identifying the type of event. You can use a space to 
+* @param {String} type (Optional) The string identifying the type of event. You can use a space to
 * specify multiple events, as in "eventName1 eventName2 eventName3". If you pass in no
 * <code>type</code> value (or other arguments), all event handlers are removed for the object.
-* @param {Function} handler (Optional) The event handler function to remove. If you pass in no 
+* @param {Function} handler (Optional) The event handler function to remove. If you pass in no
 * <code>handler</code>, all event handlers are removed for the specified event <code>type</code>.
-* @param {Object} context (Optional) If you specify a <code>context</code>, the event handler is 
+* @param {Object} context (Optional) If you specify a <code>context</code>, the event handler is
 * removed for all specified events and handlers that use the specified context.
 *
 * @memberof OT
 * @method off
 * @see <a href="#on">on()</a>
 * @see <a href="#once">once()</a>
 * @see <a href="#events">Events</a>
 */
@@ -5292,41 +7783,49 @@ OTHelpers.centerElement = function(eleme
 
   OT.Collection = function(idField) {
     var _models = [],
         _byId = {},
         _idField = idField || 'id';
 
     OT.$.eventing(this, true);
 
-    var onModelUpdate = function onModelUpdate (event) {
+    var modelProperty = function(model, property) {
+      if(OT.$.isFunction(model[property])) {
+        return model[property]();
+      } else {
+        return model[property];
+      }
+    };
+
+    var onModelUpdate = OT.$.bind(function onModelUpdate (event) {
           this.trigger('update', event);
           this.trigger('update:'+event.target.id, event);
-        }.bind(this),
-
-        onModelDestroy = function onModelDestroyed (event) {
+        }, this),
+
+        onModelDestroy = OT.$.bind(function onModelDestroyed (event) {
           this.remove(event.target, event.reason);
-        }.bind(this);
+        }, this);
 
 
     this.reset = function() {
       // Stop listening on the models, they are no longer our problem
-      _models.forEach(function(model) {
+      OT.$.forEach(_models, function(model) {
         model.off('updated', onModelUpdate, this);
         model.off('destroyed', onModelDestroy, this);
       }, this);
 
       _models = [];
       _byId = {};
     };
 
-    this.destroy = function() {
-      _models.forEach(function(model) {
+    this.destroy = function(reason) {
+      OT.$.forEach(_models, function(model) {
         if(model && typeof model.destroy === 'function') {
-          model.destroy(void 0, true);
+          model.destroy(reason, true);
         }
       });
 
       this.reset();
       this.off();
     };
 
     this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
@@ -5347,24 +7846,24 @@ OTHelpers.centerElement = function(eleme
     //
     // @example The same thing but filtering using a filter function
     //          executed with a specific this
     //   OT.publishers.where(function(publisher) {
     //     return publisher.stream.id === 4;
     //   }, self);
     //
     this.where = function(attrsOrFilterFn, context) {
-      if (OT.$.isFunction(attrsOrFilterFn)) return _models.filter(attrsOrFilterFn, context);
-
-      return _models.filter(function(model) {
+      if (OT.$.isFunction(attrsOrFilterFn)) return OT.$.filter(_models, attrsOrFilterFn, context);
+
+      return OT.$.filter(_models, function(model) {
         for (var key in attrsOrFilterFn) {
           if(!attrsOrFilterFn.hasOwnProperty(key)) {
             continue;
           }
-          if (model[key] !== attrsOrFilterFn[key]) return false;
+          if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
         }
 
         return true;
       });
     };
 
     // Similar to where in behaviour, except that it only returns
     // the first match.
@@ -5375,34 +7874,34 @@ OTHelpers.centerElement = function(eleme
         filterFn = attrsOrFilterFn;
       }
       else {
         filterFn = function(model) {
           for (var key in attrsOrFilterFn) {
             if(!attrsOrFilterFn.hasOwnProperty(key)) {
               continue;
             }
-            if (model[key] !== attrsOrFilterFn[key]) return false;
+            if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
           }
 
           return true;
         };
       }
 
-      filterFn = filterFn.bind(context);
+      filterFn = OT.$.bind(filterFn, context);
 
       for (var i=0; i<_models.length; ++i) {
         if (filterFn(_models[i]) === true) return _models[i];
       }
 
       return null;
     };
 
     this.add = function(model) {
-      var id = model[_idField];
+      var id = modelProperty(model, _idField);
 
       if (this.has(id)) {
         OT.warn('Model ' + id + ' is already in the collection', _models);
         return this;
       }
 
       _byId[id] = _models.push(model) - 1;
 
@@ -5411,17 +7910,17 @@ OTHelpers.centerElement = function(eleme
 
       this.trigger('add', model);
       this.trigger('add:'+id, model);
 
       return this;
     };
 
     this.remove = function(model, reason) {
-      var id = model[_idField];
+      var id = modelProperty(model, _idField);
 
       _models.splice(_byId[id], 1);
 
       // Shuffle everyone down one
       for (var i=_byId[id]; i<_models.length; ++i) {
         _byId[_models[i][_idField]] = i;
       }
 
@@ -5434,25 +7933,25 @@ OTHelpers.centerElement = function(eleme
       this.trigger('remove:'+id, model, reason);
 
       return this;
     };
 
     // Used by session connecto fire add events after adding listeners
     this._triggerAddEvents = function() {
       var models = this.where.apply(this, arguments);
-      models.forEach(function(model) {
+      OT.$.forEach(models, function(model) {
         this.trigger('add', model);
-        this.trigger('add:' + model[_idField], model);
+        this.trigger('add:' + modelProperty(model, _idField), model);
       }, this);
     };
 
-    OT.$.defineGetters(this, {
-      length: function() { return _models.length; }
-    });
+    this.length = function() {
+      return _models.length;
+    };
   };
 
 }(this));
 !(function() {
 
   /**
    * The Event object defines the basic OpenTok event object that is passed to
    * event listeners. Other OpenTok event classes implement the properties and methods of
@@ -5565,17 +8064,18 @@ OTHelpers.centerElement = function(eleme
     // DevicePanel Events
     DEVICES_SELECTED: 'devicesSelected',
     CLOSE_BUTTON_CLICK: 'closeButtonClick',
 
     MICLEVEL : 'microphoneActivityLevel',
     MICGAINCHANGED : 'microphoneGainChanged',
 
     // Environment Loader
-    ENV_LOADED: 'envLoaded'
+    ENV_LOADED: 'envLoaded',
+    ENV_UNLOADED: 'envUnloaded'
   };
 
   OT.ExceptionCodes = {
     JS_EXCEPTION: 2000,
     AUTHENTICATION_ERROR: 1004,
     INVALID_SESSION_ID: 1005,
     CONNECT_FAILED: 1006,
     CONNECT_REJECTED: 1007,
@@ -5881,17 +8381,17 @@ OTHelpers.centerElement = function(eleme
             connectionEventPluralDeprecationWarningShown = true;
           }
           return [connection];
         }
       });
     } else {
       this.connections = [connection];
     }
-    
+
     this.connection = connection;
     this.reason = reason;
   };
 
 /**
  * StreamEvent is an event that can have the type "streamCreated" or "streamDestroyed".
  * These events are dispatched by the Session object when another client starts or
  * stops publishing a stream to a {@link Session}. For a local client's stream, the
@@ -6064,17 +8564,16 @@ OTHelpers.centerElement = function(eleme
  *
  * @see <a href="Session.html#connect">Session.connect()</a></p>
  * @augments Event
  */
 
   var sessionConnectedConnectionsDeprecationWarningShown = false;
   var sessionConnectedStreamsDeprecationWarningShown = false;
   var sessionConnectedArchivesDeprecationWarningShown = false;
-  var sessionConnectedGroupsDeprecationWarningShown = false;
 
   OT.SessionConnectEvent = function (type) {
     OT.Event.call(this, type, false);
     if (OT.$.canDefineProperty) {
       Object.defineProperties(this, {
         connections: {
           get: function() {
             if(!sessionConnectedConnectionsDeprecationWarningShown) {
@@ -6099,47 +8598,35 @@ OTHelpers.centerElement = function(eleme
           get: function() {
             if(!sessionConnectedArchivesDeprecationWarningShown) {
               OT.warn('OT.SessionConnectedEvent no longer includes archives. Listen for ' +
                 'archiveStarted events instead.');
               sessionConnectedArchivesDeprecationWarningShown = true;
             }
             return [];
           }
-        },
-        groups: {
-          get: function() {
-            if(!sessionConnectedGroupsDeprecationWarningShown) {
-              OT.error('OT.SessionConnectedEvent no longer includes groups. There is no ' +
-                'equivelant in OpenTok v2.2');
-              sessionConnectedGroupsDeprecationWarningShown = true;
-            }
-            return [];
-          }
         }
       });
     } else {
       this.connections = [];
       this.streams = [];
       this.archives = [];
-      // Deprecated in OpenTok v0.91.48
-      this.groups = [];
     }
   };
 
 /**
- * The Session object dispatches SessionDisconnectEvent object when a session has disconnected. 
- * This event may be dispatched asynchronously in response to a successful call to the 
+ * The Session object dispatches SessionDisconnectEvent object when a session has disconnected.
+ * This event may be dispatched asynchronously in response to a successful call to the
  * <code>disconnect()</code> method of the session object.
  *
  *  <h4>
  *    <a href="example"></a>Example
  *  </h4>
  *  <p>
- *    The following code initializes a session and sets up an event listener for when a session is 
+ *    The following code initializes a session and sets up an event listener for when a session is
  * disconnected.
  *  </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
  *
@@ -6155,17 +8642,17 @@ OTHelpers.centerElement = function(eleme
  *  </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.
  *      ( See <a href="Session.html#disconnect">Session.disconnect()</a>.)</li>
  *    <li><code>"forceDisconnected"</code> &#151; A moderator has disconnected you from the session
  *     by calling the <code>forceDisconnect()</code> method of the Session object. (See
  *       <a href="Session.html#forceDisconnect">Session.forceDisconnect()</a>.)</li>
- *    <li><code>"networkDisconnected"</code> &#151; The network connection terminated abruptly 
+ *    <li><code>"networkDisconnected"</code> &#151; The network connection terminated abruptly
  *       (for example, the client lost their internet connection).</li>
  *  </ul>
  *
  * @class SessionDisconnectEvent
  * @augments Event
  */
   OT.SessionDisconnectEvent = function (type, reason, cancelable) {
     OT.Event.call(this, type, cancelable);
@@ -6173,47 +8660,47 @@ OTHelpers.centerElement = function(eleme
   };
 
 /**
 * Prevents the default behavior associated with the event from taking place.
 *
 * <p>For the <code>sessionDisconnectEvent</code>, the default behavior is that all Subscriber
 * objects are unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a
 * <code>destroyed</code> event when the element is removed from the HTML DOM. If you call the
-* <code>preventDefault()</code> method in the event listener for the <code>sessionDisconnect</code> 
-* event, the default behavior is prevented, and you can, optionally, clean up Subscriber objects 
+* <code>preventDefault()</code> method in the event listener for the <code>sessionDisconnect</code>
+* event, the default behavior is prevented, and you can, optionally, clean up Subscriber objects
 * using your own code).
 *
-* <p>To see whether an event has a default behavior, check the <code>cancelable</code> property of 
+* <p>To see whether an event has a default behavior, check the <code>cancelable</code> property of
 * the event object. </p>
 *
 * <p>Call the <code>preventDefault()</code> method in the event listener function for the event.</p>
 *
 * @method #preventDefault
 * @memberof SessionDisconnectEvent
 */
 
 /**
- * The Session object dispatches a <code>streamPropertyChanged</code> event in the 
+ * The Session object dispatches a <code>streamPropertyChanged</code> event in the
  * following circumstances:
  *
  * <ul>
  *
- *  <li>When a publisher starts or stops publishing audio or video. This change causes 
- *  the <code>hasAudio</code> or <code>hasVideo</code> property of the Stream object to 
- *  change. This change results from a call to the <code>publishAudio()</code> or 
+ *  <li>When a publisher starts or stops publishing audio or video. This change causes
+ *  the <code>hasAudio</code> or <code>hasVideo</code> property of the Stream object to
+ *  change. This change results from a call to the <code>publishAudio()</code> or
  *  <code>publishVideo()</code> methods of the Publish object.</li>
  *
  *  <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 
+ * @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).
  *
  * @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>
@@ -6257,17 +8744,17 @@ OTHelpers.centerElement = function(eleme
     this.oldValue = oldValue;
     this.newValue = newValue;
   };
 
 /**
  * The Session object dispatches a signal event when the client receives a signal from the session.
  *
  * @class SignalEvent
- * @property {String} type The type assigned to the signal (if there is one). Use the type to 
+ * @property {String} type The type assigned to the signal (if there is one). Use the type to
  * filter signals received (by adding an event handler for signal:type1 or signal:type2, etc.)
  * @property {String} data The data string sent with the signal (if there is one).
  * @property {Connection} from The Connection corresponding to the client that sent with the signal.
  *
  * @see <a href="Session.html#signal">Session.signal()</a></p>
  * @see <a href="Session.html#events">Session events (signal and signal:type)</a></p>
  * @augments Event
  */
@@ -6313,16 +8800,21 @@ OTHelpers.centerElement = function(eleme
  * limitations under the License.
  *
  * Original source: https://github.com/inexorabletash/text-encoding
  ***/
 
 (function(global) {
   'use strict';
 
+  var browser = OT.$.browserVersion();
+  if(browser.browser === 'IE' && browser.version < 10) {
+    return; // IE 8 doesn't do websockets. No websockets, no encoding.
+  }
+
   if ( (global.TextEncoder !== void 0) && (global.TextDecoder !== void 0))  {
     // defer to the native ones
     // @todo is this a good idea?
     return;
   }
 
   //
   // Utilities
@@ -8808,24 +11300,19 @@ OTHelpers.centerElement = function(eleme
       //Enhancements to support Keepalives
       PING: 7,
       PONG: 8,
       STATUS: 9
     }
   };
 
 }(this));
-!(function() {
-
-  // The interval between polling the websocket's send buffer
-  var BUFFER_DRAIN_INTERVAL = 100,
-      // The total number of times to retest the websocket's send buffer
-      BUFFER_DRAIN_MAX_RETRIES = 10,
-
-      WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000,
+!(function(OT) {
+
+  var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000,
 
       // Magic Connectivity Timeout Constant: We wait 9*the keep alive interval,
       // on the third keep alive we trigger the timeout if we haven't received the
       // server pong.
       WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5*WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100,
 
       wsCloseErrorCodes;
 
@@ -8864,27 +11351,26 @@ OTHelpers.centerElement = function(eleme
 
   OT.Rumor.SocketError = function(code, message) {
     this.code = code;
     this.message = message;
   };
 
   // The NativeSocket bit is purely to make testing simpler, it defaults to WebSocket
   // so in normal operation you would omit it.
-  OT.Rumor.Socket = function(messagingServer, notifyDisconnectAddress, NativeSocket) {
+  OT.Rumor.Socket = function(messagingURL, notifyDisconnectAddress, NativeSocket) {
+
     var states = ['disconnected',  'error', 'connected', 'connecting', 'disconnecting'],
-        server = messagingServer,
         webSocket,
         id,
         onOpen,
         onError,
         onClose,
         onMessage,
         connectCallback,
-        bufferDrainTimeout,           // Timer to poll whether th send buffer has been drained
         connectTimeout,
         lastMessageTimestamp,         // The timestamp of the last message received
         keepAliveTimer;               // Timer for the connectivity checks
 
 
     //// Private API
     var stateChanged = function(newState) {
           switch (newState) {
@@ -8907,111 +11393,86 @@ OTHelpers.centerElement = function(eleme
 
         validateCallback = function validateCallback (name, callback) {
           if (callback === null || !OT.$.isFunction(callback) ) {
             throw new Error('The Rumor.Socket ' + name +
               ' callback must be a valid function or null');
           }
         },
 
-        error = function error (errorMessage) {
+        error = OT.$.bind(function error (errorMessage) {
           OT.error('Rumor.Socket: ' + errorMessage);
 
           var socketError = new OT.Rumor.SocketError(null, errorMessage || 'Unknown Socket Error');
 
           if (connectTimeout) clearTimeout(connectTimeout);
 
           setState('error');
 
           if (this.previousState === 'connecting' && connectCallback) {
             connectCallback(socketError, null);
             connectCallback = null;
           }
 
           if (onError) onError(socketError);
-        }.bind(this),
-
-        // Immediately close the socket, only used by disconnectWhenSendBufferIsDrained
-        close = function close() {
-          setState('disconnecting');
-
-          if (bufferDrainTimeout) {
-            clearTimeout(bufferDrainTimeout);
-            bufferDrainTimeout = null;
-          }
-
-          webSocket.close();
-        },
-
-        // Ensure that the WebSocket send buffer is fully drained before disconnecting
-        // the socket. If the buffer doesn't drain after a certain length of time
-        // we give up and close it anyway.
-        disconnectWhenSendBufferIsDrained =
-          function disconnectWhenSendBufferIsDrained (bufferDrainRetries) {
-          if (!webSocket) return;
-
-          if (bufferDrainRetries === void 0) bufferDrainRetries = 0;
-          if (bufferDrainTimeout) clearTimeout(bufferDrainTimeout);
-
-          if (webSocket.bufferedAmount > 0 &&
-            (bufferDrainRetries + 1) <= BUFFER_DRAIN_MAX_RETRIES) {
-            bufferDrainTimeout = setTimeout(disconnectWhenSendBufferIsDrained,
-              BUFFER_DRAIN_INTERVAL, bufferDrainRetries+1);
-          }
-          else {
-            close();
-          }
-        },
+        }, this),
 
         hasLostConnectivity = function hasLostConnectivity () {
           if (!lastMessageTimestamp) return false;
 
           return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT;
         },
 
-        sendKeepAlive = function sendKeepAlive () {
+        sendKeepAlive = OT.$.bind(function sendKeepAlive () {
           if (!this.is('connected')) return;
 
           if ( hasLostConnectivity() ) {
             webSocketDisconnected({code: 4001});
           }
           else  {
-            webSocket.send(OT.Rumor.Message.Ping().serialize());
+            webSocket.send(OT.Rumor.Message.Ping());
             keepAliveTimer = setTimeout(sendKeepAlive.bind(this), WEB_SOCKET_KEEP_ALIVE_INTERVAL);
           }
-        }.bind(this);
+        }, this),
+
+        // Returns true if we think the DOM has been unloaded
+        // It detects this by looking for the OT global, which
+        // should always exist until the DOM is cleaned up.
+        isDOMUnloaded = function isDOMUnloaded () {
+          return !window.OT;
+        };
 
 
     //// Private Event Handlers
-    var webSocketConnected = function webSocketConnected () {
+    var webSocketConnected = OT.$.bind(function webSocketConnected () {
           if (connectTimeout) clearTimeout(connectTimeout);
           if (this.isNot('connecting')) {
             OT.debug('webSocketConnected reached in state other than connecting');
             return;
           }
 
           // Connect to Rumor by registering our connection id and the
           // app server address to notify if we disconnect.
           //
           // We don't need to wait for a reply to this message.
-          webSocket.send(OT.Rumor.Message.Connect(id, notifyDisconnectAddress).serialize());
+          webSocket.send(OT.Rumor.Message.Connect(id, notifyDisconnectAddress));
 
           setState('connected');
           if (connectCallback) {
             connectCallback(null, id);
             connectCallback = null;
           }
 
           if (onOpen) onOpen(id);
 
           setTimeout(function() {
             lastMessageTimestamp = OT.$.now();
             sendKeepAlive();
           }, WEB_SOCKET_KEEP_ALIVE_INTERVAL);
-        }.bind(this),
+        }, this),
 
         webSocketConnectTimedOut = function webSocketConnectTimedOut () {
           var webSocketWas = webSocket;
           error('Timed out while waiting for the Rumor socket to connect.');
           // This will prevent a socket eventually connecting
           // But call it _after_ the error just in case any of
           // the callbacks fire synchronously, breaking the error
           // handling code.
@@ -9026,118 +11487,129 @@ OTHelpers.centerElement = function(eleme
 
           // All errors seem to result in disconnecting the socket, the close event
           // has a close reason and code which gives some error context. This,
           // combined with the fact that the errorEvent argument contains no
           // error info at all, means we'll delay triggering the error handlers
           // until the socket is closed.
           // error(errorMessage);
 
-        webSocketDisconnected = function webSocketDisconnected (closeEvent) {
+        webSocketDisconnected = OT.$.bind(function webSocketDisconnected (closeEvent) {
           if (connectTimeout) clearTimeout(connectTimeout);
           if (keepAliveTimer) clearTimeout(keepAliveTimer);
 
+          if (isDOMUnloaded()) {
+            // Sometimes we receive the web socket close event after
+            // the DOM has already been partially or fully unloaded
+            // if that's the case here then it's not really safe, or
+            // desirable, to continue.
+            return;
+          }
+
           if (closeEvent.code !== 1000 && closeEvent.code !== 1001) {
             var reason = closeEvent.reason || closeEvent.message;
             if (!reason && wsCloseErrorCodes.hasOwnProperty(closeEvent.code)) {
               reason = wsCloseErrorCodes[closeEvent.code];
             }
 
             error('Rumor Socket Disconnected: ' + reason);
           }
 
           if (this.isNot('error')) setState('disconnected');
-        }.bind(this),
-
-        webSocketReceivedMessage = function webSocketReceivedMessage (message) {
+        }, this),
+
+        webSocketReceivedMessage = function webSocketReceivedMessage (msg) {
           lastMessageTimestamp = OT.$.now();
 
           if (onMessage) {
-
-            var msg = OT.Rumor.Message.deserialize(message.data);
-
             if (msg.type !== OT.Rumor.MessageType.PONG) {
               onMessage(msg);
             }
           }
         };
 
 
     //// Public API
 
     this.publish = function (topics, message, headers) {
-      webSocket.send(OT.Rumor.Message.Publish(topics, message, headers).serialize());
+      webSocket.send(OT.Rumor.Message.Publish(topics, message, headers));
     };
 
     this.subscribe = function(topics) {
-      webSocket.send(OT.Rumor.Message.Subscribe(topics).serialize());
+      webSocket.send(OT.Rumor.Message.Subscribe(topics));
     };
 
     this.unsubscribe = function(topics) {
-      webSocket.send(OT.Rumor.Message.Unsubscribe(topics).serialize());
+      webSocket.send(OT.Rumor.Message.Unsubscribe(topics));
     };
 
     this.connect = function (connectionId, complete) {
       if (this.is('connecting', 'connected')) {
         complete(new OT.Rumor.SocketError(null,
             'Rumor.Socket cannot connect when it is already connecting or connected.'));
         return;
       }
 
       id = connectionId;
       connectCallback = complete;
 
+      setState('connecting');
+
+      var TheWebSocket = NativeSocket || window.WebSocket;
+
+      var events = {
+        onOpen:    webSocketConnected,
+        onClose:   webSocketDisconnected,
+        onError:   webSocketError,
+        onMessage: webSocketReceivedMessage
+      };
+
       try {
-        setState('connecting');
-
-        var TheWebSocket = NativeSocket || WebSocket;
-        webSocket = new TheWebSocket(server);
-        webSocket.binaryType = 'arraybuffer';
-
-        webSocket.onopen = webSocketConnected;
-        webSocket.onclose = webSocketDisconnected;
-        webSocket.onerror = webSocketError;
-        webSocket.onmessage = webSocketReceivedMessage;
+        if(typeof TheWebSocket !== 'undefined') {
+          webSocket = new OT.Rumor.NativeSocket(TheWebSocket, messagingURL, events);
+        } else {
+          webSocket = new OT.Rumor.PluginSocket(messagingURL, events);
+        }
 
         connectTimeout = setTimeout(webSocketConnectTimedOut, OT.Rumor.Socket.CONNECT_TIMEOUT);
       }
       catch(e) {
         OT.error(e);
 
         // @todo add an actual error message
         error('Could not connect to the Rumor socket, possibly because of a blocked port.');
       }
     };
 
-    this.disconnect = function() {
+    this.disconnect = function(drainSocketBuffer) {
       if (connectTimeout) clearTimeout(connectTimeout);
       if (keepAliveTimer) clearTimeout(keepAliveTimer);
 
       if (!webSocket) {
         if (this.isNot('error')) setState('disconnected');
         return;
       }
 
-      if (webSocket.readyState === 3/* CLOSED */) {
+      if (webSocket.isClosed()) {
         if (this.isNot('error')) setState('disconnected');
       }
       else {
         if (this.is('connected')) {
           // Look! We are nice to the rumor server ;-)
-          webSocket.send(OT.Rumor.Message.Disconnect().serialize());
+          webSocket.send(OT.Rumor.Message.Disconnect());
         }
 
         // Wait until the socket is ready to close
-        disconnectWhenSendBufferIsDrained();
-      }
-    };
-
-
-
-    Object.defineProperties(this, {
+        webSocket.close(drainSocketBuffer);
+      }
+    };
+
+
+
+    OT.$.defineProperties(this, {
       id: {
         get: function() { return id; }
       },
 
       onOpen: {
         set: function(callback) {
           validateCallback('onOpen', callback);
           onOpen = callback;
@@ -9173,16 +11645,154 @@ OTHelpers.centerElement = function(eleme
         get: function() { return onMessage; }
       }
     });
   };
 
   // The number of ms to wait for the websocket to connect
   OT.Rumor.Socket.CONNECT_TIMEOUT = 15000;
 
+}(window.OT, this));
+!(function() {
+
+  var BUFFER_DRAIN_INTERVAL = 100,
+      // The total number of times to retest the websocket's send buffer
+      BUFFER_DRAIN_MAX_RETRIES = 10;
+
+  OT.Rumor.NativeSocket = function(TheWebSocket, messagingURL, events) {
+
+    var webSocket,
+        disconnectWhenSendBufferIsDrained,
+        bufferDrainTimeout,           // Timer to poll whether th send buffer has been drained
+        close;
+
+    webSocket = new TheWebSocket(messagingURL);
+    webSocket.binaryType = 'arraybuffer';
+
+    webSocket.onopen = events.onOpen;
+    webSocket.onclose = events.onClose;
+    webSocket.onerror = events.onError;
+
+    webSocket.onmessage = function(message) {
+      if (!OT) {
+        // In IE 10/11, This can apparently be called after
+        // the page is unloaded and OT is garbage-collected
+        return;
+      }
+
+      var msg = OT.Rumor.Message.deserialize(message.data);
+      events.onMessage(msg);
+    };
+
+    // Ensure that the WebSocket send buffer is fully drained before disconnecting
+    // the socket. If the buffer doesn't drain after a certain length of time
+    // we give up and close it anyway.
+    disconnectWhenSendBufferIsDrained =
+      function disconnectWhenSendBufferIsDrained (bufferDrainRetries) {
+      if (!webSocket) return;
+
+      if (bufferDrainRetries === void 0) bufferDrainRetries = 0;
+      if (bufferDrainTimeout) clearTimeout(bufferDrainTimeout);
+
+      if (webSocket.bufferedAmount > 0 &&
+        (bufferDrainRetries + 1) <= BUFFER_DRAIN_MAX_RETRIES) {
+        bufferDrainTimeout = setTimeout(disconnectWhenSendBufferIsDrained,
+          BUFFER_DRAIN_INTERVAL, bufferDrainRetries+1);
+
+      } else {
+        close();
+      }
+    };
+
+    close = function close() {
+      webSocket.close();
+    };
+
+    this.close = function(drainBuffer) {
+      if (drainBuffer) {
+        disconnectWhenSendBufferIsDrained();
+      } else {
+        close();
+      }
+    };
+
+    this.send = function(msg) {
+      webSocket.send(msg.serialize());
+    };
+
+    this.isClosed = function() {
+      return webSocket.readyState === 3;
+    };
+
+  };
+
+
+}(this));
+!(function() {
+
+  OT.Rumor.PluginSocket = function(messagingURL, events) {
+
+    var webSocket,
+        state = 'initializing';
+
+    TBPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) {
+      if(err) {
+        state = 'closed';
+        events.onClose({ code: 4999 });
+      } else if(state === 'initializing') {
+        webSocket = rumorSocket;
+
+        webSocket.onOpen(function() {
+          state = 'open';
+          events.onOpen();
+        });
+        webSocket.onClose(function(error) {
+          state = 'closed'; /* CLOSED */
+          events.onClose({ code: error });
+          webSocket.finalize();
+        });
+        webSocket.onError(function(error) {
+          state = 'closed'; /* CLOSED */
+          events.onError(error);
+          /* native websockets seem to do this, so should we */
+          events.onClose({ code: error });
+        });
+
+        webSocket.onMessage(function(type, addresses, headers, payload) {
+          var msg = new OT.Rumor.Message(type, addresses, headers, payload);
+          events.onMessage(msg);
+        });
+
+        webSocket.open();
+      } else {
+        this.close();
+      }
+    }, this));
+
+    this.close = function() {
+      if(state === 'initializing' || state === 'closed') {
+        state = 'closed';
+        return;
+      }
+
+      webSocket.close(1000, '');
+    };
+
+    this.send = function(msg) {
+      if(state === 'open') {
+        webSocket.send(msg);
+      }
+    };
+
+    this.isClosed = function() {
+      return state === 'closed';
+    };
+
+  };
+
 }(this));
 !(function() {
 
   /*global TextEncoder, TextDecoder */
 
   //
   //
   // @references
@@ -9377,37 +11987,37 @@ OTHelpers.centerElement = function(eleme
       uniqueId: uniqueId,
       notifyDisconnectAddress: notifyDisconnectAddress
     };
 
     return new OT.Rumor.Message(OT.Rumor.MessageType.CONNECT, [], headers, '');
   };
 
   OT.Rumor.Message.Disconnect = function () {
-    return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], [], '');
+    return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], {}, '');
   };
 
   OT.Rumor.Message.Subscribe = function(topics) {
-    return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, [], '');
+    return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, {}, '');
   };
 
   OT.Rumor.Message.Unsubscribe = function(topics) {
-    return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, [], '');
+    return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, {}, '');
   };
 
   OT.Rumor.Message.Publish = function(topics, message, headers) {
-    return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers||[], message);
+    return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers||{}, message || '');
   };
 
   // This message is used to implement keepalives on the persistent
   // socket connection between the client and server. Every time the
   // client sends a PING to the server, the server will respond with
   // a PONG.
   OT.Rumor.Message.Ping = function() {
-    return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], [], '');
+    return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], {}, '');
   };
 
 }(this));
 !(function() {
 
   // Rumor Messaging for JS
   //
   // https://tbwiki.tokbox.com/index.php/Raptor_Messages_(Sent_as_a_RumorMessage_payload_in_JSON)
@@ -9579,17 +12189,17 @@ OTHelpers.centerElement = function(eleme
 
   OT.Raptor.Message.connections = {};
 
   OT.Raptor.Message.connections.create = function (apiKey, sessionId, connectionId) {
     return OT.Raptor.serializeMessage({
       method: 'create',
       uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
       content: {
-        userAgent: navigator.userAgent
+        userAgent: OT.$.userAgent()
       }
     });
   };
 
   OT.Raptor.Message.connections.destroy = function (apiKey, sessionId, connectionId) {
     return OT.Raptor.serializeMessage({
       method: 'delete',
       uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
@@ -9638,23 +12248,23 @@ OTHelpers.centerElement = function(eleme
         active: hasVideo,
         width: videoWidth,
         height: videoHeight,
         orientation: videoOrientation
       };
       if (frameRate) channel.frameRate = frameRate;
       channels.push(channel);
     }
-    
+
     var messageContent = {
       id: streamId,
       name: name,
       channel: channels
     };
-    
+
     if (minBitrate) messageContent.minBitrate = minBitrate;
     if (maxBitrate) messageContent.maxBitrate = maxBitrate;
 
     return OT.Raptor.serializeMessage({
       method: 'create',
       uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
       content: messageContent
     });
@@ -9970,39 +12580,38 @@ OTHelpers.centerElement = function(eleme
         _rumor,
         _dispatcher,
         _completion;
 
 
     //// Private API
     var setState = OT.$.statable(this, _states, 'disconnected'),
 
-        onConnectComplete = function onConnectComplete (error) {
+        onConnectComplete = function onConnectComplete(error) {
           if (error) {
             setState('error');
           }
           else {
             setState('connected');
           }
 
           _completion.apply(null, arguments);
         },
 
-        onClose = function onClose (err) {
+        onClose = OT.$.bind(function onClose (err) {
           var reason = this.is('disconnecting') ? 'clientDisconnected' : 'networkDisconnected';
 
           if(err && err.code === 4001) {
             reason = 'networkTimedout';
           }
 
           setState('disconnected');
 
           _dispatcher.onClose(reason);
-
-        }.bind(this),
+        }, this),
 
         onError = function onError () {};
         // @todo what does having an error mean? Are they always fatal? Are we disconnected now?
 
 
     //// Public API
 
     this.connect = function (token, sessionInfo, completion) {
@@ -10016,61 +12625,63 @@ OTHelpers.centerElement = function(eleme
       _sessionId = sessionInfo.sessionId;
       _token = token;
       _completion = completion;
 
       var connectionId = OT.$.uuid(),
           rumorChannel = '/v2/partner/' + OT.APIKEY + '/session/' + _sessionId;
 
       _rumor = new OT.Rumor.Socket(messagingSocketUrl, symphonyUrl);
-      _rumor.onClose = onClose;
-      _rumor.onMessage = _dispatcher.dispatch.bind(_dispatcher);
-
-      _rumor.connect(connectionId, function(error) {
+      _rumor.onClose(onClose);
+      _rumor.onMessage(OT.$.bind(_dispatcher.dispatch, _dispatcher));
+
+      _rumor.connect(connectionId, OT.$.bind(function(error) {
         if (error) {
           error.message = 'WebSocketConnection:' + error.code + ':' + error.message;
           onConnectComplete(error);
           return;
         }
 
         // we do this here to avoid getting connect errors twice
-        _rumor.onError = onError;
+        _rumor.onError(onError);
 
         OT.debug('Raptor Socket connected. Subscribing to ' +
           rumorChannel + ' on ' + messagingSocketUrl);
 
         _rumor.subscribe([rumorChannel]);
 
         //connect to session
-        var connectMessage = OT.Raptor.Message.connections.create(OT.APIKEY, _sessionId, _rumor.id);
-        this.publish(connectMessage, {'X-TB-TOKEN-AUTH': _token}, function(error) {
+        var connectMessage = OT.Raptor.Message.connections.create(OT.APIKEY,
+          _sessionId, _rumor.id());
+        this.publish(connectMessage, {'X-TB-TOKEN-AUTH': _token}, OT.$.bind(function(error) {
           if (error) {
             error.message = 'ConnectToSession:' + error.code +
                 ':Received error response to connection create message.';
             onConnectComplete(error);
             return;
           }
 
           this.publish( OT.Raptor.Message.sessions.get(OT.APIKEY, _sessionId),
             function (error) {
-            if (error) error.message = 'GetSessionState:' + error.code +
-                      ':Received error response to session read';
+            if (error) {
+              error.message = 'GetSessionState:' + error.code +
+                ':Received error response to session read';
+            }
             onConnectComplete.apply(null, arguments);
           });
-        }.bind(this));
-
-      }.bind(this));
-    };
-
-
-    this.disconnect = function () {
+        }, this));
+      }, this));
+    };
+
+
+    this.disconnect = function (drainSocketBuffer) {
       if (this.is('disconnected')) return;
 
       setState('disconnecting');
-      _rumor.disconnect();
+      _rumor.disconnect(drainSocketBuffer);
     };
 
     // Publishs +message+ to the Symphony app server.
     //
     // The completion handler is optional, as is the headers
     // dict, but if you provide the completion handler it must
     // be the last argument.
     //
@@ -10101,17 +12712,17 @@ OTHelpers.centerElement = function(eleme
       if (_completion) _dispatcher.registerCallback(transactionId, _completion);
 
       OT.debug('OT.Raptor.Socket Publish (ID:' + transactionId + ') ');
       OT.debug(message);
 
       _rumor.publish([symphonyUrl], message, OT.$.extend(_headers, {
         'Content-Type': 'application/x-raptor+v2',
         'TRANSACTION-ID': transactionId,
-        'X-TB-FROM-ADDRESS': _rumor.id
+        'X-TB-FROM-ADDRESS': _rumor.id()
       }));
 
       return transactionId;
     };
 
     // Register a new stream against _sessionId
     this.streamCreate = function(name, orientation, encodedWidth, encodedHeight,
       hasAudio, hasVideo, frameRate, minBitrate, maxBitrate, completion) {
@@ -10140,17 +12751,17 @@ OTHelpers.centerElement = function(eleme
 
     this.streamChannelUpdate = function(streamId, channelId, attributes) {
       this.publish( OT.Raptor.Message.streamChannels.update(OT.APIKEY, _sessionId,
         streamId, channelId, attributes) );
     };
 
     this.subscriberCreate = function(streamId, subscriberId, channelsToSubscribeTo, completion) {
       this.publish( OT.Raptor.Message.subscribers.create(OT.APIKEY, _sessionId,
-        streamId, subscriberId, _rumor.id, channelsToSubscribeTo), completion );
+        streamId, subscriberId, _rumor.id(), channelsToSubscribeTo), completion );
     };
 
     this.subscriberDestroy = function(streamId, subscriberId) {
       this.publish( OT.Raptor.Message.subscribers.destroy(OT.APIKEY, _sessionId,
         streamId, subscriberId) );
     };
 
     this.subscriberUpdate = function(streamId, subscriberId, attributes) {
@@ -10200,17 +12811,17 @@ OTHelpers.centerElement = function(eleme
     };
 
     this.jsepAnswerP2p = function(streamId, subscriberId, answerSdp) {
       this.publish( OT.Raptor.Message.subscribers.answer(OT.APIKEY, _sessionId, streamId,
         subscriberId, answerSdp) );
     };
 
     this.signal = function(options, completion) {
-      var signal = new OT.Signal(_sessionId, _rumor.id, options || {});
+      var signal = new OT.Signal(_sessionId, _rumor.id(), options || {});
 
       if (!signal.valid) {
         if (completion && OT.$.isFunction(completion)) {
           completion( new SignalError(signal.error.code, signal.error.reason), signal.toHash() );
         }
 
         return;
       }
@@ -10218,27 +12829,19 @@ OTHelpers.centerElement = function(eleme
       this.publish( signal.toRaptorMessage(), function(err) {
         var error;
         if (err) error = new SignalError(err.code, err.message);
 
         if (completion && OT.$.isFunction(completion)) completion(error, signal.toHash());
       });
     };
 
-    OT.$.defineGetters(this, {
-      id: function() {
-        return _rumor && _rumor.id;
-      },
-      sessionId: function() {
-        return _sessionId;
-      },
-      dispatcher: function() {
-        return _dispatcher;
-      }
-    });
+    this.id = function() {
+      return _rumor && _rumor.id();
+    };
 
     if(dispatcher == null) {
       dispatcher = new OT.Raptor.Dispatcher();
     }
     _dispatcher = dispatcher;
   };
 
 }(this));
@@ -10355,17 +12958,16 @@ OTHelpers.centerElement = function(eleme
       default:
         OT.warn('OT.Raptor.dispatch: Type ' + message.resource + ' is not currently implemented');
     }
   };
 
   OT.Raptor.Dispatcher.prototype.dispatchSession = function (message) {
     switch (message.method) {
       case 'read':
-
         this.emit('session#read', message.content, message.transactionId);
         break;
 
 
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
@@ -10468,17 +13070,16 @@ OTHelpers.centerElement = function(eleme
 
 
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
   OT.Raptor.Dispatcher.prototype.dispatchSubscriber = function (message) {
-
     switch (message.method) {
       case 'created':
         this.emit('subscriber#created', message.params.stream, message.fromAddress,
           message.content.id);
         break;
 
 
       case 'deleted':
@@ -10532,20 +13133,22 @@ OTHelpers.centerElement = function(eleme
   OT.subscribers = new OT.Collection('widgetId');     // Subscribers are id'd by their widgetId
   OT.sessions = new OT.Collection();
 
   function parseStream(dict, session) {
     var channel = dict.channel.map(function(channel) {
       return new OT.StreamChannel(channel);
     });
 
+    var connectionId = dict.connectionId ? dict.connectionId : dict.connection.id;
+
     return  new OT.Stream(  dict.id,
                             dict.name,
                             dict.creationTime,
-                            session.connections.get(dict.connection.id),
+                            session.connections.get(connectionId),
                             session,
                             channel );
   }
 
   function parseAndAddStreamToSession(dict, session) {
     if (session.streams.has(dict.id)) return;
 
     var stream = parseStream(dict, session);
@@ -10576,46 +13179,46 @@ OTHelpers.centerElement = function(eleme
     dispatcher.on('close', function(reason) {
 
       var connection = session.connection;
 
       if (!connection) {
         return;
       }
 
-      if (connection.destroyedReason) {
+      if (connection.destroyedReason()) {
         OT.debug('OT.Raptor.Socket: Socket was closed but the connection had already ' +
-          'been destroyed. Reason: ' + connection.destroyedReason);
+          'been destroyed. Reason: ' + connection.destroyedReason());
         return;
       }
 
       connection.destroy( reason );
 
     });
 
     dispatcher.on('session#read', function(content, transactionId) {
 
       var state = {},
           connection;
 
       state.streams = [];
       state.connections = [];
       state.archives = [];
 
-      content.connection.forEach(function(connectionParams) {
+      OT.$.forEach(content.connection, function(connectionParams) {
         connection = OT.Connection.fromHash(connectionParams);
         state.connections.push(connection);
         session.connections.add(connection);
       });
 
-      content.stream.forEach(function(streamParams) {
+      OT.$.forEach(content.stream, function(streamParams) {
         state.streams.push( parseAndAddStreamToSession(streamParams, session) );
       });
-      
-      (content.archive || content.archives).forEach(function(archiveParams) {
+
+      OT.$.forEach(content.archive || content.archives, function(archiveParams) {
         state.archives.push( parseAndAddArchiveToSession(archiveParams, session) );
       });
 
       session._.subscriberMap = {};
 
       dispatcher.triggerCallback(transactionId, null, state);
     });
 
@@ -10630,17 +13233,17 @@ OTHelpers.centerElement = function(eleme
       connection = session.connections.get(connection);
       connection.destroy(reason);
     });
 
     dispatcher.on('stream#created', function(stream, transactionId) {
       stream = parseAndAddStreamToSession(stream, session);
 
       if (stream.publisher) {
-        stream.publisher.stream = stream;
+        stream.publisher.setStream(stream);
       }
 
       dispatcher.triggerCallback(transactionId, null, stream);
     });
 
     dispatcher.on('stream#deleted', function(streamId, reason) {
       var stream = session.streams.get(streamId);
 
@@ -10709,17 +13312,16 @@ OTHelpers.centerElement = function(eleme
           break;
 
 
         // Messages for Publishers
         case 'answer':
         case 'pranswer':
         case 'generateoffer':
         case 'unsubscribe':
-          console.warn('generateoffer maybe?');
           actors = OT.publishers.where({streamId: streamId});
           break;
 
 
         // Messages for Publishers and Subscribers
         case 'candidate':
           // send to whichever of your publisher or subscribers are
           // subscribing/publishing that stream
@@ -10738,37 +13340,37 @@ OTHelpers.centerElement = function(eleme
 
       // This is a bit hacky. We don't have the session in the message so we iterate
       // until we find the actor that the message relates to this stream, and then
       // we grab the session from it.
       fromConnection = actors[0].session.connections.get(fromAddress);
       if(!fromConnection && fromAddress.match(/^symphony\./)) {
         fromConnection = OT.Connection.fromHash({
           id: fromAddress,
-          creationTime: Date.now()
+          creationTime: Math.floor(OT.$.now())
         });
 
         actors[0].session.connections.add(fromConnection);
       } else if(!fromConnection) {
         OT.warn('OT.Raptor.dispatch: Messsage comes from a connection (' +
           fromAddress + ') that we do not know about. The message was ignored.');
         return;
       }
 
-      actors.forEach(function(actor) {
+      OT.$.forEach(actors, function(actor) {
         actor.processMessage(method, fromConnection, message);
       });
     };
 
-    dispatcher.on('jsep#offer', jsepHandler.bind(null, 'offer'));
-    dispatcher.on('jsep#answer', jsepHandler.bind(null, 'answer'));
-    dispatcher.on('jsep#pranswer', jsepHandler.bind(null, 'pranswer'));
-    dispatcher.on('jsep#generateoffer', jsepHandler.bind(null, 'generateoffer'));
-    dispatcher.on('jsep#unsubscribe', jsepHandler.bind(null, 'unsubscribe'));
-    dispatcher.on('jsep#candidate', jsepHandler.bind(null, 'candidate'));
+    dispatcher.on('jsep#offer', OT.$.bind(jsepHandler, null, 'offer'));
+    dispatcher.on('jsep#answer', OT.$.bind(jsepHandler, null, 'answer'));
+    dispatcher.on('jsep#pranswer', OT.$.bind(jsepHandler, null, 'pranswer'));
+    dispatcher.on('jsep#generateoffer', OT.$.bind(jsepHandler, null, 'generateoffer'));
+    dispatcher.on('jsep#unsubscribe', OT.$.bind(jsepHandler, null, 'unsubscribe'));
+    dispatcher.on('jsep#candidate', OT.$.bind(jsepHandler, null, 'candidate'));
 
     dispatcher.on('subscriberChannel#updated', function(streamId, channelId, content) {
 
       if (!streamId || !session.streams.has(streamId)) {
         OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' +
           'exist, for subscriberChannel#updated message!');
         // @todo error
         return;
@@ -10851,107 +13453,143 @@ OTHelpers.centerElement = function(eleme
       archive._.update(update);
     });
 
     return dispatcher;
 
   };
 
 })(window);
-!(function(window) {
+!(function() {
 
   // Helper to synchronise several startup tasks and then dispatch a unified
   // 'envLoaded' event.
   //
   // This depends on:
   // * OT
   // * OT.Config
   //
   function EnvironmentLoader() {
     var _configReady = false,
-        _domReady = false,
+
+        // If the plugin is installed, then we should wait for it to
+        // be ready as well.
+        _pluginSupported = TBPlugin.isSupported(),
+        _pluginLoadAttemptComplete = _pluginSupported ? TBPlugin.isReady() : true,
 
         isReady = function() {
-          return _domReady && _configReady;
+          return !OT.$.isDOMUnloaded() && OT.$.isReady() &&
+                      _configReady && _pluginLoadAttemptComplete;
         },
 
         onLoaded = function() {
           if (isReady()) {
             OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_LOADED));
           }
         },
 
+
         onDomReady = function() {
-          _domReady = true;
-
-          // This is making an assumption about there being only one "window"
-          // that we care about.
-          OT.$.on(window, 'unload', function() {
-            OT.publishers.destroy();
-            OT.subscribers.destroy();
-            OT.sessions.destroy();
-          });
+          OT.$.onDOMUnload(onDomUnload);
 
           // The Dynamic Config won't load until the DOM is ready
           OT.Config.load(OT.properties.configURL);
 
           onLoaded();
         },
 
+        onDomUnload = function() {
+          // Disconnect the session first, this will prevent the plugin
+          // from locking up during browser unload.
+          // if (_pluginSupported) {
+          //   var sessions = OT.sessions.where();
+          //   for (var i=0; i<sessions.length; ++i) {
+          //     sessions[i].disconnect(false);
+          //   }
+          // }
+
+          OT.publishers.destroy();
+          OT.subscribers.destroy();
+          OT.sessions.destroy('unloaded');
+
+          OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_UNLOADED));
+        },
+
+        onPluginReady = function(err) {
+          // We mark the plugin as ready so as not to stall the environment
+          // loader. In this case though, TBPlugin is not supported.
+          _pluginLoadAttemptComplete = true;
+
+          if (err) {
+            OT.debug('TB Plugin failed to load or was not installed');
+          }
+
+          onLoaded();
+        },
+
         configLoaded = function() {
           _configReady = true;
           OT.Config.off('dynamicConfigChanged', configLoaded);
           OT.Config.off('dynamicConfigLoadFailed', configLoadFailed);
 
           onLoaded();
         },
 
         configLoadFailed = function() {
           configLoaded();
         };
 
+
     OT.Config.on('dynamicConfigChanged', configLoaded);
     OT.Config.on('dynamicConfigLoadFailed', configLoadFailed);
-    if (document.readyState === 'complete' ||
-      (document.readyState === 'interactive' && document.body)) {
-      onDomReady();
-    } else {
-      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();
-        });
-      }
-    }
-
-    this.onLoad = function(cb) {
+
+    OT.$.onDOMLoad(onDomReady);
+
+    // If the plugin should work on this platform then
+    // see if it loads.
+    if (_pluginSupported) TBPlugin.ready(onPluginReady);
+
+    this.onLoad = function(cb, context) {
       if (isReady()) {
-        cb();
-        return;
-      }
-
-      OT.on(OT.Event.names.ENV_LOADED, cb);
+        cb.call(context);
+        return;
+      }
+
+      OT.on(OT.Event.names.ENV_LOADED, cb, context);
+    };
+
+    this.onUnload = function(cb, context) {
+      if (this.isUnloaded()) {
+        cb.call(context);
+        return;
+      }
+
+      OT.on(OT.Event.names.ENV_UNLOADED, cb, context);
+    };
+
+    this.isUnloaded = function() {
+      return OT.$.isDOMUnloaded();
     };
   }
 
   var EnvLoader = new EnvironmentLoader();
 
   OT.onLoad = function(cb, context) {
-    if (!context) {
-      EnvLoader.onLoad(cb);
-    } else {
-      EnvLoader.onLoad(
-        cb.bind(context)
-      );
-    }
-  };
-
-})(window);
+    EnvLoader.onLoad(cb, context);
+  };
+
+  OT.onUnload = function(cb, context) {
+    EnvLoader.onUnload(cb, context);
+  };
+
+  OT.isUnloaded = function() {
+    return EnvLoader.isUnloaded();
+  };
+
+})();
 !(function() {
 
   /**
    * The Error class is used to define the error object passed into completion handlers.
    * Each of the following methods, which execute asynchronously, includes a
    * <code>completionHandler</code> parameter:
    *
    * <ul>
@@ -11230,17 +13868,17 @@ OTHelpers.centerElement = function(eleme
     var context,
         session = options.session;
 
     if (session) {
       context = {
         sessionId: session.sessionId
       };
 
-      if (session.connected) context.connectionId = session.connection.connectionId;
+      if (session.isConnected()) context.connectionId = session.connection.connectionId;
       if (!options.target) options.target = session;
 
     } else if (options.sessionId) {
       context = {
         sessionId: options.sessionId
       };
 
       if (!options.target) options.target = null;
@@ -11343,42 +13981,39 @@ OTHelpers.centerElement = function(eleme
     this.creationTime = creationTime ? Number(creationTime) : null;
     this.data = data;
     this.capabilities = new OT.ConnectionCapabilities(capabilitiesHash);
     this.permissions = new OT.Capabilities(permissionsHash);
     this.quality = null;
 
     OT.$.eventing(this);
 
-    this.destroy = function(reason, quiet) {
+    this.destroy = OT.$.bind(function(reason, quiet) {
       destroyedReason = reason || 'clientDisconnected';
 
       if (quiet !== true) {
         this.dispatchEvent(
           new OT.DestroyedEvent(
             'destroyed',      // This should be OT.Event.names.CONNECTION_DESTROYED, but
                               // the value of that is currently shared with Session
             this,
             destroyedReason
           )
         );
       }
-    }.bind(this);
-
-    Object.defineProperties(this, {
-      destroyed: {
-        get: function() { return destroyedReason !== void 0; },
-        enumerable: true
-      },
-
-      destroyedReason: {
-        get: function() { return destroyedReason; },
-        enumerable: true
-      }
-    });
+    }, this);
+
+    this.destroyed = function() {
+      return destroyedReason !== void 0;
+    };
+
+    this.destroyedReason = function() {
+      return destroyedReason;
+    };
+
   };
 
   OT.Connection.fromHash = function(hash) {
     return new OT.Connection(hash.id,
                              hash.creationTime,
                              hash.data,
                              OT.$.extend(hash.capablities || {}, { supportsWebRTC: true }),
                              hash.permissions || [] );
@@ -11445,17 +14080,17 @@ OTHelpers.centerElement = function(eleme
             OT.warn('Tried to update unknown key ' + key + ' on ' + this.type +
               ' channel ' + this.id);
             return;
         }
 
         this.trigger('update', this, key, oldValue, this[key]);
       }
 
-      if (Object.keys(videoDimensions).length) {
+      if (OT.$.keys(videoDimensions).length) {
         // To make things easier for the public API, we broadcast videoDimensions changes,
         // which is an aggregate of width, height, and orientation changes.
         this.trigger('update', this, 'videoDimensions', oldVideoDimensions, videoDimensions);
       }
 
       return true;
     };
   };
@@ -11530,21 +14165,20 @@ OTHelpers.centerElement = function(eleme
 
     this.id = this.streamId = id;
     this.name = name;
     this.creationTime = Number(creationTime);
 
     this.connection = connection;
     this.channel = channel;
     this.publisher = OT.publishers.find({streamId: this.id});
-    this.publisherId = this.publisher ? this.publisher.id : null;
 
     OT.$.eventing(this);
 
-    var onChannelUpdate = function(channel, key, oldValue, newValue) {
+    var onChannelUpdate = OT.$.bind(function(channel, key, oldValue, newValue) {
       var _key = key;
 
       switch(_key) {
         case 'active':
           _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
           this[_key] = newValue;
           break;
 
@@ -11557,32 +14191,32 @@ OTHelpers.centerElement = function(eleme
             orientation: channel.orientation
           };
 
           // We dispatch this via the videoDimensions key instead
           return;
       }
 
       this.dispatchEvent( new OT.StreamUpdatedEvent(this, _key, oldValue, newValue) );
-    }.bind(this);
-
-    var associatedWidget = function() {
+    }, this);
+
+    var associatedWidget = OT.$.bind(function() {
       if(this.publisher) {
         return this.publisher;
       } else {
         return OT.subscribers.find(function(subscriber) {
-          return subscriber.streamId === this.id &&
+          return subscriber.stream.id === this.id &&
             subscriber.session.id === session.id;
         });
       }
-    }.bind(this);
+    }, this);
 
     // Returns all channels that have a type of +type+.
     this.getChannelsOfType = function (type) {
-      return this.channel.filter(function(channel) {
+      return OT.$.filter(this.channel, function(channel) {
         return channel.type === type;
       });
     };
 
     this.getChannel = function (id) {
       for (var i=0; i<this.channel.length; ++i) {
         if (this.channel[i].id === id) return this.channel[i];
       }
@@ -11606,16 +14240,17 @@ OTHelpers.centerElement = function(eleme
 
     this.videoDimensions = {};
     if (videoChannel) {
       this.videoDimensions.width = videoChannel.width;
       this.videoDimensions.height = videoChannel.height;
       this.videoDimensions.orientation = videoChannel.orientation;
 
       videoChannel.on('update', onChannelUpdate);
+      this.frameRate = videoChannel.frameRate;
     }
 
     if (audioChannel) {
       audioChannel.on('update', onChannelUpdate);
     }
 
     this.setChannelActiveState = function(channelType, activeState, activeReason) {
       var attributes = {
@@ -11628,81 +14263,69 @@ OTHelpers.centerElement = function(eleme
     };
 
     this.setRestrictFrameRate = function(restrict) {
       updateChannelsOfType('video', {
         restrictFrameRate: restrict
       });
     };
 
-    var updateChannelsOfType = function(channelType, attributes) {
+    var updateChannelsOfType = OT.$.bind(function(channelType, attributes) {
       var setChannelActiveState;
       if (!this.publisher) {
         var subscriber = OT.subscribers.find(function(subscriber) {
-          return subscriber.streamId === this.id &&
+          return subscriber.stream.id === this.id &&
             subscriber.session.id === session.id;
         }, this);
 
         setChannelActiveState = function(channel) {
           session._.subscriberChannelUpdate(this, subscriber, channel, attributes);
         };
       } else {
         setChannelActiveState = function(channel) {
           session._.streamChannelUpdate(this, channel, attributes);
         };
       }
 
-      this.getChannelsOfType(channelType).forEach(setChannelActiveState.bind(this));
-    }.bind(this);
-
+      OT.$.forEach(this.getChannelsOfType(channelType), OT.$.bind(setChannelActiveState, this));
+    }, this);
+
+    this.destroyed = false;
+    this.destroyedReason = void 0;
+ 
     this.destroy = function(reason, quiet) {
       destroyedReason = reason || 'clientDisconnected';
+      this.destroyed = true;
+      this.destroyedReason = destroyedReason;
 
       if (quiet !== true) {
         this.dispatchEvent(
           new OT.DestroyedEvent(
             'destroyed',      // This should be OT.Event.names.STREAM_DESTROYED, but
                               // the value of that is currently shared with Session
             this,
             destroyedReason
           )
         );
       }
     };
-
-    Object.defineProperties(this, {
-      destroyed: {
-        get: function() { return destroyedReason !== void 0; },
-        enumerable: true
-      },
-
-      destroyedReason: {
-        get: function() { return destroyedReason; },
-        enumerable: true
-      },
-
-      frameRate: {
-        get: function() { return this.getChannelsOfType('video')[0].frameRate; },
-        enumerable: true
-      }
-    });
-
+    
     /// PRIVATE STUFF CALLED BY Raptor.Dispatcher
 
     // Confusingly, this should not be called when you want to change
     // the stream properties. This is used by Raptor dispatch to notify
     // the stream that it's properies have been successfully updated
     //
     // @todo make this sane. Perhaps use setters for the properties that can
     // send the appropriate Raptor message. This would require that Streams
     // have access to their session.
     //
     this._ = {};
-    this._.updateProperty = function (key, value) {
-      if (validPropertyNames.indexOf(key) === -1) {
+    this._.updateProperty = OT.$.bind(function(key, value) {
+      if (OT.$.arrayIndexOf(validPropertyNames, key) === -1) {
         OT.warn('Unknown stream property "' + key + '" was modified to "' + value + '".');
         return;
       }
 
       var oldValue = this[key],
           newValue = value;
 
       switch(key) {
@@ -11716,73 +14339,81 @@ OTHelpers.centerElement = function(eleme
             widget._.archivingStatus(newValue);
           }
           this[key] = newValue;
           break;
       }
 
       var event = new OT.StreamUpdatedEvent(this, key, oldValue, newValue);
       this.dispatchEvent(event);
-    }.bind(this);
+    }, this);
 
     // Mass update, called by Raptor.Dispatcher
-    this._.update = function (attributes) {
+    this._.update = OT.$.bind(function(attributes) {
       for (var key in attributes) {
         if(!attributes.hasOwnProperty(key)) {
           continue;
         }
         this._.updateProperty(key, attributes[key]);
       }
-    }.bind(this);
-
-    this._.updateChannel = function (channelId, attributes) {
+    }, this);
+
+    this._.updateChannel = OT.$.bind(function(channelId, attributes) {
       this.getChannel(channelId).update(attributes);
-    }.bind(this);
+    }, this);
   };
 
 })(window);
 !(function() {
-  
+
 
   OT.Archive = function(id, name, status) {
-    
     this.id = id;
     this.name = name;
     this.status = status;
-    
+
     this._ = {};
 
     OT.$.eventing(this);
-    
+
     // Mass update, called by Raptor.Dispatcher
-    this._.update = function (attributes) {
+    this._.update = OT.$.bind(function (attributes) {
       for (var key in attributes) {
         if(!attributes.hasOwnProperty(key)) {
           continue;
         }
         var oldValue = this[key];
         this[key] = attributes[key];
-        
+
         var event = new OT.ArchiveUpdatedEvent(this, key, oldValue, this[key]);
         this.dispatchEvent(event);
-        
-      }
-    }.bind(this);
+      }
+    }, this);
 
     this.destroy = function() {};
 
   };
 
 })(window);
 !(function(window) {
 
-  // order is very important: "RTCSessionDescription" defined in Firefox Nighly but useless
-  var NativeRTCSessionDescription = (window.mozRTCSessionDescription ||
-    window.RTCSessionDescription);
-  var NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
+  // Normalise these
+  var NativeRTCSessionDescription,
+      NativeRTCIceCandidate;
+
+  if (!TBPlugin.isInstalled()) {
+    // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless
+    NativeRTCSessionDescription = (window.mozRTCSessionDescription ||
+                                   window.RTCSessionDescription);
+    NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
+  }
+  else {
+    NativeRTCSessionDescription = TBPlugin.RTCSessionDescription;
+    NativeRTCIceCandidate = TBPlugin.RTCIceCandidate;
+  }
 
   // Helper function to forward Ice Candidates via +messageDelegate+
   var iceCandidateForwarder = function(messageDelegate) {
     return function(event) {
       if (event.candidate) {
         messageDelegate(OT.Raptor.Actions.CANDIDATE, event.candidate);
       } else {
         OT.debug('IceCandidateForwarder: No more ICE candidates.');
@@ -11798,29 +14429,26 @@ OTHelpers.centerElement = function(eleme
   //
   // @example
   //
   //  var iceProcessor = new IceCandidateProcessor();
   //  iceProcessor.process(iceMessage1);
   //  iceProcessor.process(iceMessage2);
   //  iceProcessor.process(iceMessage3);
   //
-  //  iceProcessor.peerConnection = peerConnection;
+  //  iceProcessor.setPeerConnection(peerConnection);
   //  iceProcessor.processPending();
   //
   var IceCandidateProcessor = function() {
     var _pendingIceCandidates = [],
         _peerConnection = null;
 
-
-    Object.defineProperty(this, 'peerConnection', {
-      set: function(peerConnection) {
-        _peerConnection = peerConnection;
-      }
-    });
+    this.setPeerConnection = function(peerConnection) {
+      _peerConnection = peerConnection;
+    };
 
     this.process = function(message) {
       var iceCandidate = new NativeRTCIceCandidate(message.content);
 
       if (_peerConnection) {
         _peerConnection.addIceCandidate(iceCandidate);
       } else {
         _pendingIceCandidates.push(iceCandidate);
@@ -11846,17 +14474,17 @@ OTHelpers.centerElement = function(eleme
         sdpLines,
         match;
 
     // Icky code. This filter operation has two side effects in addition
     // to doing the actual filtering:
     //   1. extract all the payload types from the rtpmap CN lines
     //   2. find the index of the audio media line
     //
-    sdpLines = sdp.split('\r\n').filter(function(line, index) {
+    sdpLines = OT.$.filter(sdp.split('\r\n'), function(line, index) {
       if (line.indexOf('m=audio') !== -1) audioMediaLineIndex = index;
 
       match = line.match(matcher);
       if (match !== null) {
         payloadTypes.push(match[1]);
 
         // remove this line as it contains CN
         return false;
@@ -12018,104 +14646,159 @@ OTHelpers.centerElement = function(eleme
    * Responsible for:
    * * offer-answer exchange
    * * iceCandidates
    * * notification of remote streams being added/removed
    *
    */
   OT.PeerConnection = function(config) {
     var _peerConnection,
+        _peerConnectionCompletionHandlers = [],
         _iceProcessor = new IceCandidateProcessor(),
         _offer,
         _answer,
         _state = 'new',
-        _messageDelegates = [],
-        _gettingStats,
-        _createTime = OT.$.now();
+        _messageDelegates = [];
+
 
     OT.$.eventing(this);
 
     // if ice servers doesn't exist Firefox will throw an exception. Chrome
-    // interprets this as "Use my default STUN servers" whereas FF reads it
-    // as "Don't use STUN at all". *Grumble*
+    // interprets this as 'Use my default STUN servers' whereas FF reads it
+    // as 'Don't use STUN at all'. *Grumble*
     if (!config.iceServers) config.iceServers = [];
 
     // Private methods
-    var delegateMessage = function(type, messagePayload) {
+    var delegateMessage = OT.$.bind(function(type, messagePayload) {
           if (_messageDelegates.length) {
             // We actually only ever send to the first delegate. This is because
             // each delegate actually represents a Publisher/Subscriber that
             // shares a single PeerConnection. If we sent to all delegates it
             // would result in each message being processed multiple times by
             // each PeerConnection.
             _messageDelegates[0](type, messagePayload);
           }
-        }.bind(this),
-
-        setupPeerConnection = function() {
-          if (!_peerConnection) {
-            try {
-              OT.debug('Creating peer connection config "' + JSON.stringify(config) + '".');
-              if (!config.iceServers || config.iceServers.length === 0) {
-                // This should never happen unless something is misconfigured
-                OT.error('No ice servers present');
+        }, this),
+
+        // Create and initialise the PeerConnection object. This deals with
+        // any differences between the various browser implementations and
+        // our own TBPlugin version.
+        //
+        // +completion+ is the function is call once we've either successfully
+        // created the PeerConnection or on failure.
+        //
+        // +localWebRtcStream+ will be null unless the callee is representing
+        // a publisher. This is an unfortunate implementation limitation
+        // of TBPlugin, it's not used for vanilla WebRTC. Hopefully this can
+        // be tidied up later.
+        //
+        createPeerConnection = OT.$.bind(function (completion, localWebRtcStream) {
+          if (_peerConnection) {
+            completion.call(null, null, _peerConnection);
+            return;
+          }
+
+          _peerConnectionCompletionHandlers.push(completion);
+
+          if (_peerConnectionCompletionHandlers.length > 1) {
+            // The PeerConnection is already being setup, just wait for
+            // it to be ready.
+            return;
+          }
+
+          var pcConstraints = {
+            optional: [
+              {DtlsSrtpKeyAgreement: true}
+            ]
+          };
+
+          OT.debug('Creating peer connection config "' + JSON.stringify(config) + '".');
+
+          if (!config.iceServers || config.iceServers.length === 0) {
+            // This should never happen unless something is misconfigured
+            OT.error('No ice servers present');
+          }
+
+          OT.$.createPeerConnection(config, pcConstraints, localWebRtcStream,
+                                    OT.$.bind(attachEventsToPeerConnection, this));
+        }, this),
+
+        // An auxiliary function to createPeerConnection. This binds the various event callbacks
+        // once the peer connection is created.
+        //
+        // +err+ will be non-null if an err occured while creating the PeerConnection
+        // +pc+ will be the PeerConnection object itself.
+        //
+        attachEventsToPeerConnection = OT.$.bind(function(err, pc) {
+          if (err) {
+            triggerError('Failed to create PeerConnection, exception: ' +
+                err.toString(), 'NewPeerConnection');
+
+            _peerConnectionCompletionHandlers = [];
+            return;
+          }
+
+          OT.debug('OT attachEventsToPeerConnection');
+          _peerConnection = pc;
+
+          _peerConnection.onicecandidate = iceCandidateForwarder(delegateMessage);
+          _peerConnection.onaddstream = OT.$.bind(onRemoteStreamAdded, this);
+          _peerConnection.onremovestream = OT.$.bind(onRemoteStreamRemoved, this);
+
+          if (_peerConnection.onsignalingstatechange !== undefined) {
+            _peerConnection.onsignalingstatechange = OT.$.bind(routeStateChanged, this);
+          } else if (_peerConnection.onstatechange !== undefined) {
+            _peerConnection.onstatechange = OT.$.bind(routeStateChanged, this);
+          }
+
+          if (_peerConnection.oniceconnectionstatechange !== undefined) {
+            var failedStateTimer;
+            _peerConnection.oniceconnectionstatechange = function (event) {
+              if (event.target.iceConnectionState === 'failed') {
+                if (failedStateTimer) {
+                  clearTimeout(failedStateTimer);
+                }
+                // We wait 5 seconds and make sure that it's still in the failed state
+                // before we trigger the error. This is because we sometimes see
+                // 'failed' and then 'connected' afterwards.
+                setTimeout(function () {
+                  if (event.target.iceConnectionState === 'failed') {
+                    triggerError('The stream was unable to connect due to a network error.' +
+                     ' Make sure your connection isn\'t blocked by a firewall.', 'ICEWorkflow');
+                  }
+                }, 5000);
               }
-              _peerConnection = OT.$.createPeerConnection(config, {
-                optional: [
-                  { DtlsSrtpKeyAgreement: true }
-                ]
-              });
-            } catch(e) {
-              triggerError('Failed to create PeerConnection, exception: ' +
-                  e.message, 'NewPeerConnection');
-              return null;
-            }
-
-            _peerConnection.onicecandidate = iceCandidateForwarder(delegateMessage);
-            _peerConnection.onaddstream = onRemoteStreamAdded.bind(this);
-            _peerConnection.onremovestream = onRemoteStreamRemoved.bind(this);
-
-            if (_peerConnection.onsignalingstatechange !== undefined) {
-              _peerConnection.onsignalingstatechange = routeStateChanged.bind(this);
-            } else if (_peerConnection.onstatechange !== undefined) {
-              _peerConnection.onstatechange = routeStateChanged.bind(this);
-            }
-            
-            if (_peerConnection.oniceconnectionstatechange !== undefined) {
-              var failedStateTimer;
-              _peerConnection.oniceconnectionstatechange = function (event) {
-                if (event.target.iceConnectionState === 'failed') {
-                  if (failedStateTimer) {
-                    clearTimeout(failedStateTimer);
-                  }
-                  // We wait 5 seconds and make sure that it's still in the failed state
-                  // before we trigger the error. This is because we sometimes see
-                  // 'failed' and then 'connected' afterwards.
-                  setTimeout(function () {
-                    if (event.target.iceConnectionState === 'failed') {
-                      triggerError('The stream was unable to connect due to a network error.' +
-                       ' Make sure your connection isn\'t blocked by a firewall.', 'ICEWorkflow');
-                    }
-                  }, 5000);
-                }
-              };
-            }
-          }
-
-          return _peerConnection;
-        }.bind(this),
+            };
+          }
+
+          triggerPeerConnectionCompletion(null);
+        }, this),
+
+        triggerPeerConnectionCompletion = function () {
+          while (_peerConnectionCompletionHandlers.length) {
+            _peerConnectionCompletionHandlers.shift().call(null);
+          }
+        },
 
         // Clean up the Peer Connection and trigger the close event.
         // This function can be called safely multiple times, it will
         // only trigger the close event once (per PeerConnection object)
         tearDownPeerConnection = function() {
           // Our connection is dead, stop processing ICE candidates
-          if (_iceProcessor) _iceProcessor.peerConnection = null;
+          if (_iceProcessor) _iceProcessor.setPeerConnection(null);
+
+          qos.stopCollecting();
 
           if (_peerConnection !== null) {
+            if (_peerConnection.destroy) {
+              // OTPlugin defines a destroy method on PCs. This allows
+              // the plugin to release any resources that it's holding.
+              _peerConnection.destroy();
+            }
+
             _peerConnection = null;
             this.trigger('close');
           }
         },
 
         routeStateChanged = function(event) {
           var newState;
 
@@ -12127,42 +14810,45 @@ OTHelpers.centerElement = function(eleme
             // The slightly older version
             newState = event.target.signalingState;
 
           } else {
             // At least six months old version. Positively ancient, yeah?
             newState = event.target.readyState;
           }
 
-          OT.debug('PeerConnection.stateChange: ' + newState);
           if (newState && newState.toLowerCase() !== _state) {
             _state = newState.toLowerCase();
             OT.debug('PeerConnection.stateChange: ' + _state);
 
             switch(_state) {
               case 'closed':
                 tearDownPeerConnection.call(this);
                 break;
             }
           }
         },
 
+        qosCallback = OT.$.bind(function(parsedStats) {
+          this.trigger('qos', parsedStats);
+        }, this),
+
         getRemoteStreams = function() {
           var streams;
 
           if (_peerConnection.getRemoteStreams) {
             streams = _peerConnection.getRemoteStreams();
           } else if (_peerConnection.remoteStreams) {
             streams = _peerConnection.remoteStreams;
           } else {
             throw new Error('Invalid Peer Connection object implements no ' +
               'method for retrieving remote streams');
           }
 
-          // Force streams to be an Array, rather than a "Sequence" object,
+          // Force streams to be an Array, rather than a 'Sequence' object,
           // which is browser dependent and does not behaviour like an Array
           // in every case.
           return Array.prototype.slice.call(streams);
         },
 
         /// PeerConnection signaling
         onRemoteStreamAdded = function(event) {
           this.trigger('streamAdded', event.stream);
@@ -12176,40 +14862,43 @@ OTHelpers.centerElement = function(eleme
 
 
         // Relays a SDP payload (+sdp+), that is part of a message of type +messageType+
         // via the registered message delegators
         relaySDP = function(messageType, sdp) {
           delegateMessage(messageType, sdp);
         },
 
+
         // Process an offer that
         processOffer = function(message) {
           var offer = new NativeRTCSessionDescription({type: 'offer', sdp: message.content.sdp}),
 
               // Relays +answer+ Answer
               relayAnswer = function(answer) {
-                _iceProcessor.peerConnection = _peerConnection;
+                _iceProcessor.setPeerConnection(_peerConnection);
                 _iceProcessor.processPending();
                 relaySDP(OT.Raptor.Actions.ANSWER, answer);
+
+                qos.startCollecting(_peerConnection);
               },
 
               reportError = function(message, errorReason, prefix) {
                 triggerError('PeerConnection.offerProcessor ' + message + ': ' +
                   errorReason, prefix);
               };
 
-          setupPeerConnection();
-
-          offerProcessor(
-            _peerConnection,
-            offer,
-            relayAnswer,
-            reportError
-          );
+          createPeerConnection(function() {
+            offerProcessor(
+              _peerConnection,
+              offer,
+              relayAnswer,
+              reportError
+            );
+          });
         },
 
         processAnswer = function(message) {
           if (!message.content.sdp) {
             OT.error('PeerConnection.processMessage: Weird answer message, no SDP.');
             return;
           }
 
@@ -12218,50 +14907,60 @@ OTHelpers.centerElement = function(eleme
           _peerConnection.setRemoteDescription(_answer,
               function () {
                 OT.debug('setRemoteDescription Success');
               }, function (errorReason) {
                 triggerError('Error while setting RemoteDescription ' + errorReason,
                   'SetRemoteDescription');
               });
 
-          _iceProcessor.peerConnection = _peerConnection;
+          _iceProcessor.setPeerConnection(_peerConnection);
           _iceProcessor.processPending();
+
+          qos.startCollecting(_peerConnection);
         },
 
         processSubscribe = function() {
           OT.debug('PeerConnection.processSubscribe: Sending offer to subscriber.');
 
-          setupPeerConnection();
-
-          suscribeProcessor(
-            _peerConnection,
-
-            // Success: Relay Offer
-            function(offer) {
-              _offer = offer;
-              relaySDP(OT.Raptor.Actions.OFFER, _offer);
-            },
-
-            // Failure
-            function(message, errorReason, prefix) {
-              triggerError('PeerConnection.suscribeProcessor ' + message + ': ' +
-                errorReason, prefix);
-            }
-          );
-        },
-
-        triggerError = function(errorReason, prefix) {
+          if (!_peerConnection) {
+            // TODO(rolly) I need to examine whether this can
+            // actually happen. If it does happen in the short
+            // term, I want it to be noisy.
+            throw new Error('PeerConnection broke!');
+          }
+
+          createPeerConnection(function() {
+            suscribeProcessor(
+              _peerConnection,
+
+              // Success: Relay Offer
+              function(offer) {
+                _offer = offer;
+                relaySDP(OT.Raptor.Actions.OFFER, _offer);
+              },
+
+              // Failure
+              function(message, errorReason, prefix) {
+                triggerError('PeerConnection.suscribeProcessor ' + message + ': ' +
+                  errorReason, prefix);
+              }
+            );
+          });
+        },
+
+        triggerError = OT.$.bind(function(errorReason, prefix) {
           OT.error(errorReason);
           this.trigger('error', errorReason, prefix);
-        }.bind(this);
+        }, this);
 
     this.addLocalStream = function(webRTCStream) {
-      setupPeerConnection();
-      _peerConnection.addStream(webRTCStream);
+      createPeerConnection(function() {
+        _peerConnection.addStream(webRTCStream);
+      }, webRTCStream);
     };
 
     this.disconnect = function() {
       _iceProcessor = null;
 
       if (_peerConnection) {
         var currentState = (_peerConnection.signalingState || _peerConnection.readyState);
         if (currentState && currentState.toLowerCase() !== 'closed') _peerConnection.close();
@@ -12273,16 +14972,17 @@ OTHelpers.centerElement = function(eleme
       }
 
       this.off();
     };
 
     this.processMessage = function(type, message) {
       OT.debug('PeerConnection.processMessage: Received ' +
         type + ' from ' + message.fromAddress);
+
       OT.debug(message);
 
       switch(type) {
         case 'generateoffer':
           processSubscribe.call(this, message);
           break;
 
         case 'offer':
@@ -12300,223 +15000,390 @@ OTHelpers.centerElement = function(eleme
 
         default:
           OT.debug('PeerConnection.processMessage: Received an unexpected message of type ' +
             type + ' from ' + message.fromAddress + ': ' + JSON.stringify(message));
       }
 
       return this;
     };
-    
+
     this.setIceServers = function (iceServers) {
       if (iceServers) {
         config.iceServers = iceServers;
       }
     };
 
     this.registerMessageDelegate = function(delegateFn) {
       return _messageDelegates.push(delegateFn);
     };
 
     this.unregisterMessageDelegate = function(delegateFn) {
-      var index = _messageDelegates.indexOf(delegateFn);
+      var index = OT.$.arrayIndexOf(_messageDelegates, delegateFn);
 
       if ( index !== -1 ) {
         _messageDelegates.splice(index, 1);
       }
       return _messageDelegates.length;
     };
 
-    /**
-     * Retrieves the PeerConnection stats.
-     *
-     * TODO document what the format of the final reports that +callback+ gets is
-     *
-     * @ignore
-     * @private
-     * @memberof PeerConnection
-     * @param callback {Function} this will be triggered once the stats a are ready.