Merge m-c to inbound on a CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Fri, 08 Aug 2014 14:15:34 -0700
changeset 198645 c176d84866c5aff8dea0d5cd6af4081b1ce4cd9d
parent 198552 7d99b4caf0424c5933d8a22ccb175f7ff6736f66 (current diff)
parent 198644 1d6500527f66a4cb61c549ff23a363f4d69d0ff0 (diff)
child 198646 a6424bfa8f39b4405846fd8b2600319e125f3237
push id47429
push userkwierso@gmail.com
push dateFri, 08 Aug 2014 21:15:46 +0000
treeherdermozilla-inbound@c176d84866c5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound on a CLOSED TREE
configure.in
content/media/gmp/GMPChild.cpp
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/TabParent.cpp
testing/marionette/client/marionette/runner/base.py
xpcom/ds/CharTokenizer.h
--- 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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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"/>
@@ -140,13 +140,13 @@
   <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/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_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": "41b3413a893fef684b380bb344f9d4a5f491f858", 
+    "revision": "8d2554880efe6011591aa8055e3cc8989559ce0a", 
     "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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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="c45627132ae7f00026e361a14d5d084a1236af24"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="b0b5df8c194be48fc8a2f8d1c4310c36388eec9e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="2d2475b521351e200136e463358e6c8e91957702"/>
+  <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/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/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -7,32 +7,38 @@
 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");
+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() {
@@ -234,17 +240,41 @@ function injectLoopAPI(targetWindow) {
      * @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/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/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.
-     * It takes a single argument which is the stats report, which may be undefined
-     * if there is presently no stats.
-     */
-    this.getStats = function(prevStats, callback) {
-      var parsedStats = {},
-          now,
-          timeDifference,
-          parseAvgVideoBitrate,
-          parseAvgAudioBitrate,
-          parseFrameRate,
-          parseStatsReports,
-          parseStats,
-          needsNewGetStats;
-
-      // need to make sure that this isn't called again when in the middle of processing
-      if (_gettingStats === true) {
-        OT.warn('PeerConnection.getStats: Already getting the stats!');
-        return;
-      }
-
-      // locking this function
-      _gettingStats = true;
-
-      // get the previous timestamp (seconds)
-      now = OT.$.now();
-      timeDifference = (now - prevStats.timeStamp) / 1000; // how many seconds has passed
-
-      // now update the date
-      prevStats.timeStamp = now;
-
-      /* this parses a result if there it contains the video bitrate */
-      parseAvgVideoBitrate = function(result) {
-        var lastBytesSent = prevStats.videoBytesTransferred || 0;
-
-        if (result.bytesSent || result.bytesReceived) {
-          prevStats.videoBytesTransferred = result.bytesSent || result.bytesReceived;
-          return Math.round((prevStats.videoBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else if (result.stat('googFrameHeightSent')) {
-          prevStats.videoBytesTransferred = result.stat('bytesSent');
-          return Math.round((prevStats.videoBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else if (result.stat('googFrameHeightReceived')) {
-          prevStats.videoBytesTransferred = result.stat('bytesReceived');
-          return Math.round((prevStats.videoBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else {
-          return NaN;
-        }
-
-      };
-
-        /* this parses a result if there it contains the audio bitrate */
-      parseAvgAudioBitrate = function(result) {
-        var lastBytesSent = prevStats.audioBytesTransferred || 0;
-
-        if (result.bytesSent || result.bytesReceived) {
-          prevStats.audioBytesTransferred = result.bytesSent || result.bytesReceived;
-          return Math.round((prevStats.audioBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else if (result.stat('audioInputLevel')) {
-          prevStats.audioBytesTransferred = result.stat('bytesSent');
-          return Math.round((prevStats.audioBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else if (result.stat('audioOutputLevel')) {
-          prevStats.audioBytesTransferred = result.stat('bytesReceived');
-          return Math.round((prevStats.audioBytesTransferred - lastBytesSent) * 8 / timeDifference);
-
-        } else {
-          return NaN;
-        }
-
-      };
-        
-      parseFrameRate = function(result) {
-        if (result.stat('googFrameRateSent')) {
-          return result.stat('googFrameRateSent');
-        } else if (result.stat('googFrameRateReceived')) {
-          return result.stat('googFrameRateReceived');
-        }
-        return null;
-      };
-
-      parseStatsReports = function(stats) {
-        if (stats.result) {
-          var resultList = stats.result();
-          for (var resultIndex = 0; resultIndex < resultList.length; resultIndex++) {
-            var result = resultList[resultIndex];
-            if (result.stat) {
-
-              if(result.stat('googActiveConnection') === 'true') {
-                parsedStats.localCandidateType = result.stat('googLocalCandidateType');
-                parsedStats.remoteCandidateType = result.stat('googRemoteCandidateType');
-                parsedStats.transportType = result.stat('googTransportType');
-              }
-
-              var avgVideoBitrate = parseAvgVideoBitrate(result);
-              if (!isNaN(avgVideoBitrate)) {
-                parsedStats.avgVideoBitrate = avgVideoBitrate;
-              }
-
-              var avgAudioBitrate = parseAvgAudioBitrate(result);
-              if (!isNaN(avgAudioBitrate)) {
-                parsedStats.avgAudioBitrate = avgAudioBitrate;
-              }
-              var frameRate = parseFrameRate(result);
-              if (frameRate != null) {
-                parsedStats.frameRate = frameRate;
-              }
-            }
-          }
-        }
-
-        _gettingStats = false;
-        callback(parsedStats);
-      };
-      parsedStats.duration = Math.round(now - _createTime);
-
-      parseStats = function(stats) {
-        for (var key in stats) {
-          if (stats.hasOwnProperty(key) &&
-            (stats[key].type === 'outboundrtp' || stats[key].type === 'inboundrtp')) {
-            var res = stats[key];
-            // Find the bandwidth info for video
-            if (res.id.indexOf('video') !== -1) {
-
-              var avgVideoBitrate = parseAvgVideoBitrate(res);
-              if(!isNaN(avgVideoBitrate)) {
-                parsedStats.avgVideoBitrate = avgVideoBitrate;
-              }
-
-            } else if (res.id.indexOf('audio') !== -1) {
-
-              var avgAudioBitrate = parseAvgAudioBitrate(res);
-              if(!isNaN(avgAudioBitrate)) {
-                parsedStats.avgAudioBitrate = avgAudioBitrate;
-              }
-
-            }
-          }
-        }
-
-        _gettingStats = false;
-        callback(parsedStats);
-      };
-
-      needsNewGetStats = function() {
-        var firefoxVersion = window.navigator.userAgent.toLowerCase()
-        .match(/Firefox\/([0-9\.]+)/i);
-        var needs = (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) >= 27.0);
-        needsNewGetStats = function() { return needs; };
-        return needs;
-      };
-
-      if (_peerConnection && _peerConnection.getStats) {
-        if(needsNewGetStats()) {
-          _peerConnection.getStats(null, parseStats, function(err) {
-            OT.warn('Error collecting stats', err);
-            _gettingStats = false;
-          });
-        } else {
-          _peerConnection.getStats(parseStatsReports);
-        }
-      } else {
-        // there was no peer connection yet or getStats isn't implemented in this enviroment
-        _gettingStats = false;
-        callback(parsedStats);
-      }
-    };
-
-    Object.defineProperty(this, 'remoteStreams', {
-      get: function() {
-        return _peerConnection ? getRemoteStreams() : [];
-      }
-    });
+    this.remoteStreams = function() {
+      return _peerConnection ? getRemoteStreams() : [];
+    };
+
+    var qos = new OT.PeerConnection.QOS(qosCallback);
   };
 
 })(window);
+//
+// There are three implementations of stats parsing in this file.
+// 1. For Chrome: Chrome is currently using an older version of the API
+// 2. For OTPlugin: The plugin is using a newer version of the API that
+//    exists in the latest WebRTC codebase
+// 3. For Firefox: FF is using a version that looks a lot closer to the
+//    current spec.
+//
+// I've attempted to keep the three implementations from sharing any code,
+// accordingly you'll notice a bunch of duplication between the three.
+//
+// This is acceptable as the goal is to be able to remove each implementation
+// as it's no longer needed without any risk of affecting the others. If there
+// was shared code between them then each removal would require an audit of
+// all the others.
+//
+//
+!(function() {
+
+  ///
+  // Get Stats using the older API. Used by all current versions
+  // of Chrome.
+  //
+  var parseStatsOldAPI = function parseStatsOldAPI (peerConnection,
+                                                    prevStats,
+                                                    currentStats,
+                                                    completion) {
+
+    /* this parses a result if there it contains the video bitrate */
+    var parseAvgVideoBitrate = function (result) {
+          if (result.stat('googFrameHeightSent')) {
+            currentStats.videoBytesTransferred = result.stat('bytesSent');
+          } else if (result.stat('googFrameHeightReceived')) {
+            currentStats.videoBytesTransferred = result.stat('bytesReceived');
+          } else {
+            return NaN;
+          }
+
+          var transferDelta = currentStats.videoBytesTransferred -
+                                        (prevStats.videoBytesTransferred || 0);
+
+          return Math.round(transferDelta * 8 / currentStats.deltaSecs);
+        },
+
+        /* this parses a result if there it contains the audio bitrate */
+        parseAvgAudioBitrate = function (result) {
+          if (result.stat('audioInputLevel')) {
+            currentStats.audioBytesTransferred = result.stat('bytesSent');
+          } else if (result.stat('audioOutputLevel')) {
+            currentStats.audioBytesTransferred = result.stat('bytesReceived');
+          } else {
+            return NaN;
+          }
+
+          var transferDelta = currentStats.audioBytesTransferred -
+                                        (prevStats.audioBytesTransferred || 0);
+          return Math.round(transferDelta * 8 / currentStats.deltaSecs);
+        },
+
+        parseFrameRate = function (result) {
+          if (result.stat('googFrameRateSent')) {
+            return result.stat('googFrameRateSent');
+          } else if (result.stat('googFrameRateReceived')) {
+            return result.stat('googFrameRateReceived');
+          }
+          return null;
+        },
+
+        parseStatsReports = function (stats) {
+          if (stats.result) {
+            var resultList = stats.result();
+            for (var resultIndex = 0; resultIndex < resultList.length; resultIndex++) {
+              var result = resultList[resultIndex];
+
+              if (result.stat) {
+
+                if(result.stat('googActiveConnection') === 'true') {
+                  currentStats.localCandidateType = result.stat('googLocalCandidateType');
+                  currentStats.remoteCandidateType = result.stat('googRemoteCandidateType');
+                  currentStats.transportType = result.stat('googTransportType');
+                }
+
+                var avgVideoBitrate = parseAvgVideoBitrate(result);
+                if (!isNaN(avgVideoBitrate)) {
+                  currentStats.avgVideoBitrate = avgVideoBitrate;
+                }
+
+                var avgAudioBitrate = parseAvgAudioBitrate(result);
+                if (!isNaN(avgAudioBitrate)) {
+                  currentStats.avgAudioBitrate = avgAudioBitrate;
+                }
+
+                var frameRate = parseFrameRate(result);
+                if (frameRate != null) {
+                  currentStats.frameRate = frameRate;
+                }
+              }
+            }
+          }
+
+          completion(null, currentStats);
+        };
+
+    peerConnection.getStats(parseStatsReports);
+  };
+
+  ///
+  // Get Stats for the OT Plugin, newer than Chromes version, but
+  // still not in sync with the spec.
+  //
+  var parseStatsOTPlugin = function parseStatsOTPlugin (peerConnection,
+                                                    prevStats,
+                                                    currentStats,
+                                                    completion) {
+
+    var onStatsError = function onStatsError (error) {
+          completion(error);
+        },
+
+        ///
+        // From the Audio Tracks
+        // * avgAudioBitrate
+        // * audioBytesTransferred
+        //
+        parseAudioStats = function (statsReport) {
+          var lastBytesSent = prevStats.audioBytesTransferred || 0,
+              transferDelta;
+
+          if (statsReport.audioInputLevel) {
+            currentStats.audioBytesTransferred = statsReport.bytesSent;
+          }
+          else if (statsReport.audioOutputLevel) {
+            currentStats.audioBytesTransferred = statsReport.bytesReceived;
+          }
+
+          if (currentStats.audioBytesTransferred) {
+            transferDelta = currentStats.audioBytesTransferred - lastBytesSent;
+            currentStats.avgAudioBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs);
+          }
+        },
+
+        ///
+        // From the Video Tracks
+        // * frameRate
+        // * avgVideoBitrate
+        // * videoBytesTransferred
+        //
+        parseVideoStats = function (statsReport) {
+
+          var lastBytesSent = prevStats.videoBytesTransferred || 0,
+              transferDelta;
+
+          if (statsReport.googFrameHeightSent) {
+            currentStats.videoBytesTransferred = statsReport.bytesSent;
+          }
+          else if (statsReport.googFrameHeightReceived) {
+            currentStats.videoBytesTransferred = statsReport.bytesReceived;
+          }
+
+          if (currentStats.videoBytesTransferred) {
+            transferDelta = currentStats.videoBytesTransferred - lastBytesSent;
+            currentStats.avgVideoBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs);
+          }
+
+          if (statsReport.googFrameRateSent) {
+            currentStats.frameRate = statsReport.googFrameRateSent;
+          } else if (statsReport.googFrameRateReceived) {
+            currentStats.frameRate = statsReport.googFrameRateReceived;
+          }
+        },
+
+        isStatsForVideoTrack = function(statsReport) {
+          return statsReport.googFrameHeightSent !== void 0 ||
+                  statsReport.googFrameHeightReceived !== void 0 ||
+                  currentStats.videoBytesTransferred !== void 0 ||
+                  statsReport.googFrameRateSent !== void 0;
+        },
+
+        isStatsForIceCandidate = function(statsReport) {
+          return statsReport.googActiveConnection === 'true';
+        };
+
+    peerConnection.getStats(null, function(statsReports) {
+      statsReports.forEach(function(statsReport) {
+        if (isStatsForIceCandidate(statsReport)) {
+          currentStats.localCandidateType = statsReport.googLocalCandidateType;
+          currentStats.remoteCandidateType = statsReport.googRemoteCandidateType;
+          currentStats.transportType = statsReport.googTransportType;
+        }
+        else if (isStatsForVideoTrack(statsReport)) {
+          parseVideoStats(statsReport);
+        }
+        else {
+          parseAudioStats(statsReport);
+        }
+      });
+
+      completion(null, currentStats);
+    }, onStatsError);
+  };
+
+
+  ///
+  // Get Stats using the newer API.
+  //
+  var parseStatsNewAPI = function parseStatsNewAPI (peerConnection,
+                                                    prevStats,
+                                                    currentStats,
+                                                    completion) {
+
+    var onStatsError = function onStatsError (error) {
+          completion(error);
+        },
+
+        parseAvgVideoBitrate = function parseAvgVideoBitrate (result) {
+          if (result.bytesSent || result.bytesReceived) {
+            currentStats.videoBytesTransferred = result.bytesSent || result.bytesReceived;
+          }
+          else {
+            return NaN;
+          }
+
+          var transferDelta = currentStats.videoBytesTransferred -
+                                        (prevStats.videoBytesTransferred || 0);
+
+          return Math.round(transferDelta * 8 / currentStats.deltaSecs);
+        },
+
+        parseAvgAudioBitrate = function parseAvgAudioBitrate (result) {
+          if (result.bytesSent || result.bytesReceived) {
+            currentStats.audioBytesTransferred = result.bytesSent || result.bytesReceived;
+          } else {
+            return NaN;
+          }
+
+          var transferDelta = currentStats.audioBytesTransferred -
+                                        (prevStats.audioBytesTransferred || 0);
+          return Math.round(transferDelta * 8 / currentStats.deltaSecs);
+        };
+
+
+    peerConnection.getStats(null, function(stats) {
+
+      for (var key in stats) {
+        if (stats.hasOwnProperty(key) &&
+          (stats[key].type === 'outboundrtp' || stats[key].type === 'inboundrtp')) {
+
+          var res = stats[key];
+
+          // Find the bandwidth info for video
+          if (res.id.indexOf('video') !== -1) {
+            var avgVideoBitrate = parseAvgVideoBitrate(res);
+            if(!isNaN(avgVideoBitrate)) {
+              currentStats.avgVideoBitrate = avgVideoBitrate;
+            }
+
+          } else if (res.id.indexOf('audio') !== -1) {
+            var avgAudioBitrate = parseAvgAudioBitrate(res);
+            if(!isNaN(avgAudioBitrate)) {
+              currentStats.avgAudioBitrate = avgAudioBitrate;
+            }
+
+          }
+        }
+      }
+
+      completion(null, currentStats);
+    }, onStatsError);
+  };
+
+
+  var parseQOS = function (peerConnection, prevStats, currentStats, completion) {
+    var firefoxVersion = window.navigator.userAgent
+                              .toLowerCase().match(/Firefox\/([0-9\.]+)/i);
+
+    if (TBPlugin.isInstalled()) {
+      parseQOS = parseStatsOTPlugin;
+      return parseStatsOTPlugin(peerConnection, prevStats, currentStats, completion);
+    }
+    else if (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) >= 27.0) {
+      parseQOS = parseStatsNewAPI;
+      return parseStatsNewAPI(peerConnection, prevStats, currentStats, completion);
+    }
+    else {
+      parseQOS = parseStatsOldAPI;
+      return parseStatsOldAPI(peerConnection, prevStats, currentStats, completion);
+    }
+  };
+
+  OT.PeerConnection.QOS = function (qosCallback) {
+    var _creationTime = OT.$.now(),
+        _peerConnection;
+
+    var calculateQOS = OT.$.bind(function calculateQOS (prevStats) {
+      if (!_peerConnection) {
+        // We don't have a PeerConnection yet, or we did and
+        // it's been closed. Either way we're done.
+        return;
+      }
+
+      var now = OT.$.now();
+
+      var currentStats = {
+        timeStamp: now,
+        duration: Math.round(now - _creationTime),
+        deltaSecs: (now - prevStats.timeStamp) / 1000
+      };
+
+      var onParsedStats = function (err, parsedStats) {
+        if (err) {
+          OT.error('Failed to Parse QOS Stats: ' + JSON.stringify(err));
+          return;
+        }
+
+        qosCallback(parsedStats, prevStats);
+
+        // Recalculate the stats
+        setTimeout(OT.$.bind(calculateQOS, null, parsedStats), OT.PeerConnection.QOS.INTERVAL);
+      };
+
+      parseQOS(_peerConnection, prevStats, currentStats, onParsedStats);
+    }, this);
+
+
+    this.startCollecting = function (peerConnection) {
+      if (!peerConnection || !peerConnection.getStats) {
+        // It looks like this browser doesn't support getStats
+        // Bail.
+        return;
+      }
+
+      _peerConnection = peerConnection;
+
+      calculateQOS({
+        timeStamp: OT.$.now()
+      });
+    };
+
+    this.stopCollecting = function () {
+      _peerConnection = null;
+    };
+  };
+
+  // Recalculate the stats in 30 sec
+  OT.PeerConnection.QOS.INTERVAL = 30000;
+})();
 !(function() {
 
   var _peerConnections = {};
 
   OT.PeerConnections = {
     add: function(remoteConnection, streamId, config) {
       var key = remoteConnection.id + '_' + streamId,
           ref = _peerConnections[key];
@@ -12557,44 +15424,45 @@ OTHelpers.centerElement = function(eleme
    *
    * Responsible for:
    * * setting up the underlying PeerConnection (delegates to OT.PeerConnections)
    * * triggering a connected event when the Peer connection is opened
    * * triggering a disconnected event when the Peer connection is closed
    * * providing a destroy method
    * * providing a processMessage method
    *
-   * Once the PeerConnection is connected and the video element playing it triggers 
+   * Once the PeerConnection is connected and the video element playing it triggers
    * the connected event
    *
    * Triggers the following events
    * * connected
    * * disconnected
    */
   OT.PublisherPeerConnection = function(remoteConnection, session, streamId, webRTCStream) {
     var _peerConnection,
         _hasRelayCandidates = false,
         _subscriberId = session._.subscriberMap[remoteConnection.id + '_' + streamId],
         _onPeerClosed,
         _onPeerError,
-        _relayMessageToPeer;
+        _relayMessageToPeer,
+        _onQOS;
 
     // Private
     _onPeerClosed = function() {
       this.destroy();
       this.trigger('disconnected', this);
     };
 
     // Note: All Peer errors are fatal right now.
     _onPeerError = function(errorReason, prefix) {
       this.trigger('error', null, errorReason, this, prefix);
       this.destroy();
     };
 
-    _relayMessageToPeer = function(type, payload) {
+    _relayMessageToPeer = OT.$.bind(function(type, payload) {
       if (!_hasRelayCandidates){
         var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
                                 type === OT.Raptor.Actions.OFFER ||
                                 type === OT.Raptor.Actions.ANSWER ||
                                 type === OT.Raptor.Actions.PRANSWER ;
 
         if (extractCandidates) {
           var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
@@ -12628,60 +15496,62 @@ OTHelpers.centerElement = function(eleme
         case OT.Raptor.Actions.CANDIDATE:
           if (session.sessionInfo.p2pEnabled) {
             session._.jsepCandidateP2p(streamId, _subscriberId, payload);
 
           } else {
             session._.jsepCandidate(streamId, payload);
           }
       }
-    }.bind(this);
+    }, this);
+
+    _onQOS = OT.$.bind(function _onQOS (parsedStats, prevStats) {
+      this.trigger('qos', remoteConnection, parsedStats, prevStats);
+    }, this);
 
     OT.$.eventing(this);
 
     // Public
     this.destroy = function() {
       // Clean up our PeerConnection
       if (_peerConnection) {
+        _peerConnection.off();
         OT.PeerConnections.remove(remoteConnection, streamId);
       }
 
-      _peerConnection.off();
       _peerConnection = null;
     };
 
     this.processMessage = function(type, message) {
       _peerConnection.processMessage(type, message);
     };
 
-    this.getStats = function(prevStats, callback) {
-      _peerConnection.getStats(prevStats, callback);
-    };
-
     // Init
     this.init = function(iceServers) {
       _peerConnection = OT.PeerConnections.add(remoteConnection, streamId, {
         iceServers: iceServers
       });
 
       _peerConnection.on({
         close: _onPeerClosed,
-        error: _onPeerError
+        error: _onPeerError,
+        qos: _onQOS
       }, this);
 
       _peerConnection.registerMessageDelegate(_relayMessageToPeer);
       _peerConnection.addLocalStream(webRTCStream);
 
-      Object.defineProperty(this, 'remoteConnection', {
-        value: remoteConnection
-      });
-
-      Object.defineProperty(this, 'hasRelayCandidates', {
-        get: function() { return _hasRelayCandidates; }
-      });
+      this.remoteConnection = function() {
+        return remoteConnection;
+      };
+
+      this.hasRelayCandidates = function() {
+        return _hasRelayCandidates;
+      };
+
     };
   };
 
 })(window);
 !(function() {
 
   /*
    * Abstracts PeerConnection related stuff away from OT.Subscriber.
@@ -12705,23 +15575,25 @@ OTHelpers.centerElement = function(eleme
    * * remoteStreamRemoved
    * * error
    *
    */
 
   OT.SubscriberPeerConnection = function(remoteConnection, session, stream,
     subscriber, properties) {
     var _peerConnection,
+        _destroyed = false,
         _hasRelayCandidates = false,
         _onPeerClosed,
         _onRemoteStreamAdded,
         _onRemoteStreamRemoved,
         _onPeerError,
         _relayMessageToPeer,
-        _setEnabledOnStreamTracksCurry;
+        _setEnabledOnStreamTracksCurry,
+        _onQOS;
 
     // Private
     _onPeerClosed = function() {
       this.destroy();
       this.trigger('disconnected', this);
     };
 
     _onRemoteStreamAdded = function(remoteRTCStream) {
@@ -12732,17 +15604,17 @@ OTHelpers.centerElement = function(eleme
       this.trigger('remoteStreamRemoved', remoteRTCStream, this);
     };
 
     // Note: All Peer errors are fatal right now.
     _onPeerError = function(errorReason, prefix) {
       this.trigger('error', null, errorReason, this, prefix);
     };
 
-    _relayMessageToPeer = function(type, payload) {
+    _relayMessageToPeer = OT.$.bind(function(type, payload) {
       if (!_hasRelayCandidates){
         var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
                                 type === OT.Raptor.Actions.OFFER ||
                                 type === OT.Raptor.Actions.ANSWER ||
                                 type === OT.Raptor.Actions.PRANSWER ;
 
         if (extractCandidates) {
           var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
@@ -12761,24 +15633,24 @@ OTHelpers.centerElement = function(eleme
         case OT.Raptor.Actions.OFFER:
           session._.jsepOfferP2p(stream.id, subscriber.widgetId, payload.sdp);
           break;
 
         case OT.Raptor.Actions.CANDIDATE:
           session._.jsepCandidateP2p(stream.id, subscriber.widgetId, payload);
           break;
       }
-    }.bind(this);
+    }, this);
 
     // Helper method used by subscribeToAudio/subscribeToVideo
     _setEnabledOnStreamTracksCurry = function(isVideo) {
       var method = 'get' + (isVideo ? 'Video' : 'Audio') + 'Tracks';
 
       return function(enabled) {
-        var remoteStreams = _peerConnection.remoteStreams,
+        var remoteStreams = _peerConnection.remoteStreams(),
             tracks,
             stream;
 
         if (remoteStreams.length === 0 || !remoteStreams[0][method]) {
           // either there is no remote stream or we are in a browser that doesn't
           // expose the media tracks (Firefox)
           return;
         }
@@ -12791,106 +15663,110 @@ OTHelpers.centerElement = function(eleme
             // Only change the enabled property if it's different
             // otherwise we get flickering of the video
             if (tracks[k].enabled !== enabled) tracks[k].enabled=enabled;
           }
         }
       };
     };
 
+    _onQOS = OT.$.bind(function _onQOS (parsedStats, prevStats) {
+      this.trigger('qos', parsedStats, prevStats);
+    }, this);
 
     OT.$.eventing(this);
 
     // Public
     this.destroy = function() {
+      if (_destroyed) return;
+      _destroyed = true;
+
       if (_peerConnection) {
         var numDelegates = _peerConnection.unregisterMessageDelegate(_relayMessageToPeer);
-        
+
         // Only clean up the PeerConnection if there isn't another Subscriber using it
         if (numDelegates === 0) {
           // Unsubscribe us from the stream, if it hasn't already been destroyed
-          if (session && session.connected && stream && !stream.destroyed) {
+          if (session && session.isConnected() && stream && !stream.destroyed) {
               // Notify the server components
             session._.subscriberDestroy(stream, subscriber);
           }
-          
+
           // Ref: OPENTOK-2458 disable all audio tracks before removing it.
           this.subscribeToAudio(false);
         }
-        OT.PeerConnections.remove(remoteConnection, stream.streamId);
+        OT.PeerConnections.remove(remoteConnection, stream.id);
       }
       _peerConnection = null;
       this.off();
     };
 
     this.processMessage = function(type, message) {
       _peerConnection.processMessage(type, message);
     };
 
-    this.getStats = function(prevStats, callback) {
-      _peerConnection.getStats(prevStats, callback);
-    };
 
     this.subscribeToAudio = _setEnabledOnStreamTracksCurry(false);
     this.subscribeToVideo = _setEnabledOnStreamTracksCurry(true);
 
-    Object.defineProperty(this, 'hasRelayCandidates', {
-      get: function() { return _hasRelayCandidates; }
-    });
+    this.hasRelayCandidates = function() {
+      return _hasRelayCandidates;
+    };
 
     // Init
     this.init = function() {
       _peerConnection = OT.PeerConnections.add(remoteConnection, stream.streamId, {});
 
       _peerConnection.on({
         close: _onPeerClosed,
         streamAdded: _onRemoteStreamAdded,
         streamRemoved: _onRemoteStreamRemoved,
-        error: _onPeerError
+        error: _onPeerError,
+        qos: _onQOS
       }, this);
 
       var numDelegates = _peerConnection.registerMessageDelegate(_relayMessageToPeer);
 
         // If there are already remoteStreams, add them immediately
-      if (_peerConnection.remoteStreams.length > 0) {
-        _peerConnection.remoteStreams.forEach(_onRemoteStreamAdded, this);
+      if (_peerConnection.remoteStreams().length > 0) {
+        OT.$.forEach(_peerConnection.remoteStreams(), _onRemoteStreamAdded, this);
       } else if (numDelegates === 1) {
         // We only bother with the PeerConnection negotiation if we don't already
         // have a remote stream.
 
         var channelsToSubscribeTo;
 
         if (properties.subscribeToVideo || properties.subscribeToAudio) {
           var audio = stream.getChannelsOfType('audio'),
               video = stream.getChannelsOfType('video');
 
-          channelsToSubscribeTo = audio.map(function(channel) {
+          channelsToSubscribeTo = OT.$.map(audio, function(channel) {
             return {
               id: channel.id,
               type: channel.type,
               active: properties.subscribeToAudio
             };
-          }).concat(video.map(function(channel) {
+          }).concat(OT.$.map(video, function(channel) {
             return {
               id: channel.id,
               type: channel.type,
               active: properties.subscribeToVideo,
               restrictFrameRate: properties.restrictFrameRate !== void 0 ?
                 properties.restrictFrameRate : false
             };
           }));
         }
 
         session._.subscriberCreate(stream, subscriber, channelsToSubscribeTo,
-          function(err, message) {
+          OT.$.bind(function(err, message) {
             if (err) {
               this.trigger('error', null, err.message, this, 'Subscribe');
             }
             _peerConnection.setIceServers(OT.Raptor.parseIceServers(message));
-          }.bind(this));
+          }, this));
       }
     };
   };
 
 })(window);
 !(function() {
 
 // Manages N Chrome elements
@@ -12900,19 +15776,17 @@ OTHelpers.centerElement = function(eleme
 
         // Private helper function
         _set = function(name, widget) {
           widget.parent = this;
           widget.appendTo(properties.parent);
 
           _widgets[name] = widget;
 
-          Object.defineProperty(this, name, {
-            get: function() { return _widgets[name]; }
-          });
+          this[name] = widget;
         };
 
     if (!properties.parent) {
       // @todo raise an exception
       return;
     }
 
     OT.$.eventing(this);
@@ -13067,27 +15941,17 @@ OTHelpers.centerElement = function(eleme
   // when the stream is first displayed and when the user mouses over the display),
   // "off" (the mute button is not displayed), and "on" (the mute button is displayed).
   //
   // displays a backing bar
   // can be shown/hidden
   // can be destroyed
   OT.Chrome.BackingBar = function(options) {
     var _nameMode = options.nameMode,
-        _muteMode = options.muteMode,
-        _domElement;
-
-    // This behaviour must be implemented to make the widget behaviour work.
-    // @fixme This is a nasty code smell
-    Object.defineProperty(this, 'domElement', {
-      get: function() { return _domElement; },
-      set: function(domElement) {
-        _domElement = domElement;
-      }
-    });
+        _muteMode = options.muteMode;
 
     function getDisplayMode() {
       if(_nameMode === 'on' || _muteMode === 'on') {
         return 'on';
       } else if(_nameMode === 'mini' || _muteMode === 'mini') {
         return 'mini';
       } else if(_nameMode === 'mini-auto' || _muteMode === 'mini-auto') {
         return 'mini-auto';
@@ -13130,159 +15994,129 @@ OTHelpers.centerElement = function(eleme
   // when the stream is first displayed and when the user mouses over the display),
   // "off" (the name is not displayed), and "on" (the name is displayed).
   //
   // displays a name
   // can be shown/hidden
   // can be destroyed
   OT.Chrome.NamePanel = function(options) {
     var _name = options.name,
-        _bugMode = options.bugMode,
-        _domElement;
-
-    if (!_name || _name.trim().length === '') {
+        _bugMode = options.bugMode;
+
+    if (!_name || OT.$.trim(_name).length === '') {
       _name = null;
 
       // THere's no name, just flip the mode off
       options.mode = 'off';
     }
 
-    // This behaviour must be implemented to make the widget behaviour work.
-    // @fixme This is a nasty code smell
-    Object.defineProperty(this, 'domElement', {
-      get: function() { return _domElement; },
-      set: function(domElement) {
-        _domElement = domElement;
-      }
-    });
-
-    Object.defineProperty(this, 'name', {
-      set: function(name) {
-        if (!_name) this.setDisplayMode('auto');
-        _name = name;
-        _domElement.innerHTML = _name;
-      }.bind(this)
-    });
-
-    this.setBugMode = function(bugMode) {
+    this.setName = OT.$.bind(function(name) {
+      if (!_name) this.setDisplayMode('auto');
+      _name = name;
+      this.domElement.innerHTML = _name;
+    });
+
+    this.setBugMode = OT.$.bind(function(bugMode) {
       _bugMode = bugMode;
       if(bugMode === 'off') {
-        OT.$.addClass(_domElement, 'OT_name-no-bug');
-      } else {
-        OT.$.removeClass(_domElement, 'OT_name-no-bug');
-      }
-    };
+        OT.$.addClass(this.domElement, 'OT_name-no-bug');
+      } else {
+        OT.$.removeClass(this.domElement, 'OT_name-no-bug');
+      }
+    }, this);
 
     // Mixin common widget behaviour
     OT.Chrome.Behaviour.Widget(this, {
       mode: options.mode,
       nodeName: 'h1',
       htmlContent: _name,
       htmlAttributes: {
         className: 'OT_name OT_edge-bar-item'
       },
-      onCreate: function() {
+      onCreate: OT.$.bind(function() {
         this.setBugMode(_bugMode);
-      }.bind(this)
+      }, this)
     });
 
   };
 
 })(window);
 !(function() {
 
   OT.Chrome.MuteButton = function(options) {
     var _onClickCb,
         _muted = options.muted || false,
-        _domElement,
         updateClasses,
         attachEvents,
         detachEvents,
         onClick;
 
-    // This behaviour must be implemented to make the widget behaviour work.
-    // @fixme This is a nasty code smell
-    Object.defineProperty(this, 'domElement', {
-      get: function() { return _domElement; },
-      set: function(domElement) {
-        _domElement = domElement;
-      }
-    });
-
-    updateClasses = function() {
+    updateClasses = OT.$.bind(function() {
       if (_muted) {
-        OT.$.addClass(_domElement, 'OT_active');
-      } else {
-        OT.$.removeClass(_domElement, 'OT_active ');
-      }
-    };
+        OT.$.addClass(this.domElement, 'OT_active');
+      } else {
+        OT.$.removeClass(this.domElement, 'OT_active ');
+      }
+    }, this);
 
     // Private Event Callbacks
     attachEvents = function(elem) {
-      _onClickCb = onClick.bind(this);
-      elem.addEventListener('click', _onClickCb, false);
+      _onClickCb = OT.$.bind(onClick, this);
+      OT.$.on(elem, 'click', _onClickCb);
     };
 
     detachEvents = function(elem) {
       _onClickCb = null;
-      elem.removeEventListener('click', _onClickCb, false);
+      OT.$.off(elem, 'click', _onClickCb);
     };
 
     onClick = function() {
       _muted = !_muted;
 
       updateClasses();
 
       if (_muted) {
         this.parent.trigger('muted', this);
       } else {
         this.parent.trigger('unmuted', this);
       }
 
       return false;
     };
 
-    Object.defineProperty(this, 'muted', {
-      get: function() { return _muted; },
-      set: function(muted) {
-        _muted = muted;
-        updateClasses();
+    OT.$.defineProperties(this, {
+      muted: {
+        get: function() { return _muted; },
+        set: function(muted) {
+          _muted = muted;
+          updateClasses();
+        }
       }
     });
 
     // Mixin common widget behaviour
     var classNames = _muted ? 'OT_edge-bar-item OT_mute OT_active' : 'OT_edge-bar-item OT_mute';
     OT.Chrome.Behaviour.Widget(this, {
       mode: options.mode,
       nodeName: 'button',
       htmlContent: 'Mute',
       htmlAttributes: {
         className: classNames
       },
-      onCreate: attachEvents.bind(this),
-      onDestroy: detachEvents.bind(this)
+      onCreate: OT.$.bind(attachEvents, this),
+      onDestroy: OT.$.bind(detachEvents, this)
     });
   };
 
 
 })(window);
 !(function() {
 
   OT.Chrome.OpenTokButton = function(options) {
 
-    // This behaviour must be implemented to make the widget behaviour work.
-    // @fixme This is a nasty code smell
-    var _domElement;
-    Object.defineProperty(this, 'domElement', {
-      get: function() { return _domElement; },
-      set: function(domElement) {
-        _domElement = domElement;
-      }
-    });
-
     // Mixin common widget behaviour
     OT.Chrome.Behaviour.Widget(this, {
       mode: options ? options.mode : null,
       nodeName: 'span',
       htmlContent: 'OpenTok',
       htmlAttributes: {
         className: 'OT_opentok OT_edge-bar-item'
       }
@@ -13309,107 +16143,100 @@ OTHelpers.centerElement = function(eleme
   OT.Chrome.Archiving = function(options) {
     var _archiving = options.archiving,
         _archivingStarted = options.archivingStarted || 'Archiving on',
         _archivingEnded = options.archivingEnded || 'Archiving off',
         _initialState = true,
         _lightBox,
         _light,
         _text,
-        _domElement,
+        _textNode,
         renderStageDelayedAction,
         renderText,
         renderStage;
 
-    // This behaviour must be implemented to make the widget behaviour work.
-    // @fixme This is a nasty code smell
-    Object.defineProperty(this, 'domElement', {
-      get: function() { return _domElement; },
-      set: function(domElement) {
-        _domElement = domElement;
-      }
-    });
-
     renderText = function(text) {
-      _text.innerText = text;
+      _textNode.nodeValue = text;
       _lightBox.setAttribute('title', text);
     };
 
-    renderStage = function() {
+    renderStage = OT.$.bind(function() {
       if(renderStageDelayedAction) {
         clearTimeout(renderStageDelayedAction);
         renderStageDelayedAction = null;
       }
 
       if(_archiving) {
         OT.$.addClass(_light, 'OT_active');
       } else {
         OT.$.removeClass(_light, 'OT_active');
       }
 
-      OT.$.removeClass(_domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off'));
-      OT.$.addClass(_domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off'));
+      OT.$.removeClass(this.domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off'));
+      OT.$.addClass(this.domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off'));
       if(options.show && _archiving) {
         renderText(_archivingStarted);
         OT.$.addClass(_text, 'OT_mode-on');
         OT.$.removeClass(_text, 'OT_mode-auto');
         this.setDisplayMode('on');
         renderStageDelayedAction = setTimeout(function() {
           OT.$.addClass(_text, 'OT_mode-auto');
           OT.$.removeClass(_text, 'OT_mode-on');
-        }.bind(this), 5000);
+        }, 5000);
       } else if(options.show && !_initialState) {
         OT.$.addClass(_text, 'OT_mode-on');
         OT.$.removeClass(_text, 'OT_mode-auto');
         this.setDisplayMode('on');
         renderText(_archivingEnded);
-        renderStageDelayedAction = setTimeout(function() {
+        renderStageDelayedAction = setTimeout(OT.$.bind(function() {
           this.setDisplayMode('off');
-        }.bind(this), 5000);
+        }, this), 5000);
       } else {
         this.setDisplayMode('off');
       }
-    }.bind(this);
+    }, this);
 
     // Mixin common widget behaviour
     OT.Chrome.Behaviour.Widget(this, {
       mode: _archiving && options.show && 'on' || 'off',
       nodeName: 'h1',
       htmlAttributes: {className: 'OT_archiving OT_edge-bar-item OT_edge-bottom'},
-      onCreate: function() {
+      onCreate: OT.$.bind(function() {
         _lightBox = OT.$.createElement('div', {
           className: 'OT_archiving-light-box'
         }, '');
         _light = OT.$.createElement('div', {
           className: 'OT_archiving-light'
         }, '');
         _lightBox.appendChild(_light);
         _text = OT.$.createElement('div', {
           className: 'OT_archiving-status OT_mode-on OT_edge-bar-item OT_edge-bottom'
         }, '');
-        _domElement.appendChild(_lightBox);
-        _domElement.appendChild(_text);
+        _textNode = document.createTextNode('');
+        _text.appendChild(_textNode);
+        this.domElement.appendChild(_lightBox);
+        this.domElement.appendChild(_text);
         renderStage();
-      }
-    });
-
-    this.setShowArchiveStatus = function(show) {
+      }, this)
+    });
+
+    this.setShowArchiveStatus = OT.$.bind(function(show) {
       options.show = show;
-      if(_domElement) {
+      if(this.domElement) {
         renderStage.call(this);
       }
-    };
-
-    this.setArchiving = function(status) {
+    }, this);
+
+    this.setArchiving = OT.$.bind(function(status) {
       _archiving = status;
       _initialState = false;
-      if(_domElement) {
+      if(this.domElement) {
         renderStage.call(this);
       }
-    };
+    }, this);
 
   };
 
 })(window);
 (function() {
 /* Stylable Notes
  * RTC doesn't need to wait until anything is loaded
  * Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode.
@@ -13451,30 +16278,30 @@ OTHelpers.centerElement = function(eleme
       } else {
         self.trigger('styleValueChanged', key, value);
       }
     };
 
     var _style = new Style(initalStyles, onStyleChange);
 
   /**
-   * Returns an object that has the properties that define the current user interface controls of 
-   * the Publisher. You can modify the properties of this object and pass the object to the 
-   * <code>setStyle()</code> method of thePublisher object. (See the documentation for 
+   * Returns an object that has the properties that define the current user interface controls of
+   * the Publisher. You can modify the properties of this object and pass the object to the
+   * <code>setStyle()</code> method of thePublisher object. (See the documentation for
    * <a href="#setStyle">setStyle()</a> to see the styles that define this object.)
    * @return {Object} The object that defines the styles of the Publisher.
    * @see <a href="#setStyle">setStyle()</a>
    * @method #getStyle
    * @memberOf Publisher
    */
 
 	/**
-	 * Returns an object that has the properties that define the current user interface controls of 
-   * the Subscriber. You can modify the properties of this object and pass the object to the 
-   * <code>setStyle()</code> method of the Subscriber object. (See the documentation for 
+	 * Returns an object that has the properties that define the current user interface controls of
+   * the Subscriber. You can modify the properties of this object and pass the object to the
+   * <code>setStyle()</code> method of the Subscriber object. (See the documentation for
    * <a href="#setStyle">setStyle()</a> to see the styles that define this object.)
 	 * @return {Object} The object that defines the styles of the Subscriber.
 	 * @see <a href="#setStyle">setStyle()</a>
 	 * @method #getStyle
 	 * @memberOf Subscriber
 	 */
     // If +key+ is falsly then all styles will be returned.
     self.getStyle = function(key) {
@@ -13513,32 +16340,32 @@ OTHelpers.centerElement = function(eleme
    *       displayed), and <code>"on"</code> (the name is always displayed).</li>
    *   </ul>
    * </p>
    *
    * <p>For example, the following code passes one parameter to the method:</p>
    *
    * <pre>myPublisher.setStyle({nameDisplayMode: "off"});</pre>
    *
-   * <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are 
-   * key-value pair that define one property of the display style. For example, the following 
+   * <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are
+   * key-value pair that define one property of the display style. For example, the following
    * code passes two parameter values to the method:</p>
    *
    * <pre>myPublisher.setStyle("nameDisplayMode", "off");</pre>
    *
    * <p>You can set the initial settings when you call the <code>Session.publish()</code>
    * or <code>OT.initPublisher()</code> method. Pass a <code>style</code> property as part of the
    * <code>properties</code> parameter of the method.</p>
    *
-   * <p>The OT object dispatches an <code>exception</code> event if you pass in an invalid style 
+   * <p>The OT object dispatches an <code>exception</code> event if you pass in an invalid style
    * to the method. The <code>code</code> property of the ExceptionEvent object is set to 1011.</p>
    *
-   * @param {Object} style Either an object containing properties that define the style, or a 
+   * @param {Object} style Either an object containing properties that define the style, or a
    * String defining this single style property to set.
-   * @param {String} value The value to set for the <code>style</code> passed in. Pass a value 
+   * @param {String} value The value to set for the <code>style</code> passed in. Pass a value
    * for this parameter only if the value of the <code>style</code> parameter is a String.</p>
    *
    * @see <a href="#getStyle">getStyle()</a>
    * @return {Publisher} The Publisher object
    * @see <a href="#setStyle">setStyle()</a>
    *
    * @see <a href="Session.html#subscribe">Session.publish()</a>
    * @see <a href="OT.html#initPublisher">OT.initPublisher()</a>
@@ -13578,32 +16405,32 @@ OTHelpers.centerElement = function(eleme
    *       displayed), and <code>"on"</code> (the name is always displayed).</li>
    *   </ul>
    * </p>
    *
    * <p>For example, the following code passes one parameter to the method:</p>
    *
    * <pre>mySubscriber.setStyle({nameDisplayMode: "off"});</pre>
    *
-   * <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are key-value 
-   * pair that define one property of the display style. For example, the following code passes 
+   * <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are key-value
+   * pair that define one property of the display style. For example, the following code passes
    * two parameter values to the method:</p>
    *
    * <pre>mySubscriber.setStyle("nameDisplayMode", "off");</pre>
    *
    * <p>You can set the initial settings when you call the <code>Session.subscribe()</code> method.
-   * Pass a <code>style</code> property as part of the <code>properties</code> parameter of the 
+   * Pass a <code>style</code> property as part of the <code>properties</code> parameter of the
    * method.</p>
    *
-   * <p>The OT object dispatches an <code>exception</code> event if you pass in an invalid style 
+   * <p>The OT object dispatches an <code>exception</code> event if you pass in an invalid style
    * to the method. The <code>code</code> property of the ExceptionEvent object is set to 1011.</p>
    *
-   * @param {Object} style Either an object containing properties that define the style, or a 
+   * @param {Object} style Either an object containing properties that define the style, or a
    * String defining this single style property to set.
-   * @param {String} value The value to set for the <code>style</code> passed in. Pass a value 
+   * @param {String} value The value to set for the <code>style</code> passed in. Pass a value
    * for this parameter only if the value of the <code>style</code> parameter is a String.</p>
    *
    * @returns {Subscriber} The Subscriber object.
    *
    * @see <a href="#getStyle">getStyle()</a>
    * @see <a href="#setStyle">setStyle()</a>
    *
    * @see <a href="Session.html#subscribe">Session.subscribe()</a>
@@ -13646,18 +16473,18 @@ OTHelpers.centerElement = function(eleme
       showControlBar: [true, false],
       showArchiveStatus: [true, false]
     };
 
 
     // Validates the style +key+ and also whether +value+ is valid for +key+
     isValidStyle = function(key, value) {
       return key === 'backgroundImageURI' ||
-        (   _validStyleValues.hasOwnProperty(key) &&
-        _validStyleValues[key].indexOf(value) !== -1 );
+        (_validStyleValues.hasOwnProperty(key) &&
+          OT.$.arrayIndexOf(_validStyleValues[key], value) !== -1 );
     };
 
     castValue = function(value) {
       switch(value) {
         case 'true':
           return true;
         case 'false':
           return false;
@@ -13669,17 +16496,18 @@ OTHelpers.centerElement = function(eleme
     // Returns a shallow copy of the styles.
     this.getAll = function() {
       var style = OT.$.clone(_style);
 
       for (var key in style) {
         if(!style.hasOwnProperty(key)) {
           continue;
         }
-        if (_COMPONENT_STYLES.indexOf(key) < 0) {
+        if (OT.$.arrayIndexOf(_COMPONENT_STYLES, key) < 0) {
+
           // Strip unnecessary properties out, should this happen on Set?
           delete style[key];
         }
       }
 
       return style;
     };
 
@@ -13748,58 +16576,52 @@ OTHelpers.centerElement = function(eleme
 
 /*
  * A Publishers Microphone.
  *
  * TODO
  * * bind to changes in mute/unmute/volume/etc and respond to them
  */
   OT.Microphone = function(webRTCStream, muted) {
-    var _muted,
-        _gain = 50;
-
-
-    Object.defineProperty(this, 'muted', {
-      get: function() { return _muted; },
-      set: function(muted) {
-        if (_muted === muted) return;
-
-        _muted = muted;
-
-        var audioTracks = webRTCStream.getAudioTracks();
-
-        for (var i=0, num=audioTracks.length; i<num; ++i) {
-          audioTracks[i].enabled = !_muted;
-        }
-      }
-    });
-
-    Object.defineProperty(this, 'gain', {
-      get: function() { return _gain; },
-      set: function(gain) {
-        OT.warn('OT.Microphone.gain IS NOT YET IMPLEMENTED');
-        _gain = gain;
+    var _muted;
+
+    OT.$.defineProperties(this, {
+      muted: {
+        get: function() {
+          return _muted;
+        },
+        set: function(muted) {
+          if (_muted === muted) return;
+
+          _muted = muted;
+
+          var audioTracks = webRTCStream.getAudioTracks();
+
+          for (var i=0, num=audioTracks.length; i<num; ++i) {
+            audioTracks[i].setEnabled(!_muted);
+          }
+        }
       }
     });
 
     // Set the initial value
     if (muted !== undefined) {
-      this.muted = muted === true;
+      this.muted(muted === true);
 
     } else if (webRTCStream.getAudioTracks().length) {
-      this.muted = !webRTCStream.getAudioTracks()[0].enabled;
+      this.muted(!webRTCStream.getAudioTracks()[0].enabled);
 
     } else {
-      this.muted = false;
+      this.muted(false);
     }
 
   };
 
 })(window);
-!(function() {
+!(function(window, OT) {
 
   // A Factory method for generating simple state machine classes.
   //
   // @usage
   //    var StateMachine = OT.generateSimpleStateMachine('start', ['start', 'middle', 'end', {
   //      start: ['middle'],
   //      middle: ['end'],
   //      end: ['start']
@@ -13809,27 +16631,30 @@ OTHelpers.centerElement = function(eleme
   //    state.current;            // <-- start
   //    state.set('middle');
   //
   OT.generateSimpleStateMachine = function(initialState, states, transitions) {
     var validStates = states.slice(),
         validTransitions = OT.$.clone(transitions);
 
     var isValidState = function (state) {
-      return validStates.indexOf(state) !== -1;
+      return OT.$.arrayIndexOf(validStates, state) !== -1;
     };
 
     var isValidTransition = function(fromState, toState) {
-      return validTransitions[fromState] && validTransitions[fromState].indexOf(toState) !== -1;
+      return validTransitions[fromState] &&
+        OT.$.arrayIndexOf(validTransitions[fromState], toState) !== -1;
     };
 
     return function(stateChangeFailed) {
       var currentState = initialState,
           previousState = null;
 
+      this.current = currentState;
+
       function signalChangeFailed(message, newState) {
         stateChangeFailed({
           message: message,
           newState: newState,
           currentState: currentState,
           previousState: previousState
         });
       }
@@ -13850,76 +16675,70 @@ OTHelpers.centerElement = function(eleme
         }
 
         return true;
       }
 
 
       this.set = function(newState) {
         if (!handleInvalidStateChanges(newState)) return;
-
         previousState = currentState;
-        currentState = newState;
-      };
-
-      Object.defineProperties(this, {
-        current: {
-          get: function() { return currentState; }
-        },
-
-        subscribing: {
-          get: function() { return currentState === 'Subscribing'; }
-        }
-      });
-    };
-  };
-
-})(window);
+        this.current = currentState = newState;
+      };
+      
+    };
+  };
+
+})(window, window.OT);
 !(function() {
 
 // Models a Subscriber's subscribing State
 //
 // Valid States:
 //     NotSubscribing            (the initial state
 //     Init                      (basic setup of DOM
 //     ConnectingToPeer          (Failure Cases -> No Route, Bad Offer, Bad Answer
-//     BindingRemoteStream       (Failure Cases -> Anything to do with the media being 
+//     BindingRemoteStream       (Failure Cases -> Anything to do with the media being
 //                               (invalid, the media never plays
 //     Subscribing               (this is 'onLoad'
-//     Failed                    (terminal state, with a reason that maps to one of the 
+//     Failed                    (terminal state, with a reason that maps to one of the
 //                               (failure cases above
+//     Destroyed                 (The subscriber has been cleaned up, terminal state
 //
 //
 // Valid Transitions:
 //     NotSubscribing ->
 //         Init
 //
 //     Init ->
 //             ConnectingToPeer
-//           | BindingRemoteStream         (if we are subscribing to ourselves and we alreay 
+//           | BindingRemoteStream         (if we are subscribing to ourselves and we alreay
 //                                         (have a stream
 //           | NotSubscribing              (destroy()
 //
 //     ConnectingToPeer ->
 //             BindingRemoteStream
 //           | NotSubscribing
 //           | Failed
 //           | NotSubscribing              (destroy()
 //
 //     BindingRemoteStream ->
 //             Subscribing
 //           | Failed
 //           | NotSubscribing              (destroy()
 //
 //     Subscribing ->
 //             NotSubscribing              (unsubscribe
-//           | Failed                      (probably a peer connection failure after we began 
+//           | Failed                      (probably a peer connection failure after we began
 //                                         (subscribing
 //
-//     Failed ->                           (terminal error state)
+//     Failed ->
+//             Destroyed
+//
+//     Destroyed ->                        (terminal state)
 //
 //
 // @example
 //     var state = new SubscribingState(function(change) {
 //       console.log(change.message);
 //     });
 //
 //     state.set('Init');
@@ -13928,50 +16747,66 @@ OTHelpers.centerElement = function(eleme
 //     state.set('Subscribing');      -> triggers stateChangeFailed and logs out the error message
 //
 //
   var validStates,
       validTransitions,
       initialState = 'NotSubscribing';
 
   validStates = [
-    'NotSubscribing', 'Init', 'ConnectingToPeer', 'BindingRemoteStream', 'Subscribing', 'Failed'
+    'NotSubscribing', 'Init', 'ConnectingToPeer',
+    'BindingRemoteStream', 'Subscribing', 'Failed',
+    'Destroyed'
   ];
 
   validTransitions = {
-    NotSubscribing: ['NotSubscribing', 'Init'],
-    Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream'],
-    ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed'],
-    BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed'],
-    Subscribing: ['NotSubscribing', 'Failed'],
-    Failed: []
+    NotSubscribing: ['NotSubscribing', 'Init', 'Destroyed'],
+    Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream', 'Destroyed'],
+    ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed', 'Destroyed'],
+    BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed', 'Destroyed'],
+    Subscribing: ['NotSubscribing', 'Failed', 'Destroyed'],
+    Failed: ['Destroyed'],
+    Destroyed: []
   };
 
   OT.SubscribingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
 
-  Object.defineProperty(OT.SubscribingState.prototype, 'attemptingToSubscribe', {
-    get: function() {
-      return [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ]
-        .indexOf(this.current) !== -1;
-    }
-  });
+  OT.SubscribingState.prototype.isDestroyed = function() {
+    return this.current === 'Destroyed';
+  };
+
+  OT.SubscribingState.prototype.isFailed = function() {
+    return this.current === 'Failed';
+  };
+
+  OT.SubscribingState.prototype.isSubscribing = function() {
+    return this.current === 'Subscribing';
+  };
+
+  OT.SubscribingState.prototype.isAttemptingToSubscribe = function() {
+    return OT.$.arrayIndexOf(
+      [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ],
+      this.current
+    ) !== -1;
+  };
 
 })(window);
 !(function() {
 
 // Models a Publisher's publishing State
 //
 // Valid States:
 //    NotPublishing
 //    GetUserMedia
 //    BindingMedia
 //    MediaBound
 //    PublishingToSession
 //    Publishing
 //    Failed
+//    Destroyed
 //
 //
 // Valid Transitions:
 //    NotPublishing ->
 //        GetUserMedia
 //
 //    GetUserMedia ->
 //        BindingMedia
@@ -14001,52 +16836,57 @@ OTHelpers.centerElement = function(eleme
 //
 //    Publishing ->
 //        NotPublishing               (Unpublish
 //      | Failed                      (Failure Reasons -> loss of network, media error, anything
 //                                    (that causes *all* Peer Connections to fail (less than all
 //                                    (failing is just an error, all is failure)
 //      | NotPublishing               (destroy()
 //
-//    Failed ->                       (Terminal state
+//    Failed ->
+//       Destroyed
+//
+//    Destroyed ->                    (Terminal state
 //
 //
 
   var validStates = [
       'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound',
-      'PublishingToSession', 'Publishing', 'Failed'
+      'PublishingToSession', 'Publishing', 'Failed',
+      'Destroyed'
     ],
 
     validTransitions = {
-      NotPublishing: ['NotPublishing', 'GetUserMedia'],
-      GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing'],
-      BindingMedia: ['MediaBound', 'Failed', 'NotPublishing'],
-      MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed'],
-      PublishingToSession: ['NotPublishing', 'Publishing', 'Failed'],
-      Publishing: ['NotPublishing', 'MediaBound', 'Failed'],
-      Failed: []
+      NotPublishing: ['NotPublishing', 'GetUserMedia', 'Destroyed'],
+      GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing', 'Destroyed'],
+      BindingMedia: ['MediaBound', 'Failed', 'NotPublishing', 'Destroyed'],
+      MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed', 'Destroyed'],
+      PublishingToSession: ['NotPublishing', 'Publishing', 'Failed', 'Destroyed'],
+      Publishing: ['NotPublishing', 'MediaBound', 'Failed', 'Destroyed'],
+      Failed: ['Destroyed'],
+      Destroyed: []
     },
 
     initialState = 'NotPublishing';
 
   OT.PublishingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
 
-  Object.defineProperties(OT.PublishingState.prototype, {
-    attemptingToPublish: {
-      get: function() {
-        return [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ]
-          .indexOf(this.current) !== -1;
-      }
-    },
-
-    publishing: {
-      get: function() { return this.current === 'Publishing'; }
-    }
-  });
-
+  OT.PublishingState.prototype.isDestroyed = function() {
+    return this.current === 'Destroyed';
+  };
+
+  OT.PublishingState.prototype.isAttemptingToPublish = function() {
+    return OT.$.arrayIndexOf(
+      [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ],
+      this.current) !== -1;
+  };
+
+  OT.PublishingState.prototype.isPublishing = function() {
+    return this.current === 'Publishing';
+  };
 
 })(window);
 !(function() {
 
   // The default constraints
   var defaultConstraints = {
     audio: true,
     video: true
@@ -14118,17 +16958,16 @@ OTHelpers.centerElement = function(eleme
         _loaded = false,
         _publishProperties,
         _publishStartTime,
         _microphone,
         _chrome,
         _analytics = new OT.Analytics(),
         _validResolutions,
         _validFrameRates = [ 1, 7, 15, 30 ],
-        _qosIntervals = {},
         _prevStats,
         _state,
         _iceServers;
 
     _validResolutions = {
       '320x240': {width: 320, height: 240},
       '640x480': {width: 640, height: 480},
       '1280x720': {width: 1280, height: 720}
@@ -14153,68 +16992,68 @@ OTHelpers.centerElement = function(eleme
     var logAnalyticsEvent = function(action, variation, payloadType, payload) {
           _analytics.logEvent({
             action: action,
             variation: variation,
             'payload_type': payloadType,
             payload: payload,
             'session_id': _session ? _session.sessionId : null,
             'connection_id': _session &&
-              _session.connected ? _session.connection.connectionId : null,
+              _session.isConnected() ? _session.connection.connectionId : null,
             'partner_id': _session ? _session.apiKey : OT.APIKEY,
             streamId: _stream ? _stream.id : null,
             'widget_id': _guid,
             'widget_type': 'Publisher'
           });
         },
 
-        recordQOS = function(connectionId) {
+        recordQOS = OT.$.bind(function(connection, parsedStats) {
           var QoSBlob = {
             'widget_type': 'Publisher',
             'stream_type': 'WebRTC',
             sessionId: _session ? _session.sessionId : null,
-            connectionId: _session && _session.connected ? _session.connection.connectionId : null,
+            connectionId: _session && _session.isConnected() ?
+              _session.connection.connectionId : null,
             partnerId: _session ? _session.apiKey : OT.APIKEY,
             streamId: _stream ? _stream.id : null,
             width: _container ? OT.$.width(_container.domElement)  : undefined,
             height: _container ? OT.$.height(_container.domElement)  : undefined,
             widgetId: _guid,
             version: OT.properties.version,
             'media_server_name': _session ? _session.sessionInfo.messagingServer : null,
             p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
             duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0,
-            'remote_connection_id': connectionId
+            'remote_connection_id': connection.id
           };
 
-          // get stats for each connection id
-          _peerConnections[connectionId].getStats(_prevStats, function(stats) {
-            var statIndex;
-            if (stats) {
-              for (statIndex in stats) {
-                QoSBlob[statIndex] = stats[statIndex];
-              }
-            }
-            _analytics.logQOS(QoSBlob);
-          });
-        },
+          _analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) );
+          this.trigger('qos', parsedStats);
+        }, this),
 
         /// Private Events
 
         stateChangeFailed = function(changeFailed) {
           OT.error('Publisher State Change Failed: ', changeFailed.message);
           OT.debug(changeFailed);
         },
 
         onLoaded = function() {
+          if (_state.isDestroyed()) {
+            // The publisher was destroyed before loading finished
+            return;
+          }
+
           OT.debug('OT.Publisher.onLoaded');
 
           _state.set('MediaBound');
+
           // If we have a session and we haven't created the stream yet then
           // wait until that is complete before hiding the loading spinner
-          _container.loading = this.session ? !_stream : false;
+          _container.loading(this.session ? !_stream : false);
+
           _loaded = true;
 
           _createChrome.call(this);
 
           this.trigger('initSuccess');
           this.trigger('loaded', this);
         },
 
@@ -14240,32 +17079,37 @@ OTHelpers.centerElement = function(eleme
 
           cleanupLocalStream();
           _webRTCStream = webOTStream;
 
           _microphone = new OT.Microphone(_webRTCStream, !_publishProperties.publishAudio);
           this.publishVideo(_publishProperties.publishVideo &&
             _webRTCStream.getVideoTracks().length > 0);
 
+          this.accessAllowed = true;
           this.dispatchEvent(
             new OT.Event(OT.Event.names.ACCESS_ALLOWED, false)
           );
 
-          _targetElement = new OT.VideoElement({
-            attributes: {muted:true}
-          });
-
-          _targetElement.on({
-            streamBound: onLoaded,
-            loadError: onLoadFailure,
-            error: onVideoError
-          }, this)
-            .bindToStream(_webRTCStream);
-
-          _container.video = _targetElement;
+          var videoContainerOptions = {
+            muted: true,
+            error: OT.$.bind(onVideoError, this)
+          };
+
+          _targetElement = _container.bindVideo(_webRTCStream,
+                                            videoContainerOptions,
+                                            OT.$.bind(function(err) {
+            if (err) {
+              onLoadFailure.call(this, err);
+              return;
+            }
+
+            onLoaded.call(this);
+          }, this));
+
         },
 
         onStreamAvailableError = function(error) {
           OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
 
           _state.set('Failed');
           this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
               error.message));
@@ -14384,79 +17228,77 @@ OTHelpers.centerElement = function(eleme
           OT.error('OT.Publisher.onVideoError');
 
           var message = errorReason + (errorCode ? ' (' + errorCode + ')' : '');
           logAnalyticsEvent('stream', null, 'reason',
             'Publisher while playing stream: ' + message);
 
           _state.set('Failed');
 
-          if (_state.attemptingToPublish) {
+          if (_state.isAttemptingToPublish()) {
             this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
                 message));
           } else {
             this.trigger('error', message);
           }
 
           OT.handleJsException('Publisher error playing stream: ' + message,
           OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
             session: _session,
             target: this
           });
         },
 
         onPeerDisconnected = function(peerConnection) {
           OT.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
 
-          this.cleanupSubscriber(peerConnection.remoteConnection.id);
+          this.cleanupSubscriber(peerConnection.remoteConnection().id);
         },
 
         onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
           logAnalyticsEvent('publish', 'Failure', 'reason|hasRelayCandidates',
             (prefix ? prefix : '') + [':Publisher PeerConnection with connection ' +
               (peerConnection && peerConnection.remoteConnection &&
-              peerConnection.remoteConnection.id)  + ' failed: ' +
-              reason, peerConnection.hasRelayCandidates
+              peerConnection.remoteConnection().id)  + ' failed: ' +
+              reason, peerConnection.hasRelayCandidates()
           ].join('|'));
 
           OT.handleJsException('Publisher PeerConnection Error: ' + reason,
           OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
             session: _session,
             target: this
           });
 
           // We don't call cleanupSubscriber as it also logs a
           // disconnected analytics event, which we don't want in this
           // instance. The duplication is crufty though and should
           // be tidied up.
-          clearInterval(_qosIntervals[peerConnection.remoteConnection.id]);
-          delete _qosIntervals[peerConnection.remoteConnection.id];
-
-          delete _peerConnections[peerConnection.remoteConnection.id];
+
+          delete _peerConnections[peerConnection.remoteConnection().id];
         },
 
         /// Private Helpers
 
         // Assigns +stream+ to this publisher. The publisher listens
         // for a bunch of events on the stream so it can respond to
         // changes.
-        assignStream = function(stream) {
-          _stream = stream;
+        assignStream = OT.$.bind(function(stream) {
+          this.stream = _stream = stream;
           _stream.on('destroyed', this.disconnect, this);
 
           _state.set('Publishing');
-          _container.loading = !_loaded;
+          _container.loading(!_loaded);
           _publishStartTime = new Date();
 
           this.trigger('publishComplete', null, this);
-      
+
           this.dispatchEvent(new OT.StreamEvent('streamCreated', stream, null, false));
-          
+
           logAnalyticsEvent('publish', 'Success', 'streamType:streamId', 'WebRTC:' + _streamId);
-        }.bind(this),
+        }, this),
 
         // Clean up our LocalMediaStream
         cleanupLocalStream = function() {
           if (_webRTCStream) {
             // Stop revokes our access cam and mic access for this instance
             // of localMediaStream.
             _webRTCStream.stop();
             _webRTCStream = null;
@@ -14468,39 +17310,35 @@ OTHelpers.centerElement = function(eleme
 
           if (!peerConnection) {
             var startConnectingTime = OT.$.now();
 
             logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
 
             // Cleanup our subscriber when they disconnect
             remoteConnection.on('destroyed',
-              this.cleanupSubscriber.bind(this, remoteConnection.id));
+              OT.$.bind(this.cleanupSubscriber, this, remoteConnection.id));
 
             peerConnection = _peerConnections[remoteConnection.id] = new OT.PublisherPeerConnection(
               remoteConnection,
               _session,
               _streamId,
               _webRTCStream
             );
 
             peerConnection.on({
               connected: function() {
                 logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
                   parseInt(OT.$.now() - startConnectingTime, 10),
-                  peerConnection.hasRelayCandidates
+                  peerConnection.hasRelayCandidates()
                 ].join('|'));
-
-                // start recording the QoS for this peer connection
-                _qosIntervals[remoteConnection.id] = setInterval(function() {
-                  recordQOS(remoteConnection.id);
-                }, 30000);
               },
               disconnected: onPeerDisconnected,
-              error: onPeerConnectionFailure
+              error: onPeerConnectionFailure,
+              qos: recordQOS
             }, this);
 
             peerConnection.init(_iceServers);
           }
 
           return peerConnection;
         },
 
@@ -14577,22 +17415,22 @@ OTHelpers.centerElement = function(eleme
             }),
 
             archive: new OT.Chrome.Archiving({
               show: this.getStyle('showArchiveStatus'),
               archiving: false
             })
 
           }).on({
-            muted: this.publishAudio.bind(this, false),
-            unmuted: this.publishAudio.bind(this, true)
+            muted: OT.$.bind(this.publishAudio, this, false),
+            unmuted: OT.$.bind(this.publishAudio, this, true)
           });
         },
 
-        reset = function() {
+        reset = OT.$.bind(function() {
           if (_chrome) {
             _chrome.destroy();
             _chrome = null;
           }
 
           this.disconnect();
 
           _microphone = null;
@@ -14604,126 +17442,189 @@ OTHelpers.centerElement = function(eleme
 
           cleanupLocalStream();
 
           if (_container) {
             _container.destroy();
             _container = null;
           }
 
-          if (this.session) {
-            this._.unpublishFromSession(this.