Merge f-t to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 31 Jan 2015 09:06:14 -0800
changeset 226906 426bc5ee47d9ba4b95718085485b2481b55e4a37
parent 226888 6f9b69780bf429b72fdd39b5bdf9f05e50e4b1b1 (current diff)
parent 226905 54996bae1084895d08042aad2a37d25a39f4aebe (diff)
child 226928 c2359a6a6958ba56b319e30f4e34d3f838bb0dee
push id54950
push userphilringnalda@gmail.com
push dateSat, 31 Jan 2015 17:14:09 +0000
treeherdermozilla-inbound@37cbadfe1bc1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.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 f-t to m-c, a=merge
browser/themes/linux/aboutTabCrashed.css
new file mode 100644
--- /dev/null
+++ b/browser/base/content/aboutTabCrashed.css
@@ -0,0 +1,8 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html:not(.crashDumpSubmitted) #reportSent,
+html:not(.crashDumpAvailable) #report-box {
+  display: none;
+}
--- a/browser/base/content/aboutTabCrashed.js
+++ b/browser/base/content/aboutTabCrashed.js
@@ -7,27 +7,41 @@ function parseQueryString() {
   let queryString = url.replace(/^about:tabcrashed?e=tabcrashed/, "");
 
   let titleMatch = queryString.match(/d=([^&]*)/);
   return titleMatch && titleMatch[1] ? decodeURIComponent(titleMatch[1]) : "";
 }
 
 document.title = parseQueryString();
 
-addEventListener("DOMContentLoaded", () => {
-  let tryAgain = document.getElementById("tryAgain");
-  let sendCrashReport = document.getElementById("checkSendReport");
+function shouldSendReport() {
+  if (!document.documentElement.classList.contains("crashDumpAvailable"))
+    return false;
+  return document.getElementById("sendReport").checked;
+}
+
+function sendEvent(message) {
+  let event = new CustomEvent("AboutTabCrashedMessage", {
+    bubbles: true,
+    detail: {
+      message,
+      sendCrashReport: shouldSendReport(),
+    },
+  });
 
-  tryAgain.addEventListener("click", () => {
-    let event = new CustomEvent("AboutTabCrashedTryAgain", {
-      bubbles: true,
-      detail: {
-        sendCrashReport: sendCrashReport.checked,
-      },
-    });
+  document.dispatchEvent(event);
+}
+
+function closeTab() {
+  sendEvent("closeTab");
+}
 
-    document.dispatchEvent(event);
-  });
-});
+function restoreTab() {
+  sendEvent("restoreTab");
+}
+
+function restoreAll() {
+  sendEvent("restoreAll");
+}
 
 // Error pages are loaded as LOAD_BACKGROUND, so they don't get load events.
 var event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true});
 document.dispatchEvent(event);
--- a/browser/base/content/aboutTabCrashed.xhtml
+++ b/browser/base/content/aboutTabCrashed.xhtml
@@ -7,43 +7,51 @@
 <!DOCTYPE html [
   <!ENTITY % htmlDTD
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "DTD/xhtml1-strict.dtd">
   %htmlDTD;
   <!ENTITY % globalDTD
     SYSTEM "chrome://global/locale/global.dtd">
   %globalDTD;
-  <!ENTITY % browserDTD
-    SYSTEM "chrome://browser/locale/browser.dtd">
-  %browserDTD;
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
   %brandDTD;
-
+  <!ENTITY % tabCrashedDTD
+    SYSTEM "chrome://browser/locale/aboutTabCrashed.dtd">
+  %tabCrashedDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <link rel="stylesheet" type="text/css" media="all"
           href="chrome://global/skin/in-content/info-pages.css"/>
     <link rel="stylesheet" type="text/css" media="all"
+          href="chrome://browser/content/aboutTabCrashed.css"/>
+    <link rel="stylesheet" type="text/css" media="all"
           href="chrome://browser/skin/aboutTabCrashed.css"/>
   </head>
 
   <body dir="&locale.dir;">
     <div class="container">
       <div class="title">
         <h1 class="title-text">&tabCrashed.header;</h1>
       </div>
       <p>&tabCrashed.message;</p>
 
       <div id="report-box">
-        <input type="checkbox" id="checkSendReport" checked="checked"/>
-        <label for="checkSendReport">&tabCrashed.checkSendReport;</label>
+        <input type="checkbox" id="sendReport" checked="checked"/>
+        <label for="sendReport">&tabCrashed.sendReport;</label>
       </div>
 
+      <p id="reportSent">&tabCrashed.reportSent;</p>
+
       <div class="button-container">
-        <button id="tryAgain">&tabCrashed.tryAgain;</button>
+        <button id="closeTab" onclick="closeTab()">
+          &tabCrashed.closeTab;</button>
+        <button id="restoreTab" onclick="restoreTab()">
+          &tabCrashed.restoreTab;</button>
+        <button id="restoreAll" onclick="restoreAll()" autofocus="true" class="primary">
+          &tabCrashed.restoreAll;</button>
       </div>
     </div>
   </body>
   <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutTabCrashed.js"/>
 </html>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -266,16 +266,22 @@ XPCOMUtils.defineLazyServiceGetter(this,
 #endif
 
 XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() {
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuParent();
 });
 
+function* browserWindows() {
+  let windows = Services.wm.getEnumerator("navigator:browser");
+  while (windows.hasMoreElements())
+    yield windows.getNext();
+}
+
 /**
 * We can avoid adding multiple load event listeners and save some time by adding
 * one listener that calls all real handlers.
 */
 function pageShowEventHandlers(persisted) {
   XULBrowserWindow.asyncUpdateUI();
 }
 
@@ -1111,17 +1117,17 @@ var gBrowserInit = {
     });
 
     gBrowser.addEventListener("AboutTabCrashedLoad", function(event) {
 #ifdef MOZ_CRASHREPORTER
       TabCrashReporter.onAboutTabCrashedLoad(gBrowser.getBrowserForDocument(event.target));
 #endif
     }, false, true);
 
-    gBrowser.addEventListener("AboutTabCrashedTryAgain", function(event) {
+    gBrowser.addEventListener("AboutTabCrashedMessage", function(event) {
       let ownerDoc = event.originalTarget;
 
       if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) {
         return;
       }
 
       let isTopFrame = (ownerDoc.defaultView.parent === ownerDoc.defaultView);
       if (!isTopFrame) {
@@ -1129,18 +1135,33 @@ var gBrowserInit = {
       }
 
       let browser = gBrowser.getBrowserForDocument(ownerDoc);
 #ifdef MOZ_CRASHREPORTER
       if (event.detail.sendCrashReport) {
         TabCrashReporter.submitCrashReport(browser);
       }
 #endif
+
       let tab = gBrowser.getTabForBrowser(browser);
-      SessionStore.reviveCrashedTab(tab);
+      switch (event.detail.message) {
+      case "closeTab":
+        gBrowser.removeTab(tab, { animate: true });
+        break;
+      case "restoreTab":
+        SessionStore.reviveCrashedTab(tab);
+        break;
+      case "restoreAll":
+        for (let browserWin of browserWindows()) {
+          for (let tab of window.gBrowser.tabs) {
+            SessionStore.reviveCrashedTab(tab);
+          }
+        }
+        break;
+      }
     }, false, true);
 
     let uriToLoad = this._getUriToLoad();
     if (uriToLoad && uriToLoad != "about:blank") {
       if (uriToLoad instanceof Ci.nsISupportsArray) {
         let count = uriToLoad.Count();
         let specs = [];
         for (let i = 0; i < count; i++) {
@@ -6466,21 +6487,19 @@ function WindowIsClosing()
 function warnAboutClosingWindow() {
   // Popups aren't considered full browser windows; we also ignore private windows.
   let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window) &&
         !PrivateBrowsingUtils.permanentPrivateBrowsing;
   if (!isPBWindow && !toolbar.visible)
     return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL);
 
   // Figure out if there's at least one other browser window around.
-  let e = Services.wm.getEnumerator("navigator:browser");
   let otherPBWindowExists = false;
   let nonPopupPresent = false;
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
+  for (let win of browserWindows()) {
     if (!win.closed && win != window) {
       if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win))
         otherPBWindowExists = true;
       if (win.toolbar.visible)
         nonPopupPresent = true;
       // If the current window is not in private browsing mode we don't need to 
       // look for other pb windows, we can leave the loop when finding the 
       // first non-popup window. If however the current window is in private 
@@ -7569,19 +7588,17 @@ function switchToTabHavingURI(aURI, aOpe
     aURI = Services.io.newURI(aURI, null, null);
 
   let isBrowserWindow = !!window.gBrowser;
 
   // Prioritise this window.
   if (isBrowserWindow && switchIfURIInWindow(window))
     return true;
 
-  let winEnum = Services.wm.getEnumerator("navigator:browser");
-  while (winEnum.hasMoreElements()) {
-    let browserWin = winEnum.getNext();
+  for (let browserWin of browserWindows()) {
     // Skip closed (but not yet destroyed) windows,
     // and the current window (which was checked earlier).
     if (browserWin.closed || browserWin == window)
       continue;
     if (switchIfURIInWindow(browserWin))
       return true;
   }
 
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -46,19 +46,20 @@ tabpanels {
 
 /* Apply crisp rendering for favicons at exactly 2dppx resolution */
 @media (resolution: 2dppx) {
   .tab-icon-image {
     image-rendering: -moz-crisp-edges;
   }
 }
 
-.tab-icon-image:not([src]):not([pinned]),
+.tab-icon-image:not([src]):not([pinned]):not([crashed]),
 .tab-throbber:not([busy]),
-.tab-throbber[busy] + .tab-icon-image {
+.tab-icon-image[busy],
+.tab-icon-overlay[busy] {
   display: none;
 }
 
 .closing-tabs-spacer {
   pointer-events: none;
 }
 
 .tabbrowser-tabs:not(:hover) > .tabbrowser-arrowscrollbox > .closing-tabs-spacer {
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -659,19 +659,23 @@
                 // It's okay to clear what the user typed when we start
                 // loading a document. If the user types, this counter gets
                 // set to zero, if the document load ends without an
                 // onLocationChange, this counter gets decremented
                 // (so we keep it while switching tabs after failed loads)
                 // We need to add 2 because loadURIWithFlags may have
                 // cancelled a pending load which would have cleared
                 // its anchor scroll detection temporary increment.
-                if (aWebProgress.isTopLevel)
+                if (aWebProgress.isTopLevel) {
                   this.mBrowser.userTypedClear += 2;
 
+                  // If the browser is loading it must not be crashed anymore
+                  this.mTab.removeAttribute("crashed");
+                }
+
                 if (this._shouldShowProgress(aRequest)) {
                   if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
                     this.mTab.setAttribute("busy", "true");
                     if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD))
                       this.mTabBrowser.setTabTitleLoading(this.mTab);
                   }
 
                   if (this.mTab.selected)
@@ -1479,16 +1483,20 @@
             aBrowser.permanentKey = permanentKey;
             parent.appendChild(aBrowser);
 
             // Restore the progress listener.
             aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
 
             if (aShouldBeRemote) {
               tab.setAttribute("remote", "true");
+              // Switching the browser to be remote will connect to a new child
+              // process so the browser can no longer be considered to be
+              // crashed.
+              tab.removeAttribute("crashed");
             } else {
               tab.removeAttribute("remote");
               aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
             }
 
             if (wasActive)
               aBrowser.focus();
 
@@ -3574,16 +3582,17 @@
           let icon = browser.mIconURL;
 
           this.updateBrowserRemotenessByURL(browser, "about:tabcrashed");
 
           browser.setAttribute("crashedPageTitle", title);
           browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
           browser.removeAttribute("crashedPageTitle");
           let tab = this.getTabForBrowser(browser);
+          tab.setAttribute("crashed", true);
           this.setIcon(tab, icon);
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="tabbrowser-tabbox"
            extends="chrome://global/content/bindings/tabbox.xml#tabbox">
@@ -4975,21 +4984,24 @@
                     class="tab-background-end"/>
         </xul:hbox>
         <xul:hbox xbl:inherits="pinned,selected,titlechanged"
                   class="tab-content" align="center">
           <xul:image xbl:inherits="fadein,pinned,busy,progress,selected"
                      class="tab-throbber"
                      role="presentation"
                      layer="true" />
-          <xul:image xbl:inherits="src=image,fadein,pinned,selected"
+          <xul:image xbl:inherits="src=image,fadein,pinned,selected,busy,crashed"
                      anonid="tab-icon-image"
                      class="tab-icon-image"
                      validate="never"
                      role="presentation"/>
+          <xul:image xbl:inherits="crashed,busy"
+                     class="tab-icon-overlay"
+                     role="presentation"/>
           <xul:label flex="1"
                      anonid="tab-label"
                      xbl:inherits="value=visibleLabel,crop,accesskey,fadein,pinned,selected"
                      class="tab-text tab-label"
                      role="presentation"/>
           <xul:toolbarbutton anonid="close-button"
                              xbl:inherits="fadein,pinned,selected"
                              class="tab-close-button close-icon"/>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -64,16 +64,17 @@ browser.jar:
 
         content/browser/certerror/aboutCertError.xhtml     (content/aboutcerterror/aboutCertError.xhtml)
         content/browser/certerror/aboutCertError.css       (content/aboutcerterror/aboutCertError.css)
 
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutSocialError.xhtml        (content/aboutSocialError.xhtml)
         content/browser/aboutProviderDirectory.xhtml  (content/aboutProviderDirectory.xhtml)
+        content/browser/aboutTabCrashed.css           (content/aboutTabCrashed.css)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/content.js                    (content/content.js)
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -1,18 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Shared conversation window styles */
-.standalone .video-layout-wrapper,
-.conversation .media video {
-  background-color: #444;
-}
-
 .conversation {
   position: relative;
 }
 
 .conversation-toolbar {
   z-index: 999; /* required to have it superimposed to the video element */
   border: 1px solid #5a5a5a;
   border-left: 0;
@@ -503,28 +498,45 @@
   left: 0;
   right: 0;
 }
 
 /*
  * XXX this approach is fragile because it makes assumptions
  * about the generated OT markup, any change will break it
  */
-.local-stream.local-stream-audio,
-.standalone .OT_subscriber .OT_video-poster,
-.fx-embedded .OT_video-container .OT_video-poster,
-.local-stream-audio .OT_publisher .OT_video-poster {
+
+/*
+ * For any audio-only streams, we want to display our own background
+ */
+.OT_audio-only .OT_widget-container .OT_video-poster {
   background-image: url("../img/audio-call-avatar.svg");
   background-repeat: no-repeat;
   background-color: #4BA6E7;
   background-size: contain;
   background-position: center;
 }
 
 /*
+ * Audio-only. For local streams, cancel out the SDK's opacity of 0.25.
+ * For remote streams we leave them shaded, as otherwise its too bright.
+ */
+.local-stream-audio .OT_publisher .OT_video-poster {
+  opacity: 1
+}
+
+/*
+ * In audio-only mode, don't display the video element, doing so interferes
+ * with the background opacity of the video-poster element.
+ */
+.OT_audio-only .OT_widget-container .OT_video-element {
+  display: none;
+}
+
+/*
  * Ensure that the publisher (i.e. local) video is never cropped, so that it's
  * not possible for someone to be presented with a picture that displays
  * (for example) a person from the neck up, even though the camera is capturing
  * and transmitting a picture of that person from the waist up.
  *
  * The !importants are necessary to override the SDK attempts to avoid
  * letterboxing entirely.
  *
@@ -532,30 +544,30 @@
  * supported something like a "testMediaToStreamURI" parameter that it would
  * use to source the stream rather than the output of gUM, it wouldn't be too
  * hard to generate a video with a 1 pixel border at the edges that one could
  * at least visually see wasn't being cropped.
  *
  * Another less ugly possibility would be to work with Ted Mielczarek to use
  * the fake camera drivers he has for Linux.
  */
-.room-conversation .OT_publisher .OT_video-container {
+.room-conversation .OT_publisher .OT_widget-container {
   height: 100% !important;
   width: 100% !important;
   top: 0 !important;
   left: 0 !important;
   background-color: transparent; /* avoid visually obvious letterboxing */
 }
 
-.room-conversation .OT_publisher .OT_video-container video {
+.room-conversation .OT_publisher .OT_widget-container video {
   background-color: transparent; /* avoid visually obvious letterboxing */
 }
 
-.fx-embedded .room-conversation .room-preview .OT_publisher .OT_video-container,
-.fx-embedded .room-conversation .room-preview .OT_publisher .OT_video-container video {
+.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container,
+.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container video {
   /* Desktop conversation window room preview local stream actually wants
      a black background */
   background-color: #000;
 }
 
 .fx-embedded .media.nested {
   min-height: 200px;
 }
@@ -668,17 +680,16 @@ html, .fx-embedded, #main,
 
   .standalone .remote_wrapper {
     position: relative;
     width: 100%;
     height: 100%;
   }
 
   .standalone {
-    max-width: 1000px;
     margin: 0 auto;
   }
 }
 
 @media screen and (max-width:640px) {
   .standalone .video-layout-wrapper,
   .standalone .conversation {
     height: 100%;
@@ -900,21 +911,16 @@ html, .fx-embedded, #main,
   background: #000;
 }
 
 .standalone .room-conversation .video_wrapper.remote_wrapper {
   background-color: #4e4e4e;
   width: 75%;
 }
 
-.standalone .room-conversation .local-stream {
-  width: 33%;
-  height: 26.5%;
-}
-
 .standalone .room-conversation .conversation-toolbar {
   background: #000;
   border: none;
 }
 
 .standalone .room-conversation .conversation-toolbar .btn-hangup-entry {
   display: block;
 }
@@ -940,21 +946,16 @@ html, .fx-embedded, #main,
   }
   .standalone .room-conversation-wrapper .video-layout-wrapper {
     /* 50px: header's height; 25px: footer's height */
     height: calc(100% - 50px - 25px);
   }
   .standalone .room-conversation .video_wrapper.remote_wrapper {
     width: 100%;
   }
-  .standalone .room-conversation .local-stream {
-    /* Assumes 4:3 aspect ratio */
-    width: 180px;
-    height: 135px;
-  }
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
   .standalone .media.nested {
     /* This forces the remote video stream to fit within wrapper's height */
     min-height: 0px;
   }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -172,16 +172,25 @@ loop.shared.actions = (function() {
 
     /**
      * Used for notifying that the media is now up for the call.
      */
     MediaConnected: Action.define("mediaConnected", {
     }),
 
     /**
+     * Used for notifying that the dimensions of a stream just changed. Also
+     * dispatched when a stream connects for the first time.
+     */
+    VideoDimensionsChanged: Action.define("videoDimensionsChanged", {
+      videoType: String,
+      dimensions: Object
+    }),
+
+    /**
      * Used to mute or unmute a stream
      */
     SetMute: Action.define("setMute", {
       // The part of the stream to enable, e.g. "audio" or "video"
       type: String,
       // Whether or not to enable the stream.
       enabled: Boolean
     }),
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -63,17 +63,19 @@ loop.store.ActiveRoomStore = (function()
         roomState: ROOM_STATES.INIT,
         audioMuted: false,
         videoMuted: false,
         failureReason: undefined,
         // Tracks if the room has been used during this
         // session. 'Used' means at least one call has been placed
         // with it. Entering and leaving the room without seeing
         // anyone is not considered as 'used'
-        used: false
+        used: false,
+        localVideoDimensions: {},
+        remoteVideoDimensions: {}
       };
     },
 
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
@@ -114,17 +116,18 @@ loop.store.ActiveRoomStore = (function()
         "joinedRoom",
         "connectedToSdkServers",
         "connectionFailure",
         "setMute",
         "remotePeerDisconnected",
         "remotePeerConnected",
         "windowUnload",
         "leaveRoom",
-        "feedbackComplete"
+        "feedbackComplete",
+        "videoDimensionsChanged"
       ]);
     },
 
     /**
      * Execute setupWindowData event action from the dispatcher. This gets
      * the room data from the mozLoop api, and dispatches an UpdateRoomInfo event.
      * It also dispatches JoinRoom as this action is only applicable to the desktop
      * client, and needs to auto-join.
@@ -472,13 +475,30 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * When feedback is complete, we reset the room to the initial state.
      */
     feedbackComplete: function() {
       // Note, that we want some values, such as the windowId, so we don't
       // do a full reset here.
       this.setStoreState(this.getInitialStoreState());
+    },
+
+    /**
+     * Handles a change in dimensions of a video stream and updates the store data
+     * with the new dimensions of a local or remote stream.
+     *
+     * @param {sharedActions.VideoDimensionsChanged} actionData
+     */
+    videoDimensionsChanged: function(actionData) {
+      // NOTE: in the future, when multiple remote video streams are supported,
+      //       we'll need to make this support multiple remotes as well. Good
+      //       starting point for video tiling.
+      var storeProp = (actionData.isLocal ? "local" : "remote") + "VideoDimensions";
+      var nextState = {};
+      nextState[storeProp] = this.getStoreState()[storeProp];
+      nextState[storeProp][actionData.videoType] = actionData.dimensions;
+      this.setStoreState(nextState);
     }
   });
 
   return ActiveRoomStore;
 })();
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -152,39 +152,227 @@ loop.shared.mixins = (function() {
     }
   };
 
   /**
    * Media setup mixin. Provides a common location for settings for the media
    * elements and handling updates of the media containers.
    */
   var MediaSetupMixin = {
+    _videoDimensionsCache: {
+      local: {},
+      remote: {}
+    },
+
     componentDidMount: function() {
-      rootObject.addEventListener('orientationchange', this.updateVideoContainer);
-      rootObject.addEventListener('resize', this.updateVideoContainer);
+      rootObject.addEventListener("orientationchange", this.updateVideoContainer);
+      rootObject.addEventListener("resize", this.updateVideoContainer);
     },
 
     componentWillUnmount: function() {
-      rootObject.removeEventListener('orientationchange', this.updateVideoContainer);
-      rootObject.removeEventListener('resize', this.updateVideoContainer);
+      rootObject.removeEventListener("orientationchange", this.updateVideoContainer);
+      rootObject.removeEventListener("resize", this.updateVideoContainer);
+    },
+
+    /**
+     * Whenever the dimensions change of a video stream, this function is called
+     * by `updateVideoDimensions` to store the new values and notifies the callee
+     * if the dimensions changed compared to the currently stored values.
+     *
+     * @param  {String} which         Type of video stream. May be 'local' or 'remote'
+     * @param  {Object} newDimensions Object containing 'width' and 'height' properties
+     * @return {Boolean}              `true` when the dimensions have changed,
+     *                                `false` if not
+     */
+    _updateDimensionsCache: function(which, newDimensions) {
+      var cache = this._videoDimensionsCache[which];
+      var cacheKeys = Object.keys(cache);
+      var changed = false;
+      Object.keys(newDimensions).forEach(function(videoType) {
+        if (cacheKeys.indexOf(videoType) === -1) {
+          cache[videoType] = newDimensions[videoType];
+          cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]);
+          changed = true;
+          return;
+        }
+        if (cache[videoType].width !== newDimensions[videoType].width) {
+          cache[videoType].width = newDimensions[videoType].width;
+          changed = true;
+        }
+        if (cache[videoType].height !== newDimensions[videoType].height) {
+          cache[videoType].height = newDimensions[videoType].height;
+          changed = true;
+        }
+        if (changed) {
+          cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]);
+        }
+      }, this);
+      return changed;
+    },
+
+    /**
+     * Whenever the dimensions change of a video stream, this function is called
+     * to process these changes and possibly trigger an update to the video
+     * container elements.
+     *
+     * @param  {Object} localVideoDimensions  Object containing 'width' and 'height'
+     *                                        properties grouped by stream name
+     * @param  {Object} remoteVideoDimensions Object containing 'width' and 'height'
+     *                                        properties grouped by stream name
+     */
+    updateVideoDimensions: function(localVideoDimensions, remoteVideoDimensions) {
+      var localChanged = this._updateDimensionsCache("local", localVideoDimensions);
+      var remoteChanged = this._updateDimensionsCache("remote", remoteVideoDimensions);
+      if (localChanged || remoteChanged) {
+        this.updateVideoContainer();
+      }
+    },
+
+    /**
+     * Get the aspect ratio of a width/ height pair, which should be the dimensions
+     * of a stream. The returned object is an aspect ratio indexed by 1; the leading
+     * size has a value smaller than 1 and the slave size has a value of 1.
+     * this is exactly the same as notations like 4:3 and 16:9, which are merely
+     * human-readable forms of their fractional counterparts. 4:3 === 1:0.75 and
+     * 16:9 === 1:0.5625.
+     * So we're using the aspect ratios in their original form, because that's
+     * easier to do calculus with.
+     *
+     * Example:
+     * A stream with dimensions `{ width: 640, height: 480 }` yields an indexed
+     * aspect ratio of `{ width: 1, height: 0.75 }`. This means that the 'height'
+     * will determine the value of 'width' when the stream is stretched or shrunk
+     * to fit inside its container element at the maximum size.
+     *
+     * @param  {Object} dimensions Object containing 'width' and 'height' properties
+     * @return {Object}            Contains the indexed aspect ratio for 'width'
+     *                             and 'height' assigned to the corresponding
+     *                             properties.
+     */
+    getAspectRatio: function(dimensions) {
+      if (dimensions.width === dimensions.height) {
+        return {width: 1, height: 1};
+      }
+      var denominator = Math.max(dimensions.width, dimensions.height);
+      return {
+        width: dimensions.width / denominator,
+        height: dimensions.height / denominator
+      };
+    },
+
+    /**
+     * Retrieve the dimensions of the remote video stream.
+     * Example output:
+     *   {
+     *     width: 680,
+     *     height: 480,
+     *     streamWidth: 640,
+     *     streamHeight: 480,
+     *     offsetX: 20,
+     *     offsetY: 0
+     *   }
+     *
+     * Note: Once we support multiple remote video streams, this function will
+     *       need to be updated.
+     * @return {Object} contains the remote stream dimension properties of its
+     *                  container node, the stream itself and offset of the stream
+     *                  relative to its container node in pixels.
+     */
+    getRemoteVideoDimensions: function() {
+      var remoteVideoDimensions;
+
+      Object.keys(this._videoDimensionsCache.remote).forEach(function(videoType) {
+        var node = this._getElement("." + (videoType === "camera" ? "remote" : videoType));
+        var width = node.offsetWidth;
+        // If the width > 0 then we record its real size by taking its aspect
+        // ratio in account. Due to the 'contain' fit-mode, the stream will be
+        // centered inside the video element.
+        // We'll need to deal with more than one remote video stream whenever
+        // that becomes something we need to support.
+        if (width) {
+          remoteVideoDimensions = {
+            width: width,
+            height: node.offsetHeight
+          };
+          var ratio = this._videoDimensionsCache.remote[videoType].aspectRatio;
+          var leadingAxis = Math.min(ratio.width, ratio.height) === ratio.width ?
+            "width" : "height";
+          var slaveSize = remoteVideoDimensions[leadingAxis] +
+            (remoteVideoDimensions[leadingAxis] * (1 - ratio[leadingAxis]));
+          remoteVideoDimensions.streamWidth = leadingAxis === "width" ?
+            remoteVideoDimensions.width : slaveSize;
+          remoteVideoDimensions.streamHeight = leadingAxis === "height" ?
+            remoteVideoDimensions.height: slaveSize;
+        }
+      }, this);
+
+      // Supply some sensible defaults for the remoteVideoDimensions if no remote
+      // stream is connected (yet).
+      if (!remoteVideoDimensions) {
+        var node = this._getElement(".remote");
+        var width = node.offsetWidth;
+        var height = node.offsetHeight;
+        remoteVideoDimensions = {
+          width: width,
+          height: height,
+          streamWidth: width,
+          streamHeight: height
+        };
+      }
+
+      // Calculate the size of each individual letter- or pillarbox for convenience.
+      remoteVideoDimensions.offsetX = remoteVideoDimensions.width -
+        remoteVideoDimensions.streamWidth
+      if (remoteVideoDimensions.offsetX > 0) {
+        remoteVideoDimensions.offsetX /= 2;
+      }
+      remoteVideoDimensions.offsetY = remoteVideoDimensions.height -
+        remoteVideoDimensions.streamHeight;
+      if (remoteVideoDimensions.offsetY > 0) {
+        remoteVideoDimensions.offsetY /= 2;
+      }
+
+      return remoteVideoDimensions;
     },
 
     /**
      * Used to update the video container whenever the orientation or size of the
      * display area changes.
+     *
+     * Buffer the calls to this function to make sure we don't overflow the stack
+     * with update calls when many 'resize' event are fired, to prevent blocking
+     * the event loop.
      */
     updateVideoContainer: function() {
-      var localStreamParent = this._getElement('.local .OT_publisher');
-      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
-      if (localStreamParent) {
-        localStreamParent.style.width = "100%";
+      if (this._bufferedUpdateVideo) {
+        rootObject.clearTimeout(this._bufferedUpdateVideo);
+        this._bufferedUpdateVideo = null;
       }
-      if (remoteStreamParent) {
-        remoteStreamParent.style.height = "100%";
-      }
+
+      this._bufferedUpdateVideo = rootObject.setTimeout(function() {
+        this._bufferedUpdateVideo = null;
+        var localStreamParent = this._getElement(".local .OT_publisher");
+        var remoteStreamParent = this._getElement(".remote .OT_subscriber");
+        if (localStreamParent) {
+          localStreamParent.style.width = "100%";
+        }
+        if (remoteStreamParent) {
+          remoteStreamParent.style.height = "100%";
+        }
+
+        // Update the position and dimensions of the containers of local video
+        // streams, if necessary. The consumer of this mixin should implement the
+        // actual updating mechanism.
+        Object.keys(this._videoDimensionsCache.local).forEach(function(videoType) {
+          var ratio = this._videoDimensionsCache.local[videoType].aspectRatio
+          if (videoType == "camera" && this.updateLocalCameraPosition) {
+            this.updateLocalCameraPosition(ratio);
+          }
+        }, this);
+      }.bind(this), 0);
     },
 
     /**
      * Returns the default configuration for publishing media on the sdk.
      *
      * @param {Object} options An options object containing:
      * - publishVideo A boolean set to true to publish video when the stream is initiated.
      */
@@ -193,25 +381,21 @@ loop.shared.mixins = (function() {
       if (!"publishVideo" in options) {
         throw new Error("missing option publishVideo");
       }
 
       // height set to 100%" to fix video layout on Google Chrome
       // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
       return {
         insertMode: "append",
+        fitMode: "contain",
         width: "100%",
         height: "100%",
         publishVideo: options.publishVideo,
-        style: {
-          audioLevelDisplayMode: "off",
-          buttonDisplayMode: "off",
-          nameDisplayMode: "off",
-          videoDisabledDisplayMode: "off"
-        }
+        showControls: false,
       };
     },
 
     /**
      * Returns either the required DOMNode
      *
      * @param {String} className The name of the class to get the element for.
      */
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -4,16 +4,17 @@
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.OTSdkDriver = (function() {
 
   var sharedActions = loop.shared.actions;
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
+  var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
 
   /**
    * This is a wrapper for the OT sdk. It is used to translate the SDK events into
    * actions, and instruct the SDK what to do as a result of actions.
    */
   var OTSdkDriver = function(options) {
       if (!options.dispatcher) {
         throw new Error("Missing option dispatcher");
@@ -46,16 +47,17 @@ loop.OTSdkDriver = (function() {
       this.getRemoteElement = actionData.getRemoteElementFunc;
       this.publisherConfig = actionData.publisherConfig;
 
       // At this state we init the publisher, even though we might be waiting for
       // the initial connect of the session. This saves time when setting up
       // the media.
       this.publisher = this.sdk.initPublisher(this.getLocalElement(),
         this.publisherConfig);
+      this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
       this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
       this.publisher.on("accessDenied", this._onPublishDenied.bind(this));
       this.publisher.on("accessDialogOpened",
         this._onAccessDialogOpened.bind(this));
     },
 
     /**
      * Handles the setMute action. Informs the published stream to mute
@@ -86,33 +88,35 @@ loop.OTSdkDriver = (function() {
       this.session = this.sdk.initSession(sessionData.sessionId);
 
       this.session.on("connectionCreated", this._onConnectionCreated.bind(this));
       this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
       this.session.on("connectionDestroyed",
         this._onConnectionDestroyed.bind(this));
       this.session.on("sessionDisconnected",
         this._onSessionDisconnected.bind(this));
+      this.session.on("streamPropertyChanged", this._onStreamPropertyChanged.bind(this));
 
       // This starts the actual session connection.
       this.session.connect(sessionData.apiKey, sessionData.sessionToken,
         this._onConnectionComplete.bind(this));
     },
 
     /**
      * Disconnects the sdk session.
      */
     disconnectSession: function() {
       if (this.session) {
-        this.session.off("streamCreated connectionDestroyed sessionDisconnected");
+        this.session.off("streamCreated streamDestroyed connectionDestroyed " +
+          "sessionDisconnected streamPropertyChanged");
         this.session.disconnect();
         delete this.session;
       }
       if (this.publisher) {
-        this.publisher.off("accessAllowed accessDenied accessDialogOpened");
+        this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
         this.publisher.destroy();
         delete this.publisher;
       }
 
       // Also, tidy these variables ready for next time.
       delete this._sessionConnected;
       delete this._publisherReady;
       delete this._publishedLocalStream;
@@ -229,26 +233,50 @@ loop.OTSdkDriver = (function() {
 
     /**
      * Handles the event when the remote stream is created.
      *
      * @param {StreamEvent} event The event details:
      * https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      */
     _onRemoteStreamCreated: function(event) {
+      if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) {
+        this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
+          isLocal: false,
+          videoType: event.stream.videoType,
+          dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
+        }));
+      }
+
       this.session.subscribe(event.stream,
         this.getRemoteElement(), this.publisherConfig);
 
       this._subscribedRemoteStream = true;
       if (this._checkAllStreamsConnected()) {
         this.dispatcher.dispatch(new sharedActions.MediaConnected());
       }
     },
 
     /**
+     * Handles the event when the local stream is created.
+     *
+     * @param  {StreamEvent} event The event details:
+     * https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
+     */
+    _onLocalStreamCreated: function(event) {
+      if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) {
+        this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
+          isLocal: true,
+          videoType: event.stream.videoType,
+          dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
+        }));
+      }
+    },
+
+    /**
      * Called from the sdk when the media access dialog is opened.
      * Prevents the default action, to prevent the SDK's "allow access"
      * dialog from being shown.
      *
      * @param {OT.Event} event
      */
     _onAccessDialogOpened: function(event) {
       event.preventDefault();
@@ -278,16 +306,29 @@ loop.OTSdkDriver = (function() {
       event.preventDefault();
 
       this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
         reason: FAILURE_DETAILS.MEDIA_DENIED
       }));
     },
 
     /**
+     * Handles publishing of property changes to a stream.
+     */
+    _onStreamPropertyChanged: function(event) {
+      if (event.changedProperty == STREAM_PROPERTIES.VIDEO_DIMENSIONS) {
+        this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
+          isLocal: event.stream.connection.id == this.session.connection.id,
+          videoType: event.stream.videoType,
+          dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
+        }));
+      }
+    },
+
+    /**
      * Publishes the local stream if the session is connected
      * and the publisher is ready.
      */
     _maybePublishLocalStream: function() {
       if (this._sessionConnected && this._publisherReady) {
         // We are clear to publish the stream to the session.
         this.session.publish(this.publisher);
 
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -37,16 +37,22 @@ loop.shared.utils = (function(mozL10n) {
   var FAILURE_DETAILS = {
     MEDIA_DENIED: "reason-media-denied",
     COULD_NOT_CONNECT: "reason-could-not-connect",
     NETWORK_DISCONNECTED: "reason-network-disconnected",
     EXPIRED_OR_INVALID: "reason-expired-or-invalid",
     UNKNOWN: "reason-unknown"
   };
 
+  var STREAM_PROPERTIES = {
+    VIDEO_DIMENSIONS: "videoDimensions",
+    HAS_AUDIO: "hasAudio",
+    HAS_VIDEO: "hasVideo"
+  };
+
   /**
    * Format a given date into an l10n-friendly string.
    *
    * @param {Integer} The timestamp in seconds to format.
    * @return {String} The formatted string.
    */
   function formatDate(timestamp) {
     var date = (new Date(timestamp * 1000));
@@ -133,14 +139,15 @@ loop.shared.utils = (function(mozL10n) {
     );
   }
 
   return {
     CALL_TYPES: CALL_TYPES,
     FAILURE_DETAILS: FAILURE_DETAILS,
     REST_ERRNOS: REST_ERRNOS,
     WEBSOCKET_REASONS: WEBSOCKET_REASONS,
+    STREAM_PROPERTIES: STREAM_PROPERTIES,
     Helper: Helper,
     composeCallUrlEmail: composeCallUrlEmail,
     formatDate: formatDate,
     getBoolPreference: getBoolPreference
   };
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
+++ b/browser/components/loop/content/shared/libs/sdk-content/css/ot.css
@@ -6,40 +6,39 @@
 
 /**
  * OT Base styles
  */
 
 /* Root OT object, this is where our CSS reset happens */
 .OT_root,
 .OT_root * {
-    color: #ffffff;
-    margin: 0;
-    padding: 0;
-    border: 0;
-    font-size: 100%;
-    font-family: Arial, Helvetica, sans-serif;
-    vertical-align: baseline;
+  color: #ffffff;
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font-family: Arial, Helvetica, sans-serif;
+  vertical-align: baseline;
 }
 
-
 /**
  * Specific Element Reset
  */
 
 .OT_root h1,
 .OT_root h2,
 .OT_root h3,
 .OT_root h4,
 .OT_root h5,
 .OT_root h6 {
-    color: #ffffff;
-    font-family: Arial, Helvetica, sans-serif;
-    font-size: 100%;
-    font-weight: bold;
+  color: #ffffff;
+  font-family: Arial, Helvetica, sans-serif;
+  font-size: 100%;
+  font-weight: bold;
 }
 
 .OT_root header {
 
 }
 
 .OT_root footer {
 
@@ -57,68 +56,68 @@
 
 }
 
 .OT_root button {
 
 }
 
 .OT_root strong {
-    font-weight: bold;
+  font-weight: bold;
 }
 
 .OT_root em {
-    font-style: italic;
+  font-style: italic;
 }
 
 .OT_root a,
 .OT_root a:link,
 .OT_root a:visited,
 .OT_root a:hover,
 .OT_root a:active {
-    font-family: Arial, Helvetica, sans-serif;
+  font-family: Arial, Helvetica, sans-serif;
 }
 
-
 .OT_root ul, .OT_root ol {
-    margin: 1em 1em 1em 2em;
+  margin: 1em 1em 1em 2em;
 }
 
 .OT_root ol {
-    list-style: decimal outside;
+  list-style: decimal outside;
 }
 
 .OT_root ul {
-    list-style: disc outside;
+  list-style: disc outside;
 }
 
 .OT_root dl {
-    margin: 4px;
+  margin: 4px;
 }
-    .OT_root dl dt,
-    .OT_root dl dd {
-        float: left;
-        margin: 0;
-        padding: 0;
-    }
+
+.OT_root dl dt,
+.OT_root dl dd {
+  float: left;
+  margin: 0;
+  padding: 0;
+}
 
-    .OT_root dl dt {
-        clear: left;
-        text-align: right;
-        width: 50px;
-    }
-    .OT_root dl dd {
-        margin-left: 10px;
-    }
+.OT_root dl dt {
+  clear: left;
+  text-align: right;
+  width: 50px;
+}
+
+.OT_root dl dd {
+  margin-left: 10px;
+}
 
 .OT_root img {
-    border: 0 none;
+  border: 0 none;
 }
 
-
 /* Modal dialog styles */
 
 /* Modal dialog styles */
 
 .OT_dialog-centering {
   display: table;
   width: 100%;
   height: 100%;
@@ -161,17 +160,16 @@
   right: 18px;
   top: 0;
 }
 
 .OT_dialog-messages {
   text-align: center;
 }
 
-
 .OT_dialog-messages-main {
   margin-bottom: 36px;
   line-height: 36px;
 
   font-weight: 300;
   font-size: 24px;
 }
 
@@ -297,146 +295,145 @@
 
 .OT_dialog-no-natural-margin {
   margin-bottom: 0;
 }
 
 /* Publisher and Subscriber styles */
 
 .OT_publisher, .OT_subscriber {
-    position: relative;
-    min-width: 48px;
-    min-height: 48px;
+  position: relative;
+  min-width: 48px;
+  min-height: 48px;
 }
 
-.OT_publisher video,
-.OT_subscriber video,
-.OT_publisher object,
-.OT_subscriber object {
-    width: 100%;
-    height: 100%;
-}
+.OT_publisher .OT_video-element,
+.OT_subscriber .OT_video-element {
+  display: block;
+  position: absolute;
+  width: 100%;
 
-.OT_publisher object,
-.OT_subscriber object {
+  transform-origin: 0 0;
 }
 
 /* Styles that are applied when the video element should be mirrored */
-.OT_publisher.OT_mirrored video{
-    -webkit-transform: scale(-1, 1);
-    -moz-transform:scale(-1,1);
+.OT_publisher.OT_mirrored .OT_video-element {
+  transform: scale(-1, 1);
+  transform-origin: 50% 50%;
 }
 
 .OT_subscriber_error {
-		background-color: #000;
-		color: #fff;
-		text-align: center;
+  background-color: #000;
+  color: #fff;
+  text-align: center;
 }
 
 .OT_subscriber_error > p {
-		padding: 20px;
+  padding: 20px;
 }
 
 /* The publisher/subscriber name/mute background */
 .OT_publisher .OT_bar,
 .OT_subscriber .OT_bar,
 .OT_publisher .OT_name,
 .OT_subscriber .OT_name,
 .OT_publisher .OT_archiving,
 .OT_subscriber .OT_archiving,
 .OT_publisher .OT_archiving-status,
 .OT_subscriber .OT_archiving-status,
 .OT_publihser .OT_archiving-light-box,
 .OT_subscriber .OT_archiving-light-box {
-   -moz-box-sizing: border-box;
-   -webkit-box-sizing: border-box;
-   -ms-box-sizing: border-box;
-    box-sizing: border-box;
-    top: 0;
-    left: 0;
-    right: 0;
-    display: block;
-    height: 34px;
-    position: absolute;
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  -ms-box-sizing: border-box;
+  box-sizing: border-box;
+  top: 0;
+  left: 0;
+  right: 0;
+  display: block;
+  height: 34px;
+  position: absolute;
 }
 
 .OT_publisher .OT_bar,
 .OT_subscriber .OT_bar {
-    background: rgba(0, 0, 0, 0.4);
+  background: rgba(0, 0, 0, 0.4);
 }
 
 .OT_publisher .OT_edge-bar-item,
 .OT_subscriber .OT_edge-bar-item {
-    z-index: 1; /* required to get audio level meter underneath */
+  z-index: 1; /* required to get audio level meter underneath */
 }
 
 /* The publisher/subscriber name panel/archiving status bar */
 .OT_publisher .OT_name,
 .OT_subscriber .OT_name {
-    background-color: transparent;
-    color: #ffffff;
-    font-size: 15px;
-    line-height: 34px;
-    font-weight: normal;
-    padding: 0 4px 0 36px;
+  background-color: transparent;
+  color: #ffffff;
+  font-size: 15px;
+  line-height: 34px;
+  font-weight: normal;
+  padding: 0 4px 0 36px;
 }
 
 .OT_publisher .OT_archiving-status,
 .OT_subscriber .OT_archiving-status {
-    background: rgba(0, 0, 0, 0.4);
-    top: auto;
-    bottom: 0;
-    left: 34px;
-    padding: 0 4px;
-    color: rgba(255,255,255,0.8);
-    font-size: 15px;
-    line-height: 34px;
-    font-weight: normal;
+  background: rgba(0, 0, 0, 0.4);
+  top: auto;
+  bottom: 0;
+  left: 34px;
+  padding: 0 4px;
+  color: rgba(255, 255, 255, 0.8);
+  font-size: 15px;
+  line-height: 34px;
+  font-weight: normal;
 }
 
 .OT_micro .OT_archiving-status,
 .OT_micro:hover .OT_archiving-status,
 .OT_mini .OT_archiving-status,
 .OT_mini:hover .OT_archiving-status {
-    display: none;
+  display: none;
 }
 
 .OT_publisher .OT_archiving-light-box,
 .OT_subscriber .OT_archiving-light-box {
-    background: rgba(0, 0, 0, 0.4);
-    top: auto;
-    bottom: 0;
-    right: auto;
-    width: 34px;
-    height: 34px;
+  background: rgba(0, 0, 0, 0.4);
+  top: auto;
+  bottom: 0;
+  right: auto;
+  width: 34px;
+  height: 34px;
 }
 
 .OT_archiving-light {
   width: 7px;
   height: 7px;
   -webkit-border-radius: 30px;
   -moz-border-radius: 30px;
   border-radius: 30px;
   position: absolute;
   top: 14px;
   left: 14px;
   background-color: #575757;
   -webkit-box-shadow: 0 0 5px 1px #575757;
   -moz-box-shadow: 0 0 5px 1px #575757;
   box-shadow: 0 0 5px 1px #575757;
 }
+
 .OT_archiving-light.OT_active {
   background-color: #970d13;
   -webkit-animation: OT_pulse 1.3s ease-in;
   -moz-animation: OT_pulse 1.3s ease-in;
   -webkit-animation: OT_pulse 1.3s ease-in;
   -webkit-animation-iteration-count: infinite;
   -moz-animation-iteration-count: infinite;
   -webkit-animation-iteration-count: infinite;
 }
+
 @-moz-keyframes OT_pulse {
   0% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 
   30% {
@@ -458,16 +455,17 @@
   }
 
   100% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 }
+
 @-webkit-keyframes OT_pulse {
   0% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 
   30% {
@@ -489,16 +487,17 @@
   }
 
   100% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 }
+
 @-o-keyframes OT_pulse {
   0% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 
   30% {
@@ -520,16 +519,17 @@
   }
 
   100% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 }
+
 @-ms-keyframes OT_pulse {
   0% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 
   30% {
@@ -551,16 +551,17 @@
   }
 
   100% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 }
+
 @-webkit-keyframes OT_pulse {
   0% {
     -webkit-box-shadow: 0 0 0px 0px #c70019;
     -moz-box-shadow: 0 0 0px 0px #c70019;
     box-shadow: 0 0 0px 0px #c70019;
   }
 
   30% {
@@ -586,25 +587,25 @@
     -moz-box-shadow: 0 0 0px 0px #c70019;
     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;
+  bottom: 0;
+  height: auto;
 }
 
 .OT_mini .OT_name.OT_mode-off,
 .OT_mini .OT_name.OT_mode-on,
 .OT_mini .OT_name.OT_mode-auto,
 .OT_mini:hover .OT_name.OT_mode-auto {
-    display: none;
+  display: none;
 }
 
 .OT_publisher .OT_name,
 .OT_subscriber .OT_name {
     left: 10px;
     right: 37px;
     height: 34px;
     padding-left: 0;
@@ -619,260 +620,273 @@
     text-align: center;
     text-indent: -9999em;
     background-color: transparent;
     background-repeat: no-repeat;
 }
 
 .OT_publisher .OT_mute,
 .OT_subscriber .OT_mute {
-    right: 0;
-    top: 0;
-    border-left: 1px solid rgba(255, 255, 255, 0.2);
-    height: 36px;
-    width: 37px;
+  right: 0;
+  top: 0;
+  border-left: 1px solid rgba(255, 255, 255, 0.2);
+  height: 36px;
+  width: 37px;
 }
 
 .OT_mini .OT_mute,
-.OT_mute.OT_mode-mini,
-.OT_mute.OT_mode-mini-auto {
-    top: 50%;
-    left: 50%;
-    right: auto;
-    margin-top: -18px;
-    margin-left: -18.5px;
-    border-left: none;
+.OT_publisher.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold,
+.OT_subscriber.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold {
+  top: 50%;
+  left: 50%;
+  right: auto;
+  margin-top: -18px;
+  margin-left: -18.5px;
+  border-left: none;
 }
 
 .OT_publisher .OT_mute {
-    background-image: url(../images/rtc/mic-on.png);
-    background-position: 9px 5px;
+  background-image: url(../images/rtc/mic-on.png);
+  background-position: 9px 5px;
 }
 
 .OT_publisher .OT_mute.OT_active {
-    background-image: url(../images/rtc/mic-off.png);
-    background-position: 9px 4px;
+  background-image: url(../images/rtc/mic-off.png);
+  background-position: 9px 4px;
 }
 
 .OT_subscriber .OT_mute {
-    background-image: url(../images/rtc/speaker-on.png);
-    background-position: 8px 7px;
+  background-image: url(../images/rtc/speaker-on.png);
+  background-position: 8px 7px;
 }
 
 .OT_subscriber .OT_mute.OT_active {
-    background-image: url(../images/rtc/speaker-off.png);
-    background-position: 7px 7px;
+  background-image: url(../images/rtc/speaker-off.png);
+  background-position: 7px 7px;
 }
 
 /**
  * Styles for display modes
  *
  * Note: It's important that these completely control the display and opacity
  * attributes, no other selectors should atempt to change them.
  */
 
 /* Default display mode transitions for various chrome elements */
 .OT_publisher .OT_edge-bar-item,
 .OT_subscriber .OT_edge-bar-item {
-    -ms-transition-property: top, bottom, opacity;
-    -ms-transition-duration: 0.5s;
-    -moz-transition-property: top, bottom, opacity;
-    -moz-transition-duration: 0.5s;
-    -webkit-transition-property: top, bottom, opacity;
-    -webkit-transition-duration: 0.5s;
-    -o-transition-property: top, bottom, opacity;
-    -o-transition-duration: 0.5s;
-    transition-property: top, bottom, opacity;
-    transition-duration: 0.5s;
-    transition-timing-function: ease-in;
+  transition-property: top, bottom, opacity;
+  transition-duration: 0.5s;
+  transition-timing-function: ease-in;
 }
 
 .OT_publisher .OT_edge-bar-item.OT_mode-off,
 .OT_subscriber .OT_edge-bar-item.OT_mode-off,
 .OT_publisher .OT_edge-bar-item.OT_mode-auto,
 .OT_subscriber .OT_edge-bar-item.OT_mode-auto,
 .OT_publisher .OT_edge-bar-item.OT_mode-mini-auto,
 .OT_subscriber .OT_edge-bar-item.OT_mode-mini-auto {
-    top: -25px;
-    opacity: 0;
+  top: -25px;
+  opacity: 0;
 }
 
 .OT_mini .OT_mute.OT_mode-auto,
 .OT_publisher .OT_mute.OT_mode-mini-auto,
 .OT_subscriber .OT_mute.OT_mode-mini-auto {
-    top: 50%;
+  top: 50%;
 }
 
 .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
 .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-off,
 .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
 .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
 .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto,
 .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto {
-    top: auto;
-    bottom: -25px;
+  top: auto;
+  bottom: -25px;
 }
 
 .OT_publisher .OT_edge-bar-item.OT_mode-on,
 .OT_subscriber .OT_edge-bar-item.OT_mode-on,
 .OT_publisher .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
 .OT_subscriber .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
 .OT_publisher:hover .OT_edge-bar-item.OT_mode-auto,
 .OT_subscriber:hover .OT_edge-bar-item.OT_mode-auto,
 .OT_publisher:hover .OT_edge-bar-item.OT_mode-mini-auto,
 .OT_subscriber:hover .OT_edge-bar-item.OT_mode-mini-auto {
-    top: 0;
-    opacity: 1;
+  top: 0;
+  opacity: 1;
 }
 
 .OT_mini .OT_mute.OT_mode-on,
 .OT_mini:hover .OT_mute.OT_mode-auto,
 .OT_mute.OT_mode-mini,
-.OT_root:hover .OT_mute.OT_mode-mini-auto  {
-    top: 50%;
+.OT_root:hover .OT_mute.OT_mode-mini-auto {
+  top: 50%;
 }
 
 .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
 .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-on,
 .OT_publisher:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto,
 .OT_subscriber:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto {
-    top: auto;
-    bottom: 0;
-    opacity: 1;
+  top: auto;
+  bottom: 0;
+  opacity: 1;
 }
 
+
 /* Contains the video element, used to fix video letter-boxing */
-.OT_video-container {
-    position: absolute;
-    background-color: #000000;
-    overflow: hidden;
+.OT_widget-container {
+  position: absolute;
+  background-color: #000000;
+  overflow: hidden;
 }
 
 .OT_hidden-audio {
-    position: absolute !important;
-    height: 1px !important;
-    width: 1px !important;
+  position: absolute !important;
+  height: 1px !important;
+  width: 1px !important;
 }
 
 /* Load animation */
 .OT_root .OT_video-loading {
-    background: url('../images/rtc/loader.gif') no-repeat;
-    display:none;
-    position: absolute;
-    width: 32px;
-    height: 32px;
-    left: 50%;
-    top: 50%;
-    margin-left: -16px;
-    margin-top: -16px;
+  background: url('../images/rtc/loader.gif') no-repeat;
+  display: none;
+  position: absolute;
+  width: 32px;
+  height: 32px;
+  left: 50%;
+  top: 50%;
+  margin-left: -16px;
+  margin-top: -16px;
 }
 
 .OT_publisher.OT_loading .OT_video-loading,
 .OT_subscriber.OT_loading .OT_video-loading {
-    display: block;
+  display: block;
+}
+
+.OT_publisher.OT_loading .OT_video-element,
+.OT_subscriber.OT_loading .OT_video-element {
+  /*display: none;*/
+}
+
+.OT_video-centering {
+  display: table;
+  width: 100%;
+  height: 100%;
 }
 
-.OT_publisher.OT_loading video,
-.OT_subscriber.OT_loading video,
-.OT_publisher.OT_loading object,
-.OT_subscriber.OT_loading object {
-    display: none;
+.OT_video-container {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.OT_video-poster {
+  position: absolute;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+  display: none;
+
+  opacity: .25;
+
+  background-repeat: no-repeat;
+  background-image: url(../images/rtc/audioonly-silhouette.svg);
 }
 
 
-.OT_video-poster {
-    width: 100%;
-    height: 100%;
-    display: none;
+.OT_fit-mode-cover .OT_video-poster {
+  background-size: auto 76%;
+  background-position: center bottom;
+}
 
-    opacity: .25;
-    background-size: auto 76%;
-    background-repeat: no-repeat;
-    background-position: center bottom;
-    background-image: url(../images/rtc/audioonly-silhouette.svg);
+.OT_fit-mode-contain .OT_video-poster {
+  background-size: contain;
+  background-position: center;
 }
 
 .OT_audio-level-meter {
-    position: absolute;
-    width:  25%;
-    max-width: 224px;
-    min-width: 21px;
-    top: 0;
-    right: 0;
-    overflow: hidden;
+  position: absolute;
+  width: 25%;
+  max-width: 224px;
+  min-width: 21px;
+  top: 0;
+  right: 0;
+  overflow: hidden;
 }
 
 .OT_audio-level-meter:before {
-    /* makes the height of the container equals its width */
-    content: '';
-    display: block;
-    padding-top: 100%;
+  /* makes the height of the container equals its width */
+  content: '';
+  display: block;
+  padding-top: 100%;
 }
 
 .OT_audio-level-meter__bar {
-    position: absolute;
-    width: 192%; /* meter value can overflow of 8% */
-    height: 192%;
-    top: -96% /* half of the size */;
-    right: -96%;
-    border-radius: 50%;
+  position: absolute;
+  width: 192%; /* meter value can overflow of 8% */
+  height: 192%;
+  top: -96% /* half of the size */;
+  right: -96%;
+  border-radius: 50%;
 
-    background-color: rgba(0, 0, 0, .8);
+  background-color: rgba(0, 0, 0, .8);
 }
 
 .OT_audio-level-meter__audio-only-img {
-    position: absolute;
-    top: 22%;
-    right: 15%;
-    width: 40%;
+  position: absolute;
+  top: 22%;
+  right: 15%;
+  width: 40%;
 
-    opacity: .7;
+  opacity: .7;
 
-    background: url(../images/rtc/audioonly-headset.svg) no-repeat center;
+  background: url(../images/rtc/audioonly-headset.svg) no-repeat center;
 }
 
 .OT_audio-level-meter__audio-only-img:before {
-    /* makes the height of the container equals its width */
-    content: '';
-    display: block;
-    padding-top: 100%;
+  /* makes the height of the container equals its width */
+  content: '';
+  display: block;
+  padding-top: 100%;
 }
 
 .OT_audio-level-meter__value {
-    position: absolute;
-    border-radius: 50%;
-    background-image: radial-gradient(circle, rgba(151,206,0,1) 0%, rgba(151,206,0,0) 100%);
+  position: absolute;
+  border-radius: 50%;
+  background-image: radial-gradient(circle, rgba(151, 206, 0, 1) 0%, rgba(151, 206, 0, 0) 100%);
 }
 
 .OT_audio-level-meter.OT_mode-off {
     display: none;
 }
 
 .OT_audio-level-meter.OT_mode-on,
 .OT_audio-only .OT_audio-level-meter.OT_mode-auto {
-    display: block;
+  display: block;
 }
 
 .OT_video-disabled-indicator {
-    opacity: 1;
-    border: none;
-    display: none;
-    position: absolute;
-    background-color: transparent;
-    background-repeat: no-repeat;
-    background-position:bottom right;
-    top: 0;
-    left: 0;
-    bottom: 3px;
-    right: 3px;
+  opacity: 1;
+  border: none;
+  display: none;
+  position: absolute;
+  background-color: transparent;
+  background-repeat: no-repeat;
+  background-position: bottom right;
+  top: 0;
+  left: 0;
+  bottom: 3px;
+  right: 3px;
 }
 
 .OT_video-disabled {
-    background-image: url(../images/rtc/video-disabled.png);
+  background-image: url(../images/rtc/video-disabled.png);
 }
 
 .OT_video-disabled-warning {
-    background-image: url(../images/rtc/video-disabled-warning.png);
+  background-image: url(../images/rtc/video-disabled-warning.png);
 }
 
 .OT_video-disabled-indicator.OT_active {
-    display: block;
+  display: block;
 }
--- a/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js
+++ b/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js
@@ -1,7 +1,7 @@
-/*
-
- Copyright (c) 2014 TokBox, Inc.
- Released under the MIT license
- http://opensource.org/licenses/MIT
-*/
+/**
+ * @license
+ * Copyright (c) 2014 TokBox, Inc.
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ */
 !function(){TB.Config.replaceWith({global:{exceptionLogging:{enabled:!0,messageLimitPerPartner:100},iceServers:{enabled:!1},instrumentation:{enabled:!1,debugging:!1},tokshow:{textchat:!0}},partners:{change878:{instrumentation:{enabled:!0,debugging:!0}}}})}(TB);
\ No newline at end of file
--- a/browser/components/loop/content/shared/libs/sdk.js
+++ b/browser/components/loop/content/shared/libs/sdk.js
@@ -1,712 +1,616 @@
 /**
- * @license  OpenTok JavaScript Library v2.2.9.7
+ * @license  OpenTok JavaScript Library v2.4.0 54ae164 HEAD
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: January 26 03:18:02 2015
- */
-
-(function(window) {
-  if (!window.OT) window.OT = {};
-
-  OT.properties = {
-    version: 'v2.2.9.7',         // The current version (eg. v2.0.4) (This is replaced by gradle)
-    build: '59e99bc',    // The current build hash (This is replaced by gradle)
-
-    // Whether or not to turn on debug logging by default
-    debug: 'false',
-    // The URL of the tokbox website
-    websiteURL: 'http://www.tokbox.com',
-
-    // The URL of the CDN
-    cdnURL: 'http://static.opentok.com',
-    // The URL to use for logging
-    loggingURL: 'http://hlg.tokbox.com/prod',
-    // The anvil API URL
-    apiURL: 'http://anvil.opentok.com',
-
-    // What protocol to use when connecting to the rumor web socket
-    messagingProtocol: 'wss',
-    // What port to use when connection to the rumor web socket
-    messagingPort: 443,
-
-    // If this environment supports SSL
-    supportSSL: 'true',
-    // The CDN to use if we're using SSL
-    cdnURLSSL: 'https://static.opentok.com',
-    // The URL to use for logging
-    loggingURLSSL: 'https://hlg.tokbox.com/prod',
-    // The anvil API URL to use if we're using SSL
-    apiURLSSL: 'https://anvil.opentok.com',
-
-    minimumVersion: {
-      firefox: parseFloat('29'),
-      chrome: parseFloat('34')
-    }
-  };
-
-})(window);
-/**
- * @license  Common JS Helpers on OpenTok 0.2.0 3fa583f master
+ * Date: January 08 08:54:40 2015
+ */
+
+
+!(function(window) {
+
+!(function(window, OTHelpers, undefined) {
+
+/**
+ * @license  Common JS Helpers on OpenTok 0.2.0 ef06638 2014Q4-2.2
  * http://www.tokbox.com/
  *
- * Copyright (c) 2014 TokBox, Inc.
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- * Date: August 08 12:31:42 2014
- *
- */
+ * Copyright (c) 2015 TokBox, Inc.
+ *
+ * Date: January 08 08:54:29 2015
+ *
+ */
+
 
 // OT Helper Methods
 //
 // helpers.js                           <- the root file
 // helpers/lib/{helper topic}.js        <- specialised helpers for specific tasks/topics
 //                                          (i.e. video, dom, etc)
 //
 // @example Getting a DOM element by it's id
-//  var element = OTHelpers('domId');
+//  var element = OTHelpers('#domId');
 //
 //
 
 /*jshint browser:true, smarttabs:true*/
 
-!(function(window, undefined) {
-
-
-  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;
-  };
-
-  var _each = Array.prototype.forEach || function(iter, ctx) {
-    for(var idx = 0, count = this.length || 0; idx < count; ++idx) {
-      if(idx in this) {
-        iter.call(ctx, this[idx], idx);
-      }
-    }
-  };
-
-  OTHelpers.forEach = function(array, iter, ctx) {
-    return _each.call(array, iter, ctx);
-  };
-
-  var _map = Array.prototype.map || function(iter, ctx) {
-    var collect = [];
-    _each.call(this, function(item, idx) {
-      collect.push(iter.call(ctx, item, idx));
-    });
-    return collect;
-  };
-
-  OTHelpers.map = function(array, iter) {
-    return _map.call(array, iter);
-  };
-
-  var _filter = Array.prototype.filter || function(iter, ctx) {
-    var collect = [];
-    _each.call(this, function(item, idx) {
-      if(iter.call(ctx, item, idx)) {
-        collect.push(item);
-      }
-    });
-    return collect;
-  };
-
-  OTHelpers.filter = function(array, iter, ctx) {
-    return _filter.call(array, iter, ctx);
-  };
-
-  var _some = Array.prototype.some || function(iter, ctx) {
-    var any = false;
-    for(var idx = 0, count = this.length || 0; idx < count; ++idx) {
-      if(idx in this) {
-        if(iter.call(ctx, this[idx], idx)) {
-          any = true;
-          break;
-        }
-      }
-    }
-    return any;
-  };
-
-  OTHelpers.some = function(array, iter, ctx) {
-    return _some.call(array, iter, ctx);
-  };
-
-  var _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;
-      }
-    }
+// Short cuts to commonly accessed prototypes
+var prototypeSlice = Array.prototype.slice;
+
+
+// RegEx to detect CSS Id selectors
+var detectIdSelectors = /^#([\w-]*)$/;
+
+// The top-level namespace, also performs basic DOMElement selecting.
+//
+// @example Get the DOM element with the id of 'domId'
+//   OTHelpers('#domId')
+//
+// @example Get all video elements
+//   OTHelpers('video')
+//
+// @example Get all elements with the class name of 'foo'
+//   OTHelpers('.foo')
+//
+// @example Get all elements with the class name of 'foo',
+// and do something with the first.
+//   var collection = OTHelpers('.foo');
+//   console.log(collection.first);
+//
+//
+// The second argument is the context, that is document or parent Element, to
+// select from.
+//
+// @example Get a video element within the element with the id of 'domId'
+//   OTHelpers('video', OTHelpers('#domId'))
+//
+//
+//
+// OTHelpers will accept any of the following and return a collection:
+//   OTHelpers()
+//   OTHelpers('css selector', optionalParentNode)
+//   OTHelpers(DomNode)
+//   OTHelpers([array of DomNode])
+//
+// The collection is a ElementCollection object, see the ElementCollection docs for usage info.
+//
+var OTHelpers = function(selector, context) {
+  var results = [];
+
+  if (typeof(selector) === 'string') {
+    var idSelector = detectIdSelectors.exec(selector);
+    context = context || document;
+
+    if (idSelector && idSelector[1]) {
+      var element = context.getElementById(idSelector[1]);
+      if (element) results.push(element);
+    }
+    else {
+      results = context.querySelectorAll(selector);
+    }
+  }
+  else if (selector.nodeType || window.XMLHttpRequest && selector instanceof XMLHttpRequest) {
+    // allow OTHelpers(DOMNode) and OTHelpers(xmlHttpRequest)
+    results = [selector];
+    context = selector;
+  }
+  else if (OTHelpers.isArray(selector)) {
+    results = selector.slice();
+    context  = null;
+  }
+
+  return new ElementCollection(results, context);
+};
+
+// alias $ to OTHelpers for internal use. This is not exposed outside of OTHelpers.
+var $ = OTHelpers;
+
+// A helper for converting a NodeList to a JS Array
+var nodeListToArray = function nodeListToArray (nodes) {
+  if ($.env.name !== 'IE' || $.env.version > 9) {
+    return prototypeSlice.call(nodes);
+  }
+
+  // IE 9 and below call use Array.prototype.slice.call against
+  // a NodeList, hence the following
+  var array = [];
+
+  for (var i=0, num=nodes.length; i<num; ++i) {
+    array.push(nodes[i]);
+  }
+
+  return array;
+};
+
+// ElementCollection contains the result of calling OTHelpers.
+//
+// It has the following properties:
+//   length
+//   first
+//   last
+//
+// It also has a get method that can be used to access elements in the collection
+//
+//   var videos = OTHelpers('video');
+//   var firstElement = videos.get(0);               // identical to videos.first
+//   var lastElement = videos.get(videos.length-1);  // identical to videos.last
+//   var allVideos = videos.get();
+//
+//
+// The collection also implements the following helper methods:
+//   some, forEach, map, filter, find,
+//   appendTo, after, before, remove, empty,
+//   attr, center, width, height,
+//   addClass, removeClass, hasClass, toggleClass,
+//   on, off, once,
+//   observeStyleChanges, observeNodeOrChildNodeRemoval
+//
+// Mostly the usage should be obvious. When in doubt, assume it functions like
+// the jQuery equivalent.
+//
+var ElementCollection = function ElementCollection (elems, context) {
+  var elements = nodeListToArray(elems);
+  this.context = context;
+  this.toArray = function() { return elements; };
+
+  this.length = elements.length;
+  this.first = elements[0];
+  this.last = elements[elements.length-1];
+
+  this.get = function(index) {
+    if (index === void 0) return elements;
+    return elements[index];
+  };
+};
+
+
+ElementCollection.prototype.some = function (iter, context) {
+  return $.some(this.get(), iter, context);
+};
+
+ElementCollection.prototype.forEach = function(fn, context) {
+  $.forEach(this.get(), fn, context);
+  return this;
+};
+
+ElementCollection.prototype.map = function(fn, context) {
+  return new ElementCollection($.map(this.get(), fn, context), this.context);
+};
+
+ElementCollection.prototype.filter = function(fn, context) {
+  return new ElementCollection($.filter(this.get(), fn, context), this.context);
+};
+
+ElementCollection.prototype.find = function(selector) {
+  return $(selector, this.first);
+};
+
+
+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;
+};
+
+var _each = Array.prototype.forEach || function(iter, ctx) {
+  for(var idx = 0, count = this.length || 0; idx < count; ++idx) {
+    if(idx in this) {
+      iter.call(ctx, this[idx], idx);
+    }
+  }
+};
+
+OTHelpers.forEach = function(array, iter, ctx) {
+  return _each.call(array, iter, ctx);
+};
+
+var _map = Array.prototype.map || function(iter, ctx) {
+  var collect = [];
+  _each.call(this, function(item, idx) {
+    collect.push(iter.call(ctx, item, idx));
+  });
+  return collect;
+};
+
+OTHelpers.map = function(array, iter) {
+  return _map.call(array, iter);
+};
+
+var _filter = Array.prototype.filter || function(iter, ctx) {
+  var collect = [];
+  _each.call(this, function(item, idx) {
+    if(iter.call(ctx, item, idx)) {
+      collect.push(item);
+    }
+  });
+  return collect;
+};
+
+OTHelpers.filter = function(array, iter, ctx) {
+  return _filter.call(array, iter, ctx);
+};
+
+var _some = Array.prototype.some || function(iter, ctx) {
+  var any = false;
+  for(var idx = 0, count = this.length || 0; idx < count; ++idx) {
+    if(idx in this) {
+      if(iter.call(ctx, this[idx], idx)) {
+        any = true;
+        break;
+      }
+    }
+  }
+  return any;
+};
+
+OTHelpers.find = function(array, iter, ctx) {
+  if (!$.isFunction(iter)) {
+    throw new TypeError('iter must be a function');
+  }
+
+  var any = void 0;
+  for(var idx = 0, count = array.length || 0; idx < count; ++idx) {
+    if(idx in array) {
+      if(iter.call(ctx, array[idx], idx)) {
+        any = array[idx];
+        break;
+      }
+    }
+  }
+  return any;
+};
+
+OTHelpers.findIndex = function(array, iter, ctx) {
+  if (!$.isFunction(iter)) {
+    throw new TypeError('iter must be a function');
+  }
+
+  for (var i = 0, count = array.length || 0; i < count; ++i) {
+    if (i in array && iter.call(ctx, array[i], i, array)) {
+      return i;
+    }
+  }
+
+  return -1;
+};
+
+
+OTHelpers.some = function(array, iter, ctx) {
+  return _some.call(array, iter, ctx);
+};
+
+var _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;
-  };
-
-  OTHelpers.arrayIndexOf = function(array, searchElement, fromIndex) {
-    return _indexOf.call(array, searchElement, fromIndex);
-  };
-
-  var _bind = Function.prototype.bind || function() {
-    var args = Array.prototype.slice.call(arguments),
-        ctx = args.shift(),
-        fn = this;
-    return function() {
-      return fn.apply(ctx, args.concat(Array.prototype.slice.call(arguments)));
-    };
-  };
-
-  OTHelpers.bind = function() {
-    var args = Array.prototype.slice.call(arguments),
-        fn = args.shift();
-    return _bind.apply(fn, args);
-  };
-
-  var _trim = String.prototype.trim || function() {
-    return this.replace(/^\s+|\s+$/g, '');
-  };
-
-  OTHelpers.trim = function(str) {
-    return _trim.call(str);
-  };
-
+  }
+
+  if (pivot < 0) {
+    pivot = length - Math.abs(pivot);
+  }
+
+  for (i = pivot; i < length; i++) {
+    if (this[i] === searchElement) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+OTHelpers.arrayIndexOf = function(array, searchElement, fromIndex) {
+  return _indexOf.call(array, searchElement, fromIndex);
+};
+
+var _bind = Function.prototype.bind || function() {
+  var args = prototypeSlice.call(arguments),
+      ctx = args.shift(),
+      fn = this;
+  return function() {
+    return fn.apply(ctx, args.concat(prototypeSlice.call(arguments)));
+  };
+};
+
+OTHelpers.bind = function() {
+  var args = prototypeSlice.call(arguments),
+      fn = args.shift();
+  return _bind.apply(fn, args);
+};
+
+var _trim = String.prototype.trim || function() {
+  return this.replace(/^\s+|\s+$/g, '');
+};
+
+OTHelpers.trim = function(str) {
+  return _trim.call(str);
+};
+
+OTHelpers.noConflict = function() {
   OTHelpers.noConflict = function() {
-    OTHelpers.noConflict = function() {
-      return OTHelpers;
-    };
-    window.OTHelpers = previousOTHelpers;
     return OTHelpers;
   };
-
-  OTHelpers.isNone = function(obj) {
-    return obj === undefined || obj === null;
-  };
-
-  OTHelpers.isObject = function(obj) {
-    return obj === Object(obj);
-  };
-
-  OTHelpers.isFunction = function(obj) {
-    return !!obj && (obj.toString().indexOf('()') !== -1 ||
-      Object.prototype.toString.call(obj) === '[object Function]');
-  };
-
-  OTHelpers.isArray = OTHelpers.isFunction(Array.isArray) && Array.isArray ||
-    function (vArg) {
-      return Object.prototype.toString.call(vArg) === '[object Array]';
-    };
-
-  OTHelpers.isEmpty = function(obj) {
-    if (obj === null || obj === undefined) return true;
-    if (OTHelpers.isArray(obj) || typeof(obj) === 'string') return obj.length === 0;
-
-    // Objects without enumerable owned properties are empty.
-    for (var key in obj) {
-      if (obj.hasOwnProperty(key)) return false;
-    }
-
-    return true;
-  };
+  window.OTHelpers = previousOTHelpers;
+  return OTHelpers;
+};
+
+OTHelpers.isNone = function(obj) {
+  return obj === void 0 || obj === null;
+};
+
+OTHelpers.isObject = function(obj) {
+  return obj === Object(obj);
+};
+
+OTHelpers.isFunction = function(obj) {
+  return !!obj && (obj.toString().indexOf('()') !== -1 ||
+    Object.prototype.toString.call(obj) === '[object Function]');
+};
+
+OTHelpers.isArray = OTHelpers.isFunction(Array.isArray) && Array.isArray ||
+  function (vArg) {
+    return Object.prototype.toString.call(vArg) === '[object Array]';
+  };
+
+OTHelpers.isEmpty = function(obj) {
+  if (obj === null || obj === void 0) return true;
+  if (OTHelpers.isArray(obj) || typeof(obj) === 'string') return obj.length === 0;
+
+  // Objects without enumerable owned properties are empty.
+  for (var key in obj) {
+    if (obj.hasOwnProperty(key)) return false;
+  }
+
+  return true;
+};
 
 // Extend a target object with the properties from one or
 // more source objects
 //
 // @example:
 //    dest = OTHelpers.extend(dest, source1, source2, source3);
 //
-  OTHelpers.extend = function(/* dest, source1[, source2, ..., , sourceN]*/) {
-    var sources = Array.prototype.slice.call(arguments),
-        dest = sources.shift();
-
-    OTHelpers.forEach(sources, function(source) {
-      for (var key in source) {
-        dest[key] = source[key];
-      }
-    });
-
-    return dest;
-  };
+OTHelpers.extend = function(/* dest, source1[, source2, ..., , sourceN]*/) {
+  var sources = prototypeSlice.call(arguments),
+      dest = sources.shift();
+
+  OTHelpers.forEach(sources, function(source) {
+    for (var key in source) {
+      dest[key] = source[key];
+    }
+  });
+
+  return dest;
+};
 
 // Ensures that the target object contains certain defaults.
 //
 // @example
 //   var options = OTHelpers.defaults(options, {
 //     loading: true     // loading by default
 //   });
 //
-  OTHelpers.defaults = function(/* dest, defaults1[, defaults2, ..., , defaultsN]*/) {
-    var sources = Array.prototype.slice.call(arguments),
-        dest = sources.shift();
-
-    OTHelpers.forEach(sources, function(source) {
-      for (var key in source) {
-        if (dest[key] === void 0) dest[key] = source[key];
-      }
-    });
-
-    return dest;
-  };
-
-  OTHelpers.clone = function(obj) {
-    if (!OTHelpers.isObject(obj)) return obj;
-    return OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj);
-  };
+OTHelpers.defaults = function(/* dest, defaults1[, defaults2, ..., , defaultsN]*/) {
+  var sources = prototypeSlice.call(arguments),
+      dest = sources.shift();
+
+  OTHelpers.forEach(sources, function(source) {
+    for (var key in source) {
+      if (dest[key] === void 0) dest[key] = source[key];
+    }
+  });
+
+  return dest;
+};
+
+OTHelpers.clone = function(obj) {
+  if (!OTHelpers.isObject(obj)) return obj;
+  return OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj);
+};
 
 
 
 // Handy do nothing function
-  OTHelpers.noop = function() {};
+OTHelpers.noop = function() {};
 
 
 // Returns the number of millisceonds since the the UNIX epoch, this is functionally
 // equivalent to executing new Date().getTime().
 //
 // Where available, we use 'performance.now' which is more accurate and reliable,
 // otherwise we default to new Date().getTime().
-  OTHelpers.now = (function() {
-    var performance = window.performance || {},
-        navigationStart,
-        now =  performance.now       ||
-               performance.mozNow    ||
-               performance.msNow     ||
-               performance.oNow      ||
-               performance.webkitNow;
-
-    if (now) {
-      now = OTHelpers.bind(now, performance);
-      navigationStart = performance.timing.navigationStart;
-
-      return  function() { return navigationStart + now(); };
-    } else {
-      return function() { return new Date().getTime(); };
-    }
-  })();
-
-  var _browser = function() {
-    var userAgent = window.navigator.userAgent.toLowerCase(),
-        appName = window.navigator.appName,
-        navigatorVendor,
-        browser = 'unknown',
-        version = -1;
-
-    if (userAgent.indexOf('opera') > -1 || userAgent.indexOf('opr') > -1) {
-      browser = 'Opera';
-
-      if (/opr\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-
-    } else if (userAgent.indexOf('firefox') > -1)   {
-      browser = 'Firefox';
-
-      if (/firefox\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-
-    } else if (appName === 'Microsoft Internet Explorer') {
-      // IE 10 and below
-      browser = 'IE';
-
-      if (/msie ([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-
-    } else if (appName === 'Netscape' && userAgent.indexOf('trident') > -1) {
-      // IE 11+
-
-      browser = 'IE';
-
-      if (/trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-
-    } else if (userAgent.indexOf('chrome') > -1) {
-      browser = 'Chrome';
-
-      if (/chrome\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-
-    } else if ((navigatorVendor = window.navigator.vendor) &&
-      navigatorVendor.toLowerCase().indexOf('apple') > -1) {
-      browser = 'Safari';
-
-      if (/version\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
-        version = parseFloat( RegExp.$1 );
-      }
-    }
-
-    return {
-      browser: browser,
-      version: version,
-      iframeNeedsLoad: userAgent.indexOf('webkit') < 0
-    };
-  }();
-
-  OTHelpers.browser = function() {
-    return _browser.browser;
-  };
-
-  OTHelpers.browserVersion = function() {
-    return _browser;
-  };
-
-
-  OTHelpers.canDefineProperty = true;
-
-  try {
-    Object.defineProperty({}, 'x', {});
-  } catch (err) {
-    OTHelpers.canDefineProperty = false;
-  }
+OTHelpers.now = (function() {
+  var performance = window.performance || {},
+      navigationStart,
+      now =  performance.now       ||
+             performance.mozNow    ||
+             performance.msNow     ||
+             performance.oNow      ||
+             performance.webkitNow;
+
+  if (now) {
+    now = OTHelpers.bind(now, performance);
+    navigationStart = performance.timing.navigationStart;
+
+    return  function() { return navigationStart + now(); };
+  } else {
+    return function() { return new Date().getTime(); };
+  }
+})();
+
+OTHelpers.canDefineProperty = true;
+
+try {
+  Object.defineProperty({}, 'x', {});
+} catch (err) {
+  OTHelpers.canDefineProperty = false;
+}
 
 // A helper for defining a number of getters at once.
 //
 // @example: from inside an object
 //   OTHelpers.defineGetters(this, {
 //     apiKey: function() { return _apiKey; },
 //     token: function() { return _token; },
 //     connected: function() { return this.is('connected'); },
 //     capabilities: function() { return _socket.capabilities; },
 //     sessionId: function() { return _sessionId; },
 //     id: function() { return _sessionId; }
 //   });
 //
-  OTHelpers.defineGetters = function(self, getters, enumerable) {
-    var propsDefinition = {};
-
-    if (enumerable === void 0) enumerable = false;
-
-    for (var key in getters) {
-      propsDefinition[key] = {
-        get: getters[key],
-        enumerable: enumerable
-      };
-    }
-
-    OTHelpers.defineProperties(self, propsDefinition);
-  };
-
-  var generatePropertyFunction = function(object, getter, setter) {
-    if(getter && !setter) {
-      return function() {
-        return getter.call(object);
-      };
-    } else if(getter && setter) {
-      return function(value) {
-        if(value !== void 0) {
-          setter.call(object, value);
-        }
-        return getter.call(object);
-      };
-    } else {
-      return function(value) {
-        if(value !== void 0) {
-          setter.call(object, value);
-        }
-      };
-    }
-  };
-
-  OTHelpers.defineProperties = function(object, getterSetters) {
-    for (var key in getterSetters) {
-      object[key] = generatePropertyFunction(object, getterSetters[key].get,
-        getterSetters[key].set);
-    }
-  };
+OTHelpers.defineGetters = function(self, getters, enumerable) {
+  var propsDefinition = {};
+
+  if (enumerable === void 0) enumerable = false;
+
+  for (var key in getters) {
+    propsDefinition[key] = {
+      get: getters[key],
+      enumerable: enumerable
+    };
+  }
+
+  OTHelpers.defineProperties(self, propsDefinition);
+};
+
+var generatePropertyFunction = function(object, getter, setter) {
+  if(getter && !setter) {
+    return function() {
+      return getter.call(object);
+    };
+  } else if(getter && setter) {
+    return function(value) {
+      if(value !== void 0) {
+        setter.call(object, value);
+      }
+      return getter.call(object);
+    };
+  } else {
+    return function(value) {
+      if(value !== void 0) {
+        setter.call(object, value);
+      }
+    };
+  }
+};
+
+OTHelpers.defineProperties = function(object, getterSetters) {
+  for (var key in getterSetters) {
+    object[key] = generatePropertyFunction(object, getterSetters[key].get,
+      getterSetters[key].set);
+  }
+};
 
 
 // Polyfill Object.create for IE8
 //
 // See https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
 //
-  if (!Object.create) {
-    Object.create = function (o) {
-      if (arguments.length > 1) {
-        throw new Error('Object.create implementation only accepts the first parameter.');
-      }
-      function F() {}
-      F.prototype = o;
-      return new F();
-    };
-  }
-
-  OTHelpers.setCookie = function(key, value) {
-    try {
-      localStorage.setItem(key, value);
-    } catch (err) {
-      // Store in browser cookie
-      var date = new Date();
-      date.setTime(date.getTime()+(365*24*60*60*1000));
-      var expires = '; expires=' + date.toGMTString();
-      document.cookie = key + '=' + value + expires + '; path=/';
-    }
-  };
-
-  OTHelpers.getCookie = function(key) {
-    var value;
-
-    try {
-      value = localStorage.getItem('opentok_client_id');
-      return value;
-    } catch (err) {
-      // Check browser cookies
-      var nameEQ = key + '=';
-      var ca = document.cookie.split(';');
-      for(var i=0;i < ca.length;i++) {
-        var c = ca[i];
-        while (c.charAt(0) === ' ') {
-          c = c.substring(1,c.length);
-        }
-        if (c.indexOf(nameEQ) === 0) {
-          value = c.substring(nameEQ.length,c.length);
-        }
-      }
-
-      if (value) {
-        return value;
-      }
-    }
-
-    return null;
-  };
-
+if (!Object.create) {
+  Object.create = function (o) {
+    if (arguments.length > 1) {
+      throw new Error('Object.create implementation only accepts the first parameter.');
+    }
+    function F() {}
+    F.prototype = o;
+    return new F();
+  };
+}
 
 // These next bits are included from Underscore.js. The original copyright
 // notice is below.
 //
 // http://underscorejs.org
 // (c) 2009-2011 Jeremy Ashkenas, DocumentCloud Inc.
 // (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
 // Underscore may be freely distributed under the MIT license.
 
-  // Invert the keys and values of an object. The values must be serializable.
-  OTHelpers.invert = function(obj) {
-    var result = {};
-    for (var key in obj) if (obj.hasOwnProperty(key)) result[obj[key]] = key;
-    return result;
-  };
-
-
-  // List of HTML entities for escaping.
-  var entityMap = {
-    escape: {
-      '&':  '&amp;',
-      '<':  '&lt;',
-      '>':  '&gt;',
-      '"':  '&quot;',
-      '\'': '&#x27;',
-      '/':  '&#x2F;'
-    }
-  };
-
-  entityMap.unescape = OTHelpers.invert(entityMap.escape);
-
-  // Regexes containing the keys and values listed immediately above.
-  var entityRegexes = {
-    escape:   new RegExp('[' + OTHelpers.keys(entityMap.escape).join('') + ']', 'g'),
-    unescape: new RegExp('(' + OTHelpers.keys(entityMap.unescape).join('|') + ')', 'g')
-  };
-
-  // Functions for escaping and unescaping strings to/from HTML interpolation.
-  OTHelpers.forEach(['escape', 'unescape'], function(method) {
-    OTHelpers[method] = function(string) {
-      if (string === null || string === undefined) return '';
-      return ('' + string).replace(entityRegexes[method], function(match) {
-        return entityMap[method][match];
-      });
-    };
-  });
-
-// By default, Underscore uses ERB-style template delimiters, change the
-// following template settings to use alternative delimiters.
-  OTHelpers.templateSettings = {
-    evaluate    : /<%([\s\S]+?)%>/g,
-    interpolate : /<%=([\s\S]+?)%>/g,
-    escape      : /<%-([\s\S]+?)%>/g
-  };
-
-// When customizing `templateSettings`, if you don't want to define an
-// interpolation, evaluation or escaping regex, we need one that is
-// guaranteed not to match.
-  var noMatch = /(.)^/;
-
-// Certain characters need to be escaped so that they can be put into a
-// string literal.
-  var escapes = {
-    '\'':     '\'',
-    '\\':     '\\',
-    '\r':     'r',
-    '\n':     'n',
-    '\t':     't',
-    '\u2028': 'u2028',
-    '\u2029': 'u2029'
-  };
-
-  var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
-
-// JavaScript micro-templating, similar to John Resig's implementation.
-// Underscore templating handles arbitrary delimiters, preserves whitespace,
-// and correctly escapes quotes within interpolated code.
-  OTHelpers.template = function(text, data, settings) {
-    var render;
-    settings = OTHelpers.defaults({}, settings, OTHelpers.templateSettings);
-
-    // Combine delimiters into one regular expression via alternation.
-    var matcher = new RegExp([
-      (settings.escape || noMatch).source,
-      (settings.interpolate || noMatch).source,
-      (settings.evaluate || noMatch).source
-    ].join('|') + '|$', 'g');
-
-    // Compile the template source, escaping string literals appropriately.
-    var index = 0;
-    var source = '__p+=\'';
-    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
-      source += text.slice(index, offset)
-        .replace(escaper, function(match) { return '\\' + escapes[match]; });
-
-      if (escape) {
-        source += '\'+\n((__t=(' + escape + '))==null?\'\':OTHelpers.escape(__t))+\n\'';
-      }
-      if (interpolate) {
-        source += '\'+\n((__t=(' + interpolate + '))==null?\'\':__t)+\n\'';
-      }
-      if (evaluate) {
-        source += '\';\n' + evaluate + '\n__p+=\'';
-      }
-      index = offset + match.length;
-      return match;
-    });
-    source += '\';\n';
-
-    // If a variable is not specified, place data values in local scope.
-    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
-
-    source = 'var __t,__p=\'\',__j=Array.prototype.join,' +
-      'print=function(){__p+=__j.call(arguments,\'\');};\n' +
-      source + 'return __p;\n';
-
-    try {
-      // evil is necessary for the new Function line
-      /*jshint evil:true */
-      render = new Function(settings.variable || 'obj', source);
-    } catch (e) {
-      e.source = source;
-      throw e;
-    }
-
-    if (data) return render(data);
-    var template = function(data) {
-      return render.call(this, data);
-    };
-
-    // Provide the compiled function source as a convenience for precompilation.
-    template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
-
-    return template;
-  };
-
-})(window);
+// Invert the keys and values of an object. The values must be serializable.
+OTHelpers.invert = function(obj) {
+  var result = {};
+  for (var key in obj) if (obj.hasOwnProperty(key)) result[obj[key]] = key;
+  return result;
+};
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../../helpers.js')
 
-(function(window, OTHelpers, undefined) {
-
-  OTHelpers.statable = function(self, possibleStates, initialState, stateChanged,
-    stateChangedFailed) {
-    var previousState,
-        currentState = self.currentState = initialState;
-
-    var setState = function(state) {
-      if (currentState !== state) {
-        if (OTHelpers.arrayIndexOf(possibleStates, state) === -1) {
-          if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) {
-            stateChangedFailed('invalidState', state);
-          }
-          return;
-        }
-
-        self.previousState = previousState = currentState;
-        self.currentState = currentState = state;
-        if (stateChanged && OTHelpers.isFunction(stateChanged)) stateChanged(state, previousState);
-      }
-    };
-
-
-    // Returns a number of states and returns true if the current state
-    // is any of them.
-    //
-    // @example
-    // if (this.is('connecting', 'connected')) {
-    //   // do some stuff
-    // }
-    //
-    self.is = function (/* state0:String, state1:String, ..., stateN:String */) {
-      return OTHelpers.arrayIndexOf(arguments, currentState) !== -1;
-    };
-
-
-    // Returns a number of states and returns true if the current state
-    // is none of them.
-    //
-    // @example
-    // if (this.isNot('connecting', 'connected')) {
-    //   // do some stuff
-    // }
-    //
-    self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) {
-      return OTHelpers.arrayIndexOf(arguments, currentState) === -1;
-    };
-
-    return setState;
-  };
-
-})(window, window.OTHelpers);
+OTHelpers.statable = function(self, possibleStates, initialState, stateChanged,
+  stateChangedFailed) {
+  var previousState,
+      currentState = self.currentState = initialState;
+
+  var setState = function(state) {
+    if (currentState !== state) {
+      if (OTHelpers.arrayIndexOf(possibleStates, state) === -1) {
+        if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) {
+          stateChangedFailed('invalidState', state);
+        }
+        return;
+      }
+
+      self.previousState = previousState = currentState;
+      self.currentState = currentState = state;
+      if (stateChanged && OTHelpers.isFunction(stateChanged)) stateChanged(state, previousState);
+    }
+  };
+
+
+  // Returns a number of states and returns true if the current state
+  // is any of them.
+  //
+  // @example
+  // if (this.is('connecting', 'connected')) {
+  //   // do some stuff
+  // }
+  //
+  self.is = function (/* state0:String, state1:String, ..., stateN:String */) {
+    return OTHelpers.arrayIndexOf(arguments, currentState) !== -1;
+  };
+
+
+  // Returns a number of states and returns true if the current state
+  // is none of them.
+  //
+  // @example
+  // if (this.isNot('connecting', 'connected')) {
+  //   // do some stuff
+  // }
+  //
+  self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) {
+    return OTHelpers.arrayIndexOf(arguments, currentState) === -1;
+  };
+
+  return setState;
+};
 
 /*!
  *  This is a modified version of Robert Kieffer awesome uuid.js library.
  *  The only modifications we've made are to remove the Node.js specific
  *  parts of the code and the UUID version 1 generator (which we don't
  *  use). The original copyright notice is below.
  *
  *     node-uuid/uuid.js
@@ -715,17 +619,17 @@
  *     Dual licensed under the MIT and GPL licenses.
  *     Documentation and details at https://github.com/broofa/node-uuid
  */
 // tb_require('../helpers.js')
 
 /*global crypto:true, Uint32Array:true, Buffer:true */
 /*jshint browser:true, smarttabs:true*/
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   // Unique ID creation requires a high quality random # generator, but
   // Math.random() does not guarantee "cryptographic quality".  So we feature
   // detect for more robust APIs, normalizing each method to return 128-bits
   // (16 bytes) of random data.
   var mathRNG, whatwgRNG;
 
   // Math.random()-based RNG.  All platforms, very fast, unknown quality
@@ -838,257 +742,884 @@
   uuid.BufferClass = BufferClass;
 
   // Export RNG options
   uuid.mathRNG = mathRNG;
   uuid.whatwgRNG = whatwgRNG;
 
   OTHelpers.uuid = uuid;
 
-}(window, window.OTHelpers));
+}());
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+
+
+var getErrorLocation;
+
+// Properties that we'll acknowledge from the JS Error object
+var safeErrorProps = [
+  'description',
+  'fileName',
+  'lineNumber',
+  'message',
+  'name',
+  'number',
+  'stack'
+];
+
+
+// OTHelpers.Error
+//
+// A construct to contain error information that also helps with extracting error
+// context, such as stack trace.
+//
+// @constructor
+// @memberof OTHelpers
+// @method Error
+//
+// @param {String} message
+//      Optional. The error message
+//
+// @param {Object} props
+//      Optional. A dictionary of properties containing extra Error info.
+//
+//
+// @example Create a simple error with juts a custom message
+//   var error = new OTHelpers.Error('Something Broke!');
+//   error.message === 'Something Broke!';
+//
+// @example Create an Error with a message and a name
+//   var error = new OTHelpers.Error('Something Broke!', 'FooError');
+//   error.message === 'Something Broke!';
+//   error.name === 'FooError';
+//
+// @example Create an Error with a message, name, and custom properties
+//   var error = new OTHelpers.Error('Something Broke!', 'FooError', {
+//     foo: 'bar',
+//     listOfImportantThings: [1,2,3,4]
+//   });
+//   error.message === 'Something Broke!';
+//   error.name === 'FooError';
+//   error.foo === 'bar';
+//   error.listOfImportantThings == [1,2,3,4];
+//
+// @example Create an Error from a Javascript Error
+//   var error = new OTHelpers.Error(domSyntaxError);
+//   error.message === domSyntaxError.message;
+//   error.name === domSyntaxError.name === 'SyntaxError';
+//   // ...continues for each properties of domSyntaxError
+//
+// @references
+// * https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
+// * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack
+// * http://www.w3.org/TR/dom/#interface-domerror
+//
+//
+// @todo
+// * update usage in OTMedia
+// * replace error handling in OT.js
+// * normalise stack behaviour under Chrome/Node/Safari with other browsers
+// * unit test for stack parsing
+//
+//
+OTHelpers.Error = function (message, name, props) {
+  switch (arguments.length) {
+  case 1:
+    if ($.isObject(message)) {
+      props = message;
+      name = void 0;
+      message = void 0;
+    }
+    // Otherwise it's the message
+    break;
+
+  case 2:
+    if ($.isObject(name)) {
+      props = name;
+      name = void 0;
+    }
+    // Otherwise name is actually the name
+
+    break;
+  }
+
+  if ( props instanceof Error) {
+    // Special handling of this due to Chrome weirdness. It seems that
+    // properties of the Error object, and it's children, are not
+    // enumerable in Chrome?
+    for (var i = 0, num = safeErrorProps.length; i < num; ++i) {
+      this[safeErrorProps[i]] = props[safeErrorProps[i]];
+    }
+  }
+  else if ( $.isObject(props)) {
+    // Use an custom properties that are provided
+    for (var key in props) {
+      if (props.hasOwnProperty(key)) {
+        this[key] = props[key];
+      }
+    }
+  }
+
+  // If any of the fundamental properties are missing then try and
+  // extract them.
+  if ( !(this.fileName && this.lineNumber && this.columnNumber && this.stack) ) {
+    var err = getErrorLocation();
+
+    if (!this.fileName && err.fileName) {
+      this.fileName = err.fileName;
+    }
+
+    if (!this.lineNumber && err.lineNumber) {
+      this.lineNumber = err.lineNumber;
+    }
+
+    if (!this.columnNumber && err.columnNumber) {
+      this.columnNumber = err.columnNumber;
+    }
+
+    if (!this.stack && err.stack) {
+      this.stack = err.stack;
+    }
+  }
+
+  if (!this.message && message) this.message = message;
+  if (!this.name && name) this.name = name;
+};
+
+OTHelpers.Error.prototype.toString =
+OTHelpers.Error.prototype.valueOf = function() {
+  var locationDetails = '';
+  if (this.fileName) locationDetails += ' ' + this.fileName;
+  if (this.lineNumber) {
+    locationDetails += ' ' + this.lineNumber;
+    if (this.columnNumber) locationDetails += ':' + this.columnNumber;
+  }
+
+  return '<' + (this.name ? this.name + ' ' : '') + this.message + locationDetails + '>';
+};
+
+
+// Normalise err.stack so that it is the same format as the other browsers
+// We skip the first two frames so that we don't capture getErrorLocation() and
+// the callee.
+//
+// Used by Environments that support the StackTrace API. (Chrome, Node, Opera)
+//
+var prepareStackTrace = function prepareStackTrace (_, stack){
+  return $.map(stack.slice(2), function(frame) {
+    var _f = {
+      fileName: frame.getFileName(),
+      linenumber: frame.getLineNumber(),
+      columnNumber: frame.getColumnNumber()
+    };
+
+    if (frame.getFunctionName()) _f.functionName = frame.getFunctionName();
+    if (frame.getMethodName()) _f.methodName = frame.getMethodName();
+    if (frame.getThis()) _f.self = frame.getThis();
+
+    return _f;
+  });
+};
+
+
+// Black magic to retrieve error location info for various environments
+getErrorLocation = function getErrorLocation () {
+  var info = {},
+      callstack,
+      errLocation,
+      err;
+
+  switch ($.env.name) {
+  case 'Firefox':
+  case 'Safari':
+  case 'IE':
+
+    if ($.env.name === 'IE') {
+      err = new Error();
+    }
+    else {
+      try {
+        window.call.js.is.explody;
+      }
+      catch(e) { err = e; }
+    }
+
+    callstack = err.stack.split('\n');
+
+    //Remove call to getErrorLocation() and the callee
+    callstack.shift();
+    callstack.shift();
+
+    info.stack = callstack;
+
+    if ($.env.name === 'IE') {
+      // IE also includes the error message in it's stack trace
+      info.stack.shift();
+
+      // each line begins with some amounts of spaces and 'at', we remove
+      // these to normalise with the other browsers.
+      info.stack = $.map(callstack, function(call) {
+        return call.replace(/^\s+at\s+/g, '');
+      });
+    }
+
+    errLocation = /@(.+?):([0-9]+)(:([0-9]+))?$/.exec(callstack[0]);
+    if (errLocation) {
+      info.fileName = errLocation[1];
+      info.lineNumber = parseInt(errLocation[2], 10);
+      if (errLocation.length > 3) info.columnNumber = parseInt(errLocation[4], 10);
+    }
+    break;
+
+  case 'Chrome':
+  case 'Node':
+  case 'Opera':
+    var currentPST = Error.prepareStackTrace;
+    Error.prepareStackTrace = prepareStackTrace;
+    err = new Error();
+    info.stack = err.stack;
+    Error.prepareStackTrace = currentPST;
+
+    var topFrame = info.stack[0];
+    info.lineNumber = topFrame.lineNumber;
+    info.columnNumber = topFrame.columnNumber;
+    info.fileName = topFrame.fileName;
+    if (topFrame.functionName) info.functionName = topFrame.functionName;
+    if (topFrame.methodName) info.methodName = topFrame.methodName;
+    if (topFrame.self) info.self = topFrame.self;
+    break;
+
+  default:
+    err = new Error();
+    if (err.stack) info.stack = err.stack.split('\n');
+    break;
+  }
+
+  if (err.message) info.message = err.message;
+  return info;
+};
+
+
+/*jshint browser:true, smarttabs:true*/
+/* global process */
+
+// tb_require('../helpers.js')
+
+
+// OTHelpers.env
+//
+// Contains information about the current environment.
+// * **OTHelpers.env.name** The name of the Environment (Chrome, FF, Node, etc)
+// * **OTHelpers.env.version** Usually a Float, except in Node which uses a String
+// * **OTHelpers.env.userAgent** The raw user agent
+// * **OTHelpers.env.versionGreaterThan** A helper method that returns true if the
+// current version is greater than the argument
+//
+// Example
+//     if (OTHelpers.env.versionGreaterThan('0.10.30')) {
+//       // do something
+//     }
+//
+(function() {
+  // @todo make exposing userAgent unnecessary
+  var version = -1;
+
+  // Returns true if otherVersion is greater than the current environment
+  // version.
+  var versionGEThan = function versionGEThan (otherVersion) {
+    if (otherVersion === version) return true;
+
+    if (typeof(otherVersion) === 'number' && typeof(version) === 'number') {
+      return otherVersion > version;
+    }
+
+    // The versions have multiple components (i.e. 0.10.30) and
+    // must be compared piecewise.
+    // Note: I'm ignoring the case where one version has multiple
+    // components and the other doesn't.
+    var v1 = otherVersion.split('.'),
+        v2 = version.split('.'),
+        versionLength = (v1.length > v2.length ? v2 : v1).length;
+
+    for (var i = 0; i < versionLength; ++i) {
+      if (parseInt(v1[i], 10) > parseInt(v2[i], 10)) {
+        return true;
+      }
+    }
+
+    // Special case, v1 has extra components but the initial components
+    // were identical, we assume this means newer but it might also mean
+    // that someone changed versioning systems.
+    if (i < v1.length) {
+      return true;
+    }
+
+    return false;
+  };
+
+  var env = function() {
+    if (typeof(process) !== 'undefined' &&
+        typeof(process.versions) !== 'undefined' &&
+        typeof(process.versions.node) === 'string') {
+
+      version = process.versions.node;
+      if (version.substr(1) === 'v') version = version.substr(1);
+
+      // Special casing node to avoid gating window.navigator.
+      // Version will be a string rather than a float.
+      return {
+        name: 'Node',
+        version: version,
+        userAgent: 'Node ' + version,
+        iframeNeedsLoad: false,
+        versionGreaterThan: versionGEThan
+      };
+    }
+
+    var userAgent = window.navigator.userAgent.toLowerCase(),
+        appName = window.navigator.appName,
+        navigatorVendor,
+        name = 'unknown';
+
+    if (userAgent.indexOf('opera') > -1 || userAgent.indexOf('opr') > -1) {
+      name = 'Opera';
+
+      if (/opr\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+
+    } else if (userAgent.indexOf('firefox') > -1)   {
+      name = 'Firefox';
+
+      if (/firefox\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+
+    } else if (appName === 'Microsoft Internet Explorer') {
+      // IE 10 and below
+      name = 'IE';
+
+      if (/msie ([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+
+    } else if (appName === 'Netscape' && userAgent.indexOf('trident') > -1) {
+      // IE 11+
+
+      name = 'IE';
+
+      if (/trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+
+    } else if (userAgent.indexOf('chrome') > -1) {
+      name = 'Chrome';
+
+      if (/chrome\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+
+    } else if ((navigatorVendor = window.navigator.vendor) &&
+      navigatorVendor.toLowerCase().indexOf('apple') > -1) {
+      name = 'Safari';
+
+      if (/version\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) {
+        version = parseFloat( RegExp.$1 );
+      }
+    }
+
+    return {
+      name: name,
+      version: version,
+      userAgent: window.navigator.userAgent,
+      iframeNeedsLoad: userAgent.indexOf('webkit') < 0,
+      versionGreaterThan: versionGEThan
+    };
+  }();
+
+
+  OTHelpers.env = env;
+
+  OTHelpers.browser = function() {
+    return OTHelpers.env.name;
+  };
+
+  OTHelpers.browserVersion = function() {
+    return OTHelpers.env;
+  };
+
+})();
+/*jshint browser:false, smarttabs:true*/
+/* global window, require */
+
+// tb_require('../../helpers.js')
+// tb_require('../environment.js')
+
+if (window.OTHelpers.env.name === 'Node') {
+  var request = require('request');
+
+  OTHelpers.request = function(url, options, callback) {
+    var completion = function(error, response, body) {
+      var event = {response: response, body: body};
+
+      // We need to detect things that Request considers a success,
+      // but we consider to be failures.
+      if (!error && response.statusCode >= 200 &&
+                  (response.statusCode < 300 || response.statusCode === 304) ) {
+        callback(null, event);
+      } else {
+        callback(error, event);
+      }
+    };
+
+    if (options.method.toLowerCase() === 'get') {
+      request.get(url, completion);
+    }
+    else {
+      request.post(url, options.body, completion);
+    }
+  };
+
+  OTHelpers.getJSON = function(url, options, callback) {
+    var extendedHeaders = require('underscore').extend(
+      {
+        'Accept': 'application/json'
+      },
+      options.headers || {}
+    );
+
+    request.get({
+      url: url,
+      headers: extendedHeaders,
+      json: true
+    }, function(err, response) {
+      callback(err, response && response.body);
+    });
+  };
+}
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../../helpers.js')
+// tb_require('../environment.js')
+
+function formatPostData(data) { //, contentType
+  // If it's a string, we assume it's properly encoded
+  if (typeof(data) === 'string') return data;
+
+  var queryString = [];
+
+  for (var key in data) {
+    queryString.push(
+      encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
+    );
+  }
+
+  return queryString.join('&').replace(/\+/g, '%20');
+}
+
+if (window.OTHelpers.env.name !== 'Node') {
+
+  OTHelpers.xdomainRequest = function(url, options, callback) {
+    /*global XDomainRequest*/
+    var xdr = new XDomainRequest(),
+        _options = options || {},
+        _method = _options.method.toLowerCase();
+
+    if(!_method) {
+      callback(new Error('No HTTP method specified in options'));
+      return;
+    }
+
+    _method = _method.toUpperCase();
+
+    if(!(_method === 'GET' || _method === 'POST')) {
+      callback(new Error('HTTP method can only be '));
+      return;
+    }
+
+    function done(err, event) {
+      xdr.onload = xdr.onerror = xdr.ontimeout = function() {};
+      xdr = void 0;
+      callback(err, event);
+    }
+
+
+    xdr.onload = function() {
+      done(null, {
+        target: {
+          responseText: xdr.responseText,
+          headers: {
+            'content-type': xdr.contentType
+          }
+        }
+      });
+    };
+
+    xdr.onerror = function() {
+      done(new Error('XDomainRequest of ' + url + ' failed'));
+    };
+
+    xdr.ontimeout = function() {
+      done(new Error('XDomainRequest of ' + url + ' timed out'));
+    };
+
+    xdr.open(_method, url);
+    xdr.send(options.body && formatPostData(options.body));
+
+  };
+
+  OTHelpers.request = function(url, options, callback) {
+    var request = new XMLHttpRequest(),
+        _options = options || {},
+        _method = _options.method;
+
+    if(!_method) {
+      callback(new Error('No HTTP method specified in options'));
+      return;
+    }
+
+    // Setup callbacks to correctly respond to success and error callbacks. This includes
+    // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore
+    // by default.
+    if(callback) {
+      OTHelpers.on(request, 'load', function(event) {
+        var status = event.target.status;
+
+        // We need to detect things that XMLHttpRequest considers a success,
+        // but we consider to be failures.
+        if ( status >= 200 && (status < 300 || status === 304) ) {
+          callback(null, event);
+        } else {
+          callback(event);
+        }
+      });
+
+      OTHelpers.on(request, 'error', callback);
+    }
+
+    request.open(options.method, url, true);
+
+    if (!_options.headers) _options.headers = {};
+
+    for (var name in _options.headers) {
+      request.setRequestHeader(name, _options.headers[name]);
+    }
+
+    request.send(options.body && formatPostData(options.body));
+  };
+
+
+  OTHelpers.getJSON = function(url, options, callback) {
+    options = options || {};
+
+    var done = function(error, event) {
+      if(error) {
+        callback(error, event && event.target && event.target.responseText);
+      } else {
+        var response;
+
+        try {
+          response = JSON.parse(event.target.responseText);
+        } catch(e) {
+          // Badly formed JSON
+          callback(e, event && event.target && event.target.responseText);
+          return;
+        }
+
+        callback(null, response, event);
+      }
+    };
+
+    if(options.xdomainrequest) {
+      OTHelpers.xdomainRequest(url, { method: 'GET' }, done);
+    } else {
+      var extendedHeaders = OTHelpers.extend({
+        'Accept': 'application/json'
+      }, options.headers || {});
+
+      OTHelpers.get(url, OTHelpers.extend(options || {}, {
+        headers: extendedHeaders
+      }), done);
+    }
+
+  };
+
+}
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
-
-(function(window, OTHelpers, undefined) {
-
-  OTHelpers.useLogHelpers = function(on){
-
-    // Log levels for OTLog.setLogLevel
-    on.DEBUG    = 5;
-    on.LOG      = 4;
-    on.INFO     = 3;
-    on.WARN     = 2;
-    on.ERROR    = 1;
-    on.NONE     = 0;
-
-    var _logLevel = on.NONE,
-        _logs = [],
-        _canApplyConsole = true;
-
-    try {
-      Function.prototype.bind.call(window.console.log, window.console);
-    } catch (err) {
-      _canApplyConsole = false;
-    }
-
-    // Some objects can't be logged in the console, mostly these are certain
-    // types of native objects that are exposed to JS. This is only really a
-    // problem with IE, hence only the IE version does anything.
-    var makeLogArgumentsSafe = function(args) { return args; };
-
-    if (OTHelpers.browser() === 'IE') {
-      makeLogArgumentsSafe = function(args) {
-        return [toDebugString(Array.prototype.slice.apply(args))];
-      };
-    }
-
-    // Generates a logging method for a particular method and log level.
-    //
-    // Attempts to handle the following cases:
-    // * the desired log method doesn't exist, call fallback (if available) instead
-    // * the console functionality isn't available because the developer tools (in IE)
-    // aren't open, call fallback (if available)
-    // * attempt to deal with weird IE hosted logging methods as best we can.
-    //
-    function generateLoggingMethod(method, level, fallback) {
-      return function() {
-        if (on.shouldLog(level)) {
-          var cons = window.console,
-              args = makeLogArgumentsSafe(arguments);
-
-          // In IE, window.console may not exist if the developer tools aren't open
-          // This also means that cons and cons[method] can appear at any moment
-          // hence why we retest this every time.
-          if (cons && cons[method]) {
-            // the desired console method isn't a real object, which means
-            // that we can't use apply on it. We force it to be a real object
-            // using Function.bind, assuming that's available.
-            if (cons[method].apply || _canApplyConsole) {
-              if (!cons[method].apply) {
-                cons[method] = Function.prototype.bind.call(cons[method], cons);
-              }
-
-              cons[method].apply(cons, args);
-            }
-            else {
-              // This isn't the same result as the above, but it's better
-              // than nothing.
-              cons[method](args);
+// tb_require('./environment.js')
+
+OTHelpers.useLogHelpers = function(on){
+
+  // Log levels for OTLog.setLogLevel
+  on.DEBUG    = 5;
+  on.LOG      = 4;
+  on.INFO     = 3;
+  on.WARN     = 2;
+  on.ERROR    = 1;
+  on.NONE     = 0;
+
+  var _logLevel = on.NONE,
+      _logs = [],
+      _canApplyConsole = true;
+
+  try {
+    Function.prototype.bind.call(window.console.log, window.console);
+  } catch (err) {
+    _canApplyConsole = false;
+  }
+
+  // Some objects can't be logged in the console, mostly these are certain
+  // types of native objects that are exposed to JS. This is only really a
+  // problem with IE, hence only the IE version does anything.
+  var makeLogArgumentsSafe = function(args) { return args; };
+
+  if (OTHelpers.env.name === 'IE') {
+    makeLogArgumentsSafe = function(args) {
+      return [toDebugString(prototypeSlice.apply(args))];
+    };
+  }
+
+  // Generates a logging method for a particular method and log level.
+  //
+  // Attempts to handle the following cases:
+  // * the desired log method doesn't exist, call fallback (if available) instead
+  // * the console functionality isn't available because the developer tools (in IE)
+  // aren't open, call fallback (if available)
+  // * attempt to deal with weird IE hosted logging methods as best we can.
+  //
+  function generateLoggingMethod(method, level, fallback) {
+    return function() {
+      if (on.shouldLog(level)) {
+        var cons = window.console,
+            args = makeLogArgumentsSafe(arguments);
+
+        // In IE, window.console may not exist if the developer tools aren't open
+        // This also means that cons and cons[method] can appear at any moment
+        // hence why we retest this every time.
+        if (cons && cons[method]) {
+          // the desired console method isn't a real object, which means
+          // that we can't use apply on it. We force it to be a real object
+          // using Function.bind, assuming that's available.
+          if (cons[method].apply || _canApplyConsole) {
+            if (!cons[method].apply) {
+              cons[method] = Function.prototype.bind.call(cons[method], cons);
             }
-          }
-          else if (fallback) {
-            fallback.apply(on, args);
-
-            // Skip appendToLogs, we delegate entirely to the fallback
-            return;
-          }
-
-          appendToLogs(method, makeLogArgumentsSafe(arguments));
-        }
-      };
-    }
-
-    on.log = generateLoggingMethod('log', on.LOG);
-
-    // Generate debug, info, warn, and error logging methods, these all fallback to on.log
-    on.debug = generateLoggingMethod('debug', on.DEBUG, on.log);
-    on.info = generateLoggingMethod('info', on.INFO, on.log);
-    on.warn = generateLoggingMethod('warn', on.WARN, on.log);
-    on.error = generateLoggingMethod('error', on.ERROR, on.log);
-
-
-    on.setLogLevel = function(level) {
-      _logLevel = typeof(level) === 'number' ? level : 0;
-      on.debug('TB.setLogLevel(' + _logLevel + ')');
-      return _logLevel;
-    };
-
-    on.getLogs = function() {
-      return _logs;
-    };
-
-    // Determine if the level is visible given the current logLevel.
-    on.shouldLog = function(level) {
-      return _logLevel >= level;
-    };
-
-    // Format the current time nicely for logging. Returns the current
-    // local time.
-    function formatDateStamp() {
-      var now = new Date();
-      return now.toLocaleTimeString() + now.getMilliseconds();
-    }
-
-    function toJson(object) {
+
+            cons[method].apply(cons, args);
+          }
+          else {
+            // This isn't the same result as the above, but it's better
+            // than nothing.
+            cons[method](args);
+          }
+        }
+        else if (fallback) {
+          fallback.apply(on, args);
+
+          // Skip appendToLogs, we delegate entirely to the fallback
+          return;
+        }
+
+        appendToLogs(method, makeLogArgumentsSafe(arguments));
+      }
+    };
+  }
+
+  on.log = generateLoggingMethod('log', on.LOG);
+
+  // Generate debug, info, warn, and error logging methods, these all fallback to on.log
+  on.debug = generateLoggingMethod('debug', on.DEBUG, on.log);
+  on.info = generateLoggingMethod('info', on.INFO, on.log);
+  on.warn = generateLoggingMethod('warn', on.WARN, on.log);
+  on.error = generateLoggingMethod('error', on.ERROR, on.log);
+
+
+  on.setLogLevel = function(level) {
+    _logLevel = typeof(level) === 'number' ? level : 0;
+    on.debug('TB.setLogLevel(' + _logLevel + ')');
+    return _logLevel;
+  };
+
+  on.getLogs = function() {
+    return _logs;
+  };
+
+  // Determine if the level is visible given the current logLevel.
+  on.shouldLog = function(level) {
+    return _logLevel >= level;
+  };
+
+  // Format the current time nicely for logging. Returns the current
+  // local time.
+  function formatDateStamp() {
+    var now = new Date();
+    return now.toLocaleTimeString() + now.getMilliseconds();
+  }
+
+  function toJson(object) {
+    try {
+      return JSON.stringify(object);
+    } catch(e) {
+      return object.toString();
+    }
+  }
+
+  function toDebugString(object) {
+    var components = [];
+
+    if (typeof(object) === 'undefined') {
+      // noop
+    }
+    else if (object === null) {
+      components.push('NULL');
+    }
+    else if (OTHelpers.isArray(object)) {
+      for (var i=0; i<object.length; ++i) {
+        components.push(toJson(object[i]));
+      }
+    }
+    else if (OTHelpers.isObject(object)) {
+      for (var key in object) {
+        var stringValue;
+
+        if (!OTHelpers.isFunction(object[key])) {
+          stringValue = toJson(object[key]);
+        }
+        else if (object.hasOwnProperty(key)) {
+          stringValue = 'function ' + key + '()';
+        }
+
+        components.push(key + ': ' + stringValue);
+      }
+    }
+    else if (OTHelpers.isFunction(object)) {
       try {
-        return JSON.stringify(object);
+        components.push(object.toString());
       } catch(e) {
-        return object.toString();
-      }
-    }
-
-    function toDebugString(object) {
-      var components = [];
-
-      if (typeof(object) === 'undefined') {
-        // noop
-      }
-      else if (object === null) {
-        components.push('NULL');
-      }
-      else if (OTHelpers.isArray(object)) {
-        for (var i=0; i<object.length; ++i) {
-          components.push(toJson(object[i]));
-        }
-      }
-      else if (OTHelpers.isObject(object)) {
-        for (var key in object) {
-          var stringValue;
-
-          if (!OTHelpers.isFunction(object[key])) {
-            stringValue = toJson(object[key]);
-          }
-          else if (object.hasOwnProperty(key)) {
-            stringValue = 'function ' + key + '()';
-          }
-
-          components.push(key + ': ' + stringValue);
-        }
-      }
-      else if (OTHelpers.isFunction(object)) {
-        try {
-          components.push(object.toString());
-        } catch(e) {
-          components.push('function()');
-        }
-      }
-      else  {
-        components.push(object.toString());
-      }
-
-      return components.join(', ');
-    }
-
-    // Append +args+ to logs, along with the current log level and the a date stamp.
-    function appendToLogs(level, args) {
-      if (!args) return;
-
-      var message = toDebugString(args);
-      if (message.length <= 2) return;
-
-      _logs.push(
-        [level, formatDateStamp(), message]
-      );
-    }
-  };
-
-  OTHelpers.useLogHelpers(OTHelpers);
-  OTHelpers.setLogLevel(OTHelpers.ERROR);
-
-})(window, window.OTHelpers);
+        components.push('function()');
+      }
+    }
+    else  {
+      components.push(object.toString());
+    }
+
+    return components.join(', ');
+  }
+
+  // Append +args+ to logs, along with the current log level and the a date stamp.
+  function appendToLogs(level, args) {
+    if (!args) return;
+
+    var message = toDebugString(args);
+    if (message.length <= 2) return;
+
+    _logs.push(
+      [level, formatDateStamp(), message]
+    );
+  }
+};
+
+OTHelpers.useLogHelpers(OTHelpers);
+OTHelpers.setLogLevel(OTHelpers.ERROR);
 
 /*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) {
+
+// 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.
+ElementCollection.prototype.on = function (eventName, handler) {
+  return this.forEach(function(element) {
     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) {
+  });
+};
+
+// Helper function for removing event listeners from dom elements.
+ElementCollection.prototype.off = function (eventName, handler) {
+  return this.forEach(function(element) {
     if (element.removeEventListener) {
       element.removeEventListener (eventName, handler,false);
     }
     else if (element.detachEvent) {
       element.detachEvent('on' + eventName, handler);
     }
-  };
-
-})(window, window.OTHelpers);
+  });
+};
+
+ElementCollection.prototype.once = function (eventName, handler) {
+  var removeAfterTrigger = $.bind(function() {
+    this.off(eventName, removeAfterTrigger);
+    handler.apply(null, arguments);
+  }, this);
+
+  return this.on(eventName, removeAfterTrigger);
+};
+
+// @remove
+OTHelpers.on = function(element, eventName,  handler) {
+  return $(element).on(eventName, handler);
+};
+
+// @remove
+OTHelpers.off = function(element, eventName, handler) {
+  return $(element).off(eventName, handler);
+};
+
+// @remove
+OTHelpers.once = function (element, eventName, handler) {
+  return $(element).once(eventName, handler);
+};
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./dom_events.js')
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   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"
+        if (typeof(document) !== 'undefined') {
+          if ( document.addEventListener ) {
+            document.removeEventListener('DOMContentLoaded', onDomReady, false);
+            window.removeEventListener('load', onDomReady, false);
+          } else {
+            document.detachEvent('onreadystatechange', onDomReady);
+            window.detachEvent('onload', onDomReady);
+          }
+        }
+
+        // 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 = [];
@@ -1126,62 +1657,287 @@
   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);
+
+      // fallback
+      window.addEventListener( 'load', 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);
+
+      // fallback
+      window.attachEvent( 'onload', onDomReady );
+    }
+  }
+
+})();
 
 /*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);
+OTHelpers.setCookie = function(key, value) {
+  try {
+    localStorage.setItem(key, value);
+  } catch (err) {
+    // Store in browser cookie
+    var date = new Date();
+    date.setTime(date.getTime()+(365*24*60*60*1000));
+    var expires = '; expires=' + date.toGMTString();
+    document.cookie = key + '=' + value + expires + '; path=/';
+  }
+};
+
+OTHelpers.getCookie = function(key) {
+  var value;
+
+  try {
+    value = localStorage.getItem(key);
+    return value;
+  } catch (err) {
+    // Check browser cookies
+    var nameEQ = key + '=';
+    var ca = document.cookie.split(';');
+    for(var i=0;i < ca.length;i++) {
+      var c = ca[i];
+      while (c.charAt(0) === ' ') {
+        c = c.substring(1,c.length);
+      }
+      if (c.indexOf(nameEQ) === 0) {
+        value = c.substring(nameEQ.length,c.length);
+      }
+    }
+
+    if (value) {
+      return value;
+    }
+  }
+
+  return null;
+};
+
+// tb_require('../helpers.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+
+
+OTHelpers.Collection = function(idField) {
+  var _models = [],
+      _byId = {},
+      _idField = idField || 'id';
+
+  OTHelpers.eventing(this, true);
+
+  var modelProperty = function(model, property) {
+    if(OTHelpers.isFunction(model[property])) {
+      return model[property]();
+    } else {
+      return model[property];
+    }
+  };
+
+  var onModelUpdate = OTHelpers.bind(function onModelUpdate (event) {
+        this.trigger('update', event);
+        this.trigger('update:'+event.target.id, event);
+      }, this),
+
+      onModelDestroy = OTHelpers.bind(function onModelDestroyed (event) {
+        this.remove(event.target, event.reason);
+      }, this);
+
+
+  this.reset = function() {
+    // Stop listening on the models, they are no longer our problem
+    OTHelpers.forEach(_models, function(model) {
+      model.off('updated', onModelUpdate, this);
+      model.off('destroyed', onModelDestroy, this);
+    }, this);
+
+    _models = [];
+    _byId = {};
+  };
+
+  this.destroy = function(reason) {
+    OTHelpers.forEach(_models, function(model) {
+      if(model && typeof model.destroy === 'function') {
+        model.destroy(reason, true);
+      }
+    });
+
+    this.reset();
+    this.off();
+  };
+
+  this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
+  this.has = function(id) { return id && _byId[id] !== void 0; };
+
+  this.toString = function() { return _models.toString(); };
+
+  // Return only models filtered by either a dict of properties
+  // or a filter function.
+  //
+  // @example Return all publishers with a streamId of 1
+  //   OT.publishers.where({streamId: 1})
+  //
+  // @example The same thing but filtering using a filter function
+  //   OT.publishers.where(function(publisher) {
+  //     return publisher.stream.id === 4;
+  //   });
+  //
+  // @example The same thing but filtering using a filter function
+  //          executed with a specific this
+  //   OT.publishers.where(function(publisher) {
+  //     return publisher.stream.id === 4;
+  //   }, self);
+  //
+  this.where = function(attrsOrFilterFn, context) {
+    if (OTHelpers.isFunction(attrsOrFilterFn)) {
+      return OTHelpers.filter(_models, attrsOrFilterFn, context);
+    }
+
+    return OTHelpers.filter(_models, function(model) {
+      for (var key in attrsOrFilterFn) {
+        if(!attrsOrFilterFn.hasOwnProperty(key)) {
+          continue;
+        }
+        if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
+      }
+
+      return true;
+    });
+  };
+
+  // Similar to where in behaviour, except that it only returns
+  // the first match.
+  this.find = function(attrsOrFilterFn, context) {
+    var filterFn;
+
+    if (OTHelpers.isFunction(attrsOrFilterFn)) {
+      filterFn = attrsOrFilterFn;
+    }
+    else {
+      filterFn = function(model) {
+        for (var key in attrsOrFilterFn) {
+          if(!attrsOrFilterFn.hasOwnProperty(key)) {
+            continue;
+          }
+          if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
+        }
+
+        return true;
+      };
+    }
+
+    filterFn = OTHelpers.bind(filterFn, context);
+
+    for (var i=0; i<_models.length; ++i) {
+      if (filterFn(_models[i]) === true) return _models[i];
+    }
+
+    return null;
+  };
+
+  this.add = function(model) {
+    var id = modelProperty(model, _idField);
+
+    if (this.has(id)) {
+      OTHelpers.warn('Model ' + id + ' is already in the collection', _models);
+      return this;
+    }
+
+    _byId[id] = _models.push(model) - 1;
+
+    model.on('updated', onModelUpdate, this);
+    model.on('destroyed', onModelDestroy, this);
+
+    this.trigger('add', model);
+    this.trigger('add:'+id, model);
+
+    return this;
+  };
+
+  this.remove = function(model, reason) {
+    var id = modelProperty(model, _idField);
+
+    _models.splice(_byId[id], 1);
+
+    // Shuffle everyone down one
+    for (var i=_byId[id]; i<_models.length; ++i) {
+      _byId[_models[i][_idField]] = i;
+    }
+
+    delete _byId[id];
+
+    model.off('updated', onModelUpdate, this);
+    model.off('destroyed', onModelDestroy, this);
+
+    this.trigger('remove', model, reason);
+    this.trigger('remove:'+id, model, reason);
+
+    return this;
+  };
+
+  // Retrigger the add event behaviour for each model. You can also
+  // select a subset of models to trigger using the same arguments
+  // as the #where method.
+  this._triggerAddEvents = function() {
+    var models = this.where.apply(this, arguments);
+    OTHelpers.forEach(models, function(model) {
+      this.trigger('add', model);
+      this.trigger('add:' + modelProperty(model, _idField), model);
+    }, this);
+  };
+
+  this.length = function() {
+    return _models.length;
+  };
+};
+
+
 /*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));
+};
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+(function() {
 
   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.env.name === '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;
@@ -1216,17 +1972,17 @@
 
 
   // 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),
+    var capNames = prototypeSlice.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;
@@ -1234,39 +1990,34 @@
       else if (testCapability(name) === false) {
         return false;
       }
     }
 
     return true;
   };
 
-})(window, window.OTHelpers);
+})();
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./capabilities.js')
 
-(function(window, OTHelpers, undefined) {
-
-  // Indicates if the client supports WebSockets.
-  OTHelpers.registerCapability('websockets', function() {
-    return 'WebSocket' in window;
-  });
-
-})(window, window.OTHelpers);
-
+// Indicates if the client supports WebSockets.
+OTHelpers.registerCapability('websockets', function() {
+  return 'WebSocket' in window && window.WebSocket !== void 0;
+});
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('../vendor/uuid.js')
 // tb_require('./dom_events.js')
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   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.
   //
@@ -1330,23 +2081,23 @@
 
     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));
+      timeouts.push(prototypeSlice.call(arguments));
       window.postMessage(messageName, '*');
     };
   }
   else {
     _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
-      var args = Array.prototype.slice.call(arguments),
+      var args = prototypeSlice.call(arguments),
           fn = args.shift();
 
       setTimeout(function() {
         fn.apply(null, args);
       }, 0);
     };
   }
 
@@ -1369,1014 +2120,908 @@
   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);
+      var args = prototypeSlice.call(arguments);
 
       OTHelpers.callAsync(function() {
         handler.apply(null, args);
       });
     };
   };
 
-})(window, window.OTHelpers);
+})();
 
 /*jshint browser:true, smarttabs:true*/
 /*global jasmine:true*/
 
 // tb_require('../../helpers.js')
 // tb_require('../callbacks.js')
 
-(function(window, OTHelpers, undefined) {
-
 /**
 * This base class defines the <code>on</code>, <code>once</code>, and <code>off</code>
 * methods of objects that can dispatch events.
 *
 * @class EventDispatcher
 */
-  OTHelpers.eventing = function(self, syncronous) {
-    var _events = {};
-
-
-    // Call the defaultAction, passing args
-    function executeDefaultAction(defaultAction, args) {
-      if (!defaultAction) return;
-
-      defaultAction.apply(null, args.slice());
-    }
-
-    // Execute each handler in +listeners+ with +args+.
-    //
-    // Each handler will be executed async. On completion the defaultAction
-    // handler will be executed with the args.
-    //
-    // @param [Array] listeners
-    //    An array of functions to execute. Each will be passed args.
-    //
-    // @param [Array] args
-    //    An array of arguments to execute each function in  +listeners+ with.
-    //
-    // @param [String] name
-    //    The name of this event.
-    //
-    // @param [Function, Null, Undefined] defaultAction
-    //    An optional function to execute after every other handler. This will execute even
-    //    if +listeners+ is empty. +defaultAction+ will be passed args as a normal
-    //    handler would.
-    //
-    // @return Undefined
-    //
-    function executeListenersAsyncronously(name, args, defaultAction) {
-      var listeners = _events[name];
-      if (!listeners || listeners.length === 0) return;
-
-      var listenerAcks = listeners.length;
-
-      OTHelpers.forEach(listeners, function(listener) { // , index
-        function filterHandlerAndContext(_listener) {
-          return _listener.context === listener.context && _listener.handler === listener.handler;
-        }
-
-        // We run this asynchronously so that it doesn't interfere with execution if an
-        // error happens
-        OTHelpers.callAsync(function() {
-          try {
-            // have to check if the listener has not been removed
-            if (_events[name] && OTHelpers.some(_events[name], filterHandlerAndContext)) {
-              (listener.closure || listener.handler).apply(listener.context || null, args);
-            }
-          }
-          finally {
-            listenerAcks--;
-
-            if (listenerAcks === 0) {
-              executeDefaultAction(defaultAction, args);
-            }
-          }
+OTHelpers.eventing = function(self, syncronous) {
+  var _events = {};
+
+  // Call the defaultAction, passing args
+  function executeDefaultAction(defaultAction, args) {
+    if (!defaultAction) return;
+
+    defaultAction.apply(null, args.slice());
+  }
+
+  // Execute each handler in +listeners+ with +args+.
+  //
+  // Each handler will be executed async. On completion the defaultAction
+  // handler will be executed with the args.
+  //
+  // @param [Array] listeners
+  //    An array of functions to execute. Each will be passed args.
+  //
+  // @param [Array] args
+  //    An array of arguments to execute each function in  +listeners+ with.
+  //
+  // @param [String] name
+  //    The name of this event.
+  //
+  // @param [Function, Null, Undefined] defaultAction
+  //    An optional function to execute after every other handler. This will execute even
+  //    if +listeners+ is empty. +defaultAction+ will be passed args as a normal
+  //    handler would.
+  //
+  // @return Undefined
+  //
+  function executeListenersAsyncronously(name, args, defaultAction) {
+    var listeners = _events[name];
+    if (!listeners || listeners.length === 0) return;
+
+    var listenerAcks = listeners.length;
+
+    OTHelpers.forEach(listeners, function(listener) { // , index
+      function filterHandlerAndContext(_listener) {
+        return _listener.context === listener.context && _listener.handler === listener.handler;
+      }
+
+      // We run this asynchronously so that it doesn't interfere with execution if an
+      // error happens
+      OTHelpers.callAsync(function() {
+        try {
+          // have to check if the listener has not been removed
+          if (_events[name] && OTHelpers.some(_events[name], filterHandlerAndContext)) {
+            (listener.closure || listener.handler).apply(listener.context || null, args);
+          }
+        }
+        finally {
+          listenerAcks--;
+
+          if (listenerAcks === 0) {
+            executeDefaultAction(defaultAction, args);
+          }
+        }
+      });
+    });
+  }
+
+
+  // This is identical to executeListenersAsyncronously except that handlers will
+  // be executed syncronously.
+  //
+  // On completion the defaultAction handler will be executed with the args.
+  //
+  // @param [Array] listeners
+  //    An array of functions to execute. Each will be passed args.
+  //
+  // @param [Array] args
+  //    An array of arguments to execute each function in  +listeners+ with.
+  //
+  // @param [String] name
+  //    The name of this event.
+  //
+  // @param [Function, Null, Undefined] defaultAction
+  //    An optional function to execute after every other handler. This will execute even
+  //    if +listeners+ is empty. +defaultAction+ will be passed args as a normal
+  //    handler would.
+  //
+  // @return Undefined
+  //
+  function executeListenersSyncronously(name, args) { // defaultAction is not used
+    var listeners = _events[name];
+    if (!listeners || listeners.length === 0) return;
+
+    OTHelpers.forEach(listeners, function(listener) { // index
+      (listener.closure || listener.handler).apply(listener.context || null, args);
+    });
+  }
+
+  var executeListeners = syncronous === true ?
+    executeListenersSyncronously : executeListenersAsyncronously;
+
+
+  var removeAllListenersNamed = function (eventName, context) {
+    if (_events[eventName]) {
+      if (context) {
+        // We are removing by context, get only events that don't
+        // match that context
+        _events[eventName] = OTHelpers.filter(_events[eventName], function(listener){
+          return listener.context !== context;
         });
-      });
-    }
-
-
-    // This is identical to executeListenersAsyncronously except that handlers will
-    // be executed syncronously.
-    //
-    // On completion the defaultAction handler will be executed with the args.
-    //
-    // @param [Array] listeners
-    //    An array of functions to execute. Each will be passed args.
-    //
-    // @param [Array] args
-    //    An array of arguments to execute each function in  +listeners+ with.
-    //
-    // @param [String] name
-    //    The name of this event.
-    //
-    // @param [Function, Null, Undefined] defaultAction
-    //    An optional function to execute after every other handler. This will execute even
-    //    if +listeners+ is empty. +defaultAction+ will be passed args as a normal
-    //    handler would.
-    //
-    // @return Undefined
-    //
-    function executeListenersSyncronously(name, args) { // defaultAction is not used
-      var listeners = _events[name];
-      if (!listeners || listeners.length === 0) return;
-
-      OTHelpers.forEach(listeners, function(listener) { // index
-        (listener.closure || listener.handler).apply(listener.context || null, args);
-      });
-    }
-
-    var executeListeners = syncronous === true ?
-      executeListenersSyncronously : executeListenersAsyncronously;
-
-
-    var removeAllListenersNamed = function (eventName, context) {
-      if (_events[eventName]) {
-        if (context) {
-          // We are removing by context, get only events that don't
-          // match that context
-          _events[eventName] = OTHelpers.filter(_events[eventName], function(listener){
-            return listener.context !== context;
-          });
-        }
-        else {
-          delete _events[eventName];
-        }
-      }
-    };
-
-    var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) {
-      var listener = {handler: handler};
-      if (context) listener.context = context;
-      if (closure) listener.closure = closure;
-
-      OTHelpers.forEach(eventNames, function(name) {
-        if (!_events[name]) _events[name] = [];
-        _events[name].push(listener);
-        var addedListener = name + ':added';
-        if (_events[addedListener]) {
-          executeListeners(addedListener, [_events[name].length]);
-        }
-      });
-    }, self);
-
-
-    var removeListeners = function (eventNames, handler, context) {
-      function filterHandlerAndContext(listener) {
-        return !(listener.handler === handler && listener.context === context);
-      }
-
-      OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) {
-        if (_events[name]) {
-          _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext);
-          if (_events[name].length === 0) delete _events[name];
-          var removedListener = name + ':removed';
-          if (_events[ removedListener]) {
-            executeListeners(removedListener, [_events[name] ? _events[name].length : 0]);
-          }
-        }
-      }, self));
-
-    };
-
-    // Execute any listeners bound to the +event+ Event.
-    //
-    // Each handler will be executed async. On completion the defaultAction
-    // handler will be executed with the args.
-    //
-    // @param [Event] event
-    //    An Event object.
-    //
-    // @param [Function, Null, Undefined] defaultAction
-    //    An optional function to execute after every other handler. This will execute even
-    //    if there are listeners bound to this event. +defaultAction+ will be passed
-    //    args as a normal handler would.
-    //
-    // @return this
-    //
-    self.dispatchEvent = function(event, defaultAction) {
-      if (!event.type) {
-        OTHelpers.error('OTHelpers.Eventing.dispatchEvent: Event has no type');
-        OTHelpers.error(event);
-
-        throw new Error('OTHelpers.Eventing.dispatchEvent: Event has no type');
-      }
-
-      if (!event.target) {
-        event.target = this;
-      }
-
-      if (!_events[event.type] || _events[event.type].length === 0) {
-        executeDefaultAction(defaultAction, [event]);
-        return;
-      }
-
-      executeListeners(event.type, [event], defaultAction);
-
-      return this;
-    };
-
-    // Execute each handler for the event called +name+.
-    //
-    // Each handler will be executed async, and any exceptions that they throw will
-    // be caught and logged
-    //
-    // How to pass these?
-    //  * defaultAction
-    //
-    // @example
-    //  foo.on('bar', function(name, message) {
-    //    alert("Hello " + name + ": " + message);
-    //  });
-    //
-    //  foo.trigger('OpenTok', 'asdf');     // -> Hello OpenTok: asdf
-    //
-    //
-    // @param [String] eventName
-    //    The name of this event.
-    //
-    // @param [Array] arguments
-    //    Any additional arguments beyond +eventName+ will be passed to the handlers.
-    //
-    // @return this
-    //
-    self.trigger = function(eventName) {
-      if (!_events[eventName] || _events[eventName].length === 0) {
-        return;
-      }
-
-      var args = Array.prototype.slice.call(arguments);
-
-      // Remove the eventName arg
-      args.shift();
-
-      executeListeners(eventName, args);
-
-      return this;
-    };
-
-   /**
-    * Adds an event handler function for one or more events.
-    *
-    * <p>
-    * The following code adds an event handler for one event:
-    * </p>
-    *
-    * <pre>
-    * obj.on("eventName", function (event) {
-    *     // This is the event handler.
-    * });
-    * </pre>
-    *
-    * <p>If you pass in multiple event names and a handler method, the handler is
-    * registered for each of those events:</p>
-    *
-    * <pre>
-    * obj.on("eventName1 eventName2",
-    *        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 value of <code>this</code> in the handler method:</p>
-    *
-    * <pre>obj.on("eventName",
-    *        function (event) {
-    *            // This is the event handler.
-    *        },
-    *        obj);
-    * </pre>
-    *
-    * <p>
-    * The method also supports an alternate syntax, in which the first parameter is an object
-    * that is a hash map of event names and handler functions and the second parameter (optional)
-    * is the context for this in each handler:
-    * </p>
-    * <pre>
-    * obj.on(
-    *    {
-    *       eventName1: function (event) {
-    *               // This is the handler for eventName1.
-    *           },
-    *       eventName2:  function (event) {
-    *               // This is the handler for eventName2.
-    *           }
-    *    },
-    *    obj);
-    * </pre>
-    *
-    * <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. You can specify multiple event
-    * names in this string, separating them with a space. The event handler will process each of
-    * the events.
-    * @param {Function} handler The handler function to process the event. This function takes
-    * the event object as a parameter.
-    * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
-    * handler function.
-    *
-    * @returns {EventDispatcher} The EventDispatcher object.
-    *
-    * @memberOf EventDispatcher
-    * @method #on
-    * @see <a href="#off">off()</a>
-    * @see <a href="#once">once()</a>
-    * @see <a href="#events">Events</a>
-    */
-    self.on = function(eventNames, handlerOrContext, context) {
-      if (typeof(eventNames) === 'string' && handlerOrContext) {
-        addListeners(eventNames.split(' '), handlerOrContext, context);
       }
       else {
-        for (var name in eventNames) {
-          if (eventNames.hasOwnProperty(name)) {
-            addListeners([name], eventNames[name], handlerOrContext);
-          }
-        }
-      }
-
-      return this;
-    };
-
-   /**
-    * Removes an event handler or handlers.
-    *
-    * <p>If you pass in one event name and a handler method, the handler is removed for that
-    * event:</p>
-    *
-    * <pre>obj.off("eventName", eventHandler);</pre>
-    *
-    * <p>If you pass in multiple event names and a handler method, the handler is removed for
-    * those events:</p>
-    *
-    * <pre>obj.off("eventName1 eventName2", eventHandler);</pre>
-    *
-    * <p>If you pass in an event name (or names) and <i>no</i> handler method, all handlers are
-    * removed for those events:</p>
-    *
-    * <pre>obj.off("event1Name event2Name");</pre>
-    *
-    * <p>If you pass in no arguments, <i>all</i> event handlers are removed for all events
-    * dispatched by the object:</p>
-    *
-    * <pre>obj.off();</pre>
-    *
-    * <p>
-    * The method also supports an alternate syntax, in which the first parameter is an object that
-    * is a hash map of event names and handler functions and the second parameter (optional) is
-    * the context for this in each handler:
-    * </p>
-    * <pre>
-    * obj.off(
-    *    {
-    *       eventName1: event1Handler,
-    *       eventName2: event2Handler
-    *    });
-    * </pre>
-    *
-    * @param {String} type (Optional) The string identifying the type of event. You can
-    * use a space to specify multiple events, as in "accessAllowed accessDenied
-    * accessDialogClosed". If you pass in no <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. The handler
-    * must be the same function object as was passed into <code>on()</code>. Be careful with
-    * helpers like <code>bind()</code> that return a new function when called. 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 removed for all specified events and handlers that use the specified context. (The
-    * context must match the context passed into <code>on()</code>.)
-    *
-    * @returns {Object} The object that dispatched the event.
-    *
-    * @memberOf EventDispatcher
-    * @method #off
-    * @see <a href="#on">on()</a>
-    * @see <a href="#once">once()</a>
-    * @see <a href="#events">Events</a>
-    */
-    self.off = function(eventNames, handlerOrContext, context) {
-      if (typeof eventNames === 'string') {
-        if (handlerOrContext && OTHelpers.isFunction(handlerOrContext)) {
-          removeListeners(eventNames.split(' '), handlerOrContext, context);
-        }
-        else {
-          OTHelpers.forEach(eventNames.split(' '), function(name) {
-            removeAllListenersNamed(name, handlerOrContext);
-          }, this);
-        }
-
-      } else if (!eventNames) {
-        // remove all bound events
-        _events = {};
-
+        delete _events[eventName];
+      }
+    }
+  };
+
+  var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) {
+    var listener = {handler: handler};
+    if (context) listener.context = context;
+    if (closure) listener.closure = closure;
+
+    OTHelpers.forEach(eventNames, function(name) {
+      if (!_events[name]) _events[name] = [];
+      _events[name].push(listener);
+      var addedListener = name + ':added';
+      if (_events[addedListener]) {
+        executeListeners(addedListener, [_events[name].length]);
+      }
+    });
+  }, self);
+
+
+  var removeListeners = function (eventNames, handler, context) {
+    function filterHandlerAndContext(listener) {
+      return !(listener.handler === handler && listener.context === context);
+    }
+
+    OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) {
+      if (_events[name]) {
+        _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext);
+        if (_events[name].length === 0) delete _events[name];
+        var removedListener = name + ':removed';
+        if (_events[ removedListener]) {
+          executeListeners(removedListener, [_events[name] ? _events[name].length : 0]);
+        }
+      }
+    }, self));
+
+  };
+
+  // Execute any listeners bound to the +event+ Event.
+  //
+  // Each handler will be executed async. On completion the defaultAction
+  // handler will be executed with the args.
+  //
+  // @param [Event] event
+  //    An Event object.
+  //
+  // @param [Function, Null, Undefined] defaultAction
+  //    An optional function to execute after every other handler. This will execute even
+  //    if there are listeners bound to this event. +defaultAction+ will be passed
+  //    args as a normal handler would.
+  //
+  // @return this
+  //
+  self.dispatchEvent = function(event, defaultAction) {
+    if (!event.type) {
+      OTHelpers.error('OTHelpers.Eventing.dispatchEvent: Event has no type');
+      OTHelpers.error(event);
+
+      throw new Error('OTHelpers.Eventing.dispatchEvent: Event has no type');
+    }
+
+    if (!event.target) {
+      event.target = this;
+    }
+
+    if (!_events[event.type] || _events[event.type].length === 0) {
+      executeDefaultAction(defaultAction, [event]);
+      return;
+    }
+
+    executeListeners(event.type, [event], defaultAction);
+
+    return this;
+  };
+
+  // Execute each handler for the event called +name+.
+  //
+  // Each handler will be executed async, and any exceptions that they throw will
+  // be caught and logged
+  //
+  // How to pass these?
+  //  * defaultAction
+  //
+  // @example
+  //  foo.on('bar', function(name, message) {
+  //    alert("Hello " + name + ": " + message);
+  //  });
+  //
+  //  foo.trigger('OpenTok', 'asdf');     // -> Hello OpenTok: asdf
+  //
+  //
+  // @param [String] eventName
+  //    The name of this event.
+  //
+  // @param [Array] arguments
+  //    Any additional arguments beyond +eventName+ will be passed to the handlers.
+  //
+  // @return this
+  //
+  self.trigger = function(eventName) {
+    if (!_events[eventName] || _events[eventName].length === 0) {
+      return;
+    }
+
+    var args = prototypeSlice.call(arguments);
+
+    // Remove the eventName arg
+    args.shift();
+
+    executeListeners(eventName, args);
+
+    return this;
+  };
+
+ /**
+  * Adds an event handler function for one or more events.
+  *
+  * <p>
+  * The following code adds an event handler for one event:
+  * </p>
+  *
+  * <pre>
+  * obj.on("eventName", function (event) {
+  *     // This is the event handler.
+  * });
+  * </pre>
+  *
+  * <p>If you pass in multiple event names and a handler method, the handler is
+  * registered for each of those events:</p>
+  *
+  * <pre>
+  * obj.on("eventName1 eventName2",
+  *        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 value of <code>this</code> in the handler method:</p>
+  *
+  * <pre>obj.on("eventName",
+  *        function (event) {
+  *            // This is the event handler.
+  *        },
+  *        obj);
+  * </pre>
+  *
+  * <p>
+  * The method also supports an alternate syntax, in which the first parameter is an object
+  * that is a hash map of event names and handler functions and the second parameter (optional)
+  * is the context for this in each handler:
+  * </p>
+  * <pre>
+  * obj.on(
+  *    {
+  *       eventName1: function (event) {
+  *               // This is the handler for eventName1.
+  *           },
+  *       eventName2:  function (event) {
+  *               // This is the handler for eventName2.
+  *           }
+  *    },
+  *    obj);
+  * </pre>
+  *
+  * <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. You can specify multiple event
+  * names in this string, separating them with a space. The event handler will process each of
+  * the events.
+  * @param {Function} handler The handler function to process the event. This function takes
+  * the event object as a parameter.
+  * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
+  * handler function.
+  *
+  * @returns {EventDispatcher} The EventDispatcher object.
+  *
+  * @memberOf EventDispatcher
+  * @method #on
+  * @see <a href="#off">off()</a>
+  * @see <a href="#once">once()</a>
+  * @see <a href="#events">Events</a>
+  */
+  self.on = function(eventNames, handlerOrContext, context) {
+    if (typeof(eventNames) === 'string' && handlerOrContext) {
+      addListeners(eventNames.split(' '), handlerOrContext, context);
+    }
+    else {
+      for (var name in eventNames) {
+        if (eventNames.hasOwnProperty(name)) {
+          addListeners([name], eventNames[name], handlerOrContext);
+        }
+      }
+    }
+
+    return this;
+  };
+
+ /**
+  * Removes an event handler or handlers.
+  *
+  * <p>If you pass in one event name and a handler method, the handler is removed for that
+  * event:</p>
+  *
+  * <pre>obj.off("eventName", eventHandler);</pre>
+  *
+  * <p>If you pass in multiple event names and a handler method, the handler is removed for
+  * those events:</p>
+  *
+  * <pre>obj.off("eventName1 eventName2", eventHandler);</pre>
+  *
+  * <p>If you pass in an event name (or names) and <i>no</i> handler method, all handlers are
+  * removed for those events:</p>
+  *
+  * <pre>obj.off("event1Name event2Name");</pre>
+  *
+  * <p>If you pass in no arguments, <i>all</i> event handlers are removed for all events
+  * dispatched by the object:</p>
+  *
+  * <pre>obj.off();</pre>
+  *
+  * <p>
+  * The method also supports an alternate syntax, in which the first parameter is an object that
+  * is a hash map of event names and handler functions and the second parameter (optional) is
+  * the context for this in each handler:
+  * </p>
+  * <pre>
+  * obj.off(
+  *    {
+  *       eventName1: event1Handler,
+  *       eventName2: event2Handler
+  *    });
+  * </pre>
+  *
+  * @param {String} type (Optional) The string identifying the type of event. You can
+  * use a space to specify multiple events, as in "accessAllowed accessDenied
+  * accessDialogClosed". If you pass in no <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. The handler
+  * must be the same function object as was passed into <code>on()</code>. Be careful with
+  * helpers like <code>bind()</code> that return a new function when called. 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 removed for all specified events and handlers that use the specified context. (The
+  * context must match the context passed into <code>on()</code>.)
+  *
+  * @returns {Object} The object that dispatched the event.
+  *
+  * @memberOf EventDispatcher
+  * @method #off
+  * @see <a href="#on">on()</a>
+  * @see <a href="#once">once()</a>
+  * @see <a href="#events">Events</a>
+  */
+  self.off = function(eventNames, handlerOrContext, context) {
+    if (typeof eventNames === 'string') {
+      if (handlerOrContext && OTHelpers.isFunction(handlerOrContext)) {
+        removeListeners(eventNames.split(' '), handlerOrContext, context);
+      }
+      else {
+        OTHelpers.forEach(eventNames.split(' '), function(name) {
+          removeAllListenersNamed(name, handlerOrContext);
+        }, this);
+      }
+
+    } else if (!eventNames) {
+      // remove all bound events
+      _events = {};
+
+    } else {
+      for (var name in eventNames) {
+        if (eventNames.hasOwnProperty(name)) {
+          removeListeners([name], eventNames[name], handlerOrContext);
+        }
+      }
+    }
+
+    return this;
+  };
+
+
+ /**
+  * Adds an event handler function for one or more events. Once the handler is called,
+  * the specified handler method is removed as a handler for this event. (When you use
+  * the <code>on()</code> method to add an event handler, the handler is <i>not</i>
+  * removed when it is called.) The <code>once()</code> method is the equivilent of
+  * calling the <code>on()</code>
+  * method and calling <code>off()</code> the first time the handler is invoked.
+  *
+  * <p>
+  * The following code adds a one-time event handler for the <code>accessAllowed</code> event:
+  * </p>
+  *
+  * <pre>
+  * obj.once("eventName", function (event) {
+  *    // This is the event handler.
+  * });
+  * </pre>
+  *
+  * <p>If you pass in multiple event names and a handler method, the handler is registered
+  * for each of those events:</p>
+  *
+  * <pre>obj.once("eventName1 eventName2"
+  *          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 value of
+  * <code>this</code> in the handler method:</p>
+  *
+  * <pre>obj.once("eventName",
+  *          function (event) {
+  *              // This is the event handler.
+  *          },
+  *          obj);
+  * </pre>
+  *
+  * <p>
+  * The method also supports an alternate syntax, in which the first parameter is an object that
+  * is a hash map of event names and handler functions and the second parameter (optional) is the
+  * context for this in each handler:
+  * </p>
+  * <pre>
+  * obj.once(
+  *    {
+  *       eventName1: function (event) {
+  *                  // This is the event handler for eventName1.
+  *           },
+  *       eventName2:  function (event) {
+  *                  // This is the event handler for eventName1.
+  *           }
+  *    },
+  *    obj);
+  * </pre>
+  *
+  * @param {String} type The string identifying the type of event. You can specify multiple
+  * event names in this string, separating them with a space. The event handler will process
+  * the first occurence of the events. After the first event, the handler is removed (for
+  * all specified events).
+  * @param {Function} handler The handler function to process the event. This function takes
+  * the event object as a parameter.
+  * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
+  * handler function.
+  *
+  * @returns {Object} The object that dispatched the event.
+  *
+  * @memberOf EventDispatcher
+  * @method #once
+  * @see <a href="#on">on()</a>
+  * @see <a href="#off">off()</a>
+  * @see <a href="#events">Events</a>
+  */
+  self.once = function(eventNames, handler, context) {
+    var names = eventNames.split(' '),
+        fun = OTHelpers.bind(function() {
+          var result = handler.apply(context || null, arguments);
+          removeListeners(names, handler, context);
+
+          return result;
+        }, this);
+
+    addListeners(names, handler, context, fun);
+    return this;
+  };
+
+
+  /**
+  * Deprecated; use <a href="#on">on()</a> or <a href="#once">once()</a> instead.
+  * <p>
+  * This method registers a method as an event listener for a specific event.
+  * <p>
+  *
+  * <p>
+  *   If a handler is not registered for an event, the event is ignored locally. If the
+  *   event listener function does not exist, the event is ignored locally.
+  * </p>
+  * <p>
+  *   Throws an exception if the <code>listener</code> name is invalid.
+  * </p>
+  *
+  * @param {String} type The string identifying the type of event.
+  *
+  * @param {Function} listener The function to be invoked when the object dispatches the event.
+  *
+  * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
+  * handler function.
+  *
+  * @memberOf EventDispatcher
+  * @method #addEventListener
+  * @see <a href="#on">on()</a>
+  * @see <a href="#once">once()</a>
+  * @see <a href="#events">Events</a>
+  */
+  // See 'on' for usage.
+  // @depreciated will become a private helper function in the future.
+  self.addEventListener = function(eventName, handler, context) {
+    OTHelpers.warn('The addEventListener() method is deprecated. Use on() or once() instead.');
+    addListeners([eventName], handler, context);
+  };
+
+
+  /**
+  * Deprecated; use <a href="#on">on()</a> or <a href="#once">once()</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>
+  *
+  * @param {String} type The string identifying the type of event.
+  *
+  * @param {Function} listener The event listener function to remove.
+  *
+  * @param {Object} context (Optional) If you specify a <code>context</code>, the event
+  * handler is removed for all specified events and event listeners that use the specified
+  context. (The context must match the context passed into
+  * <code>addEventListener()</code>.)
+  *
+  * @memberOf EventDispatcher
+  * @method #removeEventListener
+  * @see <a href="#off">off()</a>
+  * @see <a href="#events">Events</a>
+  */
+  // See 'off' for usage.
+  // @depreciated will become a private helper function in the future.
+  self.removeEventListener = function(eventName, handler, context) {
+    OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.');
+    removeListeners([eventName], handler, context);
+  };
+
+
+  return self;
+};
+
+OTHelpers.eventing.Event = function() {
+  return function (type, cancelable) {
+    this.type = type;
+    this.cancelable = cancelable !== undefined ? cancelable : true;
+
+    var _defaultPrevented = false;
+
+    this.preventDefault = function() {
+      if (this.cancelable) {
+        _defaultPrevented = true;
       } else {
-        for (var name in eventNames) {
-          if (eventNames.hasOwnProperty(name)) {
-            removeListeners([name], eventNames[name], handlerOrContext);
-          }
-        }
-      }
-
-      return this;
-    };
-
-
-   /**
-    * Adds an event handler function for one or more events. Once the handler is called,
-    * the specified handler method is removed as a handler for this event. (When you use
-    * the <code>on()</code> method to add an event handler, the handler is <i>not</i>
-    * removed when it is called.) The <code>once()</code> method is the equivilent of
-    * calling the <code>on()</code>
-    * method and calling <code>off()</code> the first time the handler is invoked.
-    *
-    * <p>
-    * The following code adds a one-time event handler for the <code>accessAllowed</code> event:
-    * </p>
-    *
-    * <pre>
-    * obj.once("eventName", function (event) {
-    *    // This is the event handler.
-    * });
-    * </pre>
-    *
-    * <p>If you pass in multiple event names and a handler method, the handler is registered
-    * for each of those events:</p>
-    *
-    * <pre>obj.once("eventName1 eventName2"
-    *          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 value of
-    * <code>this</code> in the handler method:</p>
-    *
-    * <pre>obj.once("eventName",
-    *          function (event) {
-    *              // This is the event handler.
-    *          },
-    *          obj);
-    * </pre>
-    *
-    * <p>
-    * The method also supports an alternate syntax, in which the first parameter is an object that
-    * is a hash map of event names and handler functions and the second parameter (optional) is the
-    * context for this in each handler:
-    * </p>
-    * <pre>
-    * obj.once(
-    *    {
-    *       eventName1: function (event) {
-    *                  // This is the event handler for eventName1.
-    *           },
-    *       eventName2:  function (event) {
-    *                  // This is the event handler for eventName1.
-    *           }
-    *    },
-    *    obj);
-    * </pre>
-    *
-    * @param {String} type The string identifying the type of event. You can specify multiple
-    * event names in this string, separating them with a space. The event handler will process
-    * the first occurence of the events. After the first event, the handler is removed (for
-    * all specified events).
-    * @param {Function} handler The handler function to process the event. This function takes
-    * the event object as a parameter.
-    * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
-    * handler function.
-    *
-    * @returns {Object} The object that dispatched the event.
-    *
-    * @memberOf EventDispatcher
-    * @method #once
-    * @see <a href="#on">on()</a>
-    * @see <a href="#off">off()</a>
-    * @see <a href="#events">Events</a>
-    */
-    self.once = function(eventNames, handler, context) {
-      var names = eventNames.split(' '),
-          fun = OTHelpers.bind(function() {
-            var result = handler.apply(context || null, arguments);
-            removeListeners(names, handler, context);
-
-            return result;
-          }, this);
-
-      addListeners(names, handler, context, fun);
-      return this;
-    };
-
-
-    /**
-    * Deprecated; use <a href="#on">on()</a> or <a href="#once">once()</a> instead.
-    * <p>
-    * This method registers a method as an event listener for a specific event.
-    * <p>
-    *
-    * <p>
-    *   If a handler is not registered for an event, the event is ignored locally. If the
-    *   event listener function does not exist, the event is ignored locally.
-    * </p>
-    * <p>
-    *   Throws an exception if the <code>listener</code> name is invalid.
-    * </p>
-    *
-    * @param {String} type The string identifying the type of event.
-    *
-    * @param {Function} listener The function to be invoked when the object dispatches the event.
-    *
-    * @param {Object} context (Optional) Defines the value of <code>this</code> in the event
-    * handler function.
-    *
-    * @memberOf EventDispatcher
-    * @method #addEventListener
-    * @see <a href="#on">on()</a>
-    * @see <a href="#once">once()</a>
-    * @see <a href="#events">Events</a>
-    */
-    // See 'on' for usage.
-    // @depreciated will become a private helper function in the future.
-    self.addEventListener = function(eventName, handler, context) {
-      OTHelpers.warn('The addEventListener() method is deprecated. Use on() or once() instead.');
-      addListeners([eventName], handler, context);
-    };
-
-
-    /**
-    * Deprecated; use <a href="#on">on()</a> or <a href="#once">once()</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>
-    *
-    * @param {String} type The string identifying the type of event.
-    *
-    * @param {Function} listener The event listener function to remove.
-    *
-    * @param {Object} context (Optional) If you specify a <code>context</code>, the event
-    * handler is removed for all specified events and event listeners that use the specified
-    context. (The context must match the context passed into
-    * <code>addEventListener()</code>.)
-    *
-    * @memberOf EventDispatcher
-    * @method #removeEventListener
-    * @see <a href="#off">off()</a>
-    * @see <a href="#events">Events</a>
-    */
-    // See 'off' for usage.
-    // @depreciated will become a private helper function in the future.
-    self.removeEventListener = function(eventName, handler, context) {
-      OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.');
-      removeListeners([eventName], handler, context);
-    };
-
-
-
-    return self;
-  };
-
-  OTHelpers.eventing.Event = function() {
-
-    return function (type, cancelable) {
-      this.type = type;
-      this.cancelable = cancelable !== undefined ? cancelable : true;
-
-      var _defaultPrevented = false;
-
-      this.preventDefault = function() {
-        if (this.cancelable) {
-          _defaultPrevented = true;
-        } else {
-          OTHelpers.warn('Event.preventDefault :: Trying to preventDefault ' +
-            'on an Event that isn\'t cancelable');
-        }
-      };
-
-      this.isDefaultPrevented = function() {
-        return _defaultPrevented;
-      };
-    };
-
-  };
-
-})(window, window.OTHelpers);
-
-/*jshint browser:true, smarttabs:true*/
+        OTHelpers.warn('Event.preventDefault :: Trying to preventDefault ' +
+          'on an Event that isn\'t cancelable');
+      }
+    };
+
+    this.isDefaultPrevented = function() {
+      return _defaultPrevented;
+    };
+  };
+};
+
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+// tb_require('./callbacks.js')
+// tb_require('./dom_events.js')
+
+OTHelpers.createElement = function(nodeName, attributes, children, doc) {
+  var element = (doc || document).createElement(nodeName);
+
+  if (attributes) {
+    for (var name in attributes) {
+      if (typeof(attributes[name]) === 'object') {
+        if (!element[name]) element[name] = {};
+
+        var subAttrs = attributes[name];
+        for (var n in subAttrs) {
+          element[name][n] = subAttrs[n];
+        }
+      }
+      else if (name === 'className') {
+        element.className = attributes[name];
+      }
+      else {
+        element.setAttribute(name, attributes[name]);
+      }
+    }
+  }
+
+  var setChildren = function(child) {
+    if(typeof child === 'string') {
+      element.innerHTML = element.innerHTML + child;
+    } else {
+      element.appendChild(child);
+    }
+  };
+
+  if($.isArray(children)) {
+    $.forEach(children, setChildren);
+  } else if(children) {
+    setChildren(children);
+  }
+
+  return element;
+};
+
+OTHelpers.createButton = function(innerHTML, attributes, events) {
+  var button = $.createElement('button', attributes, innerHTML);
+
+  if (events) {
+    for (var name in events) {
+      if (events.hasOwnProperty(name)) {
+        $.on(button, name, events[name]);
+      }
+    }
+
+    button._boundEvents = events;
+  }
+
+  return button;
+};
+/*jshint browser:true, smarttabs:true */
 
 // tb_require('../helpers.js')
 // tb_require('./callbacks.js')
 
 // DOM helpers
-(function(window, OTHelpers, undefined) {
-
-  OTHelpers.isElementNode = function(node) {
-    return node && typeof node === 'object' && node.nodeType === 1;
-  };
-
-  // Returns true if the client supports element.classList
-  OTHelpers.supportsClassList = function() {
-    var hasSupport = (typeof document !== 'undefined') &&
-            ('classList' in document.createElement('a'));
-
-    OTHelpers.supportsClassList = function() { return hasSupport; };
-
-    return hasSupport;
-  };
-
-  OTHelpers.removeElement = function(element) {
-    if (element && element.parentNode) {
+
+ElementCollection.prototype.appendTo = function(parentElement) {
+  if (!parentElement) throw new Error('appendTo requires a DOMElement to append to.');
+
+  return this.forEach(parentElement.appendChild.bind(parentElement));
+};
+
+ElementCollection.prototype.after = function(prevElement) {
+  if (!prevElement) throw new Error('after requires a DOMElement to insert after');
+
+  return this.forEach(function(element) {
+    if (element.parentElement) {
+      if (prevElement !== element.parentNode.lastChild) {
+        element.parentElement.before(element, prevElement);
+      }
+      else {
+        element.parentElement.appendChild(element);
+      }
+    }
+  });
+};
+
+ElementCollection.prototype.before = function(nextElement) {
+  if (!nextElement) throw new Error('before requires a DOMElement to insert before');
+
+  return this.forEach(function(element) {
+    if (element.parentElement) {
+      element.parentElement.before(element, nextElement);
+    }
+  });
+};
+
+ElementCollection.prototype.remove = function () {
+  return this.forEach(function(element) {
+    if (element.parentNode) {
       element.parentNode.removeChild(element);
     }
-  };
-
-  OTHelpers.removeElementById = function(elementId) {
-    /*jshint newcap:false */
-    this.removeElement(OTHelpers(elementId));
-  };
-
-  OTHelpers.removeElementsByType = function(parentElem, type) {
-    if (!parentElem) return;
-
-    var elements = parentElem.getElementsByTagName(type);
-
+  });
+};
+
+ElementCollection.prototype.empty = function () {
+  return this.forEach(function(element) {
     // elements is a "live" NodesList collection. Meaning that the collection
     // itself will be mutated as we remove elements from the DOM. This means
     // that "while there are still elements" is safer than "iterate over each
     // element" as the collection length and the elements indices will be modified
     // with each iteration.
-    while (elements.length) {
-      parentElem.removeChild(elements[0]);
-    }
-  };
-
-  OTHelpers.emptyElement = function(element) {
     while (element.firstChild) {
       element.removeChild(element.firstChild);
     }
-    return element;
-  };
-
-  OTHelpers.createElement = function(nodeName, attributes, children, doc) {
-    var element = (doc || document).createElement(nodeName);
-
-    if (attributes) {
-      for (var name in attributes) {
-        if (typeof(attributes[name]) === 'object') {
-          if (!element[name]) element[name] = {};
-
-          var subAttrs = attributes[name];
-          for (var n in subAttrs) {
-            element[name][n] = subAttrs[n];
-          }
-        }
-        else if (name === 'className') {
-          element.className = attributes[name];
-        }
-        else {
-          element.setAttribute(name, attributes[name]);
-        }
-      }
-    }
-
-    var setChildren = function(child) {
-      if(typeof child === 'string') {
-        element.innerHTML = element.innerHTML + child;
-      } else {
-        element.appendChild(child);
-      }
-    };
-
-    if(OTHelpers.isArray(children)) {
-      OTHelpers.forEach(children, setChildren);
-    } else if(children) {
-      setChildren(children);
-    }
-
-    return element;
-  };
-
-  OTHelpers.createButton = function(innerHTML, attributes, events) {
-    var button = OTHelpers.createElement('button', attributes, innerHTML);
-
-    if (events) {
-      for (var name in events) {
-        if (events.hasOwnProperty(name)) {
-          OTHelpers.on(button, name, events[name]);
-        }
-      }
-
-      button._boundEvents = events;
-    }
-
-    return button;
-  };
-
-
-  // Detects when an element is not part of the document flow because
-  // it or one of it's ancesters has display:none.
-  OTHelpers.isDisplayNone = function(element) {
+  });
+};
+
+
+// Detects when an element is not part of the document flow because
+// it or one of it's ancesters has display:none.
+ElementCollection.prototype.isDisplayNone = function() {
+  return this.some(function(element) {
     if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
-                OTHelpers.css(element, 'display') === 'none') return true;
-
-    if (element.parentNode && element.parentNode.style) {
-      return OTHelpers.isDisplayNone(element.parentNode);
-    }
-
-    return false;
-  };
-
-  OTHelpers.findElementWithDisplayNone = function(element) {
-    if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
-              OTHelpers.css(element, 'display') === 'none') return element;
+                $(element).css('display') === 'none') return true;
 
     if (element.parentNode && element.parentNode.style) {
-      return OTHelpers.findElementWithDisplayNone(element.parentNode);
-    }
-
-    return null;
-  };
-
-  function objectHasProperties(obj) {
-    for (var key in obj) {
-      if (obj.hasOwnProperty(key)) return true;
-    }
-    return false;
-  }
-
-
-  // Allows an +onChange+ callback to be triggered when specific style properties
-  // of +element+ are notified. The callback accepts a single parameter, which is
-  // a hash where the keys are the style property that changed and the values are
-  // an array containing the old and new values ([oldValue, newValue]).
-  //
-  // Width and Height changes while the element is display: none will not be
-  // fired until such time as the element becomes visible again.
-  //
-  // This function returns the MutationObserver itself. Once you no longer wish
-  // to observe the element you should call disconnect on the observer.
-  //
-  // Observing changes:
-  //  // observe changings to the width and height of object
-  //  dimensionsObserver = OTHelpers.observeStyleChanges(object,
-  //                                                    ['width', 'height'], function(changeSet) {
-  //      OT.debug("The new width and height are " +
-  //                      changeSet.width[1] + ',' + changeSet.height[1]);
-  //  });
-  //
-  // Cleaning up
-  //  // stop observing changes
-  //  dimensionsObserver.disconnect();
-  //  dimensionsObserver = null;
-  //
-  OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
-    var oldStyles = {};
-
-    var getStyle = function getStyle(style) {
-      switch (style) {
-      case 'width':
-        return OTHelpers.width(element);
-
-      case 'height':
-        return OTHelpers.height(element);
-
-      default:
-        return OTHelpers.css(element);
-      }
-    };
-
-    // get the inital values
-    OTHelpers.forEach(stylesToObserve, function(style) {
-      oldStyles[style] = getStyle(style);
-    });
-
-    var observer = new MutationObserver(function(mutations) {
-      var changeSet = {};
-
-      OTHelpers.forEach(mutations, function(mutation) {
-        if (mutation.attributeName !== 'style') return;
-
-        var isHidden = OTHelpers.isDisplayNone(element);
-
-        OTHelpers.forEach(stylesToObserve, function(style) {
-          if(isHidden && (style === 'width' || style === 'height')) return;
-
-          var newValue = getStyle(style);
-
-          if (newValue !== oldStyles[style]) {
-            changeSet[style] = [oldStyles[style], newValue];
-            oldStyles[style] = newValue;
-          }
-        });
-      });
-
-      if (objectHasProperties(changeSet)) {
-        // Do this after so as to help avoid infinite loops of mutations.
-        OTHelpers.callAsync(function() {
-          onChange.call(null, changeSet);
-        });
-      }
-    });
-
-    observer.observe(element, {
-      attributes:true,
-      attributeFilter: ['style'],
-      childList:false,
-      characterData:false,
-      subtree:false
-    });
-
-    return observer;
-  };
-
-
-  // trigger the +onChange+ callback whenever
-  // 1. +element+ is removed
-  // 2. or an immediate child of +element+ is removed.
-  //
-  // This function returns the MutationObserver itself. Once you no longer wish
-  // to observe the element you should call disconnect on the observer.
-  //
-  // Observing changes:
-  //  // observe changings to the width and height of object
-  //  nodeObserver = OTHelpers.observeNodeOrChildNodeRemoval(object, function(removedNodes) {
-  //      OT.debug("Some child nodes were removed");
-  //      OTHelpers.forEach(removedNodes, function(node) {
-  //          OT.debug(node);
-  //      });
-  //  });
-  //
-  // Cleaning up
-  //  // stop observing changes
-  //  nodeObserver.disconnect();
-  //  nodeObserver = null;
-  //
-  OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
-    var observer = new MutationObserver(function(mutations) {
-      var removedNodes = [];
-
-      OTHelpers.forEach(mutations, function(mutation) {
-        if (mutation.removedNodes.length) {
-          removedNodes = removedNodes.concat(Array.prototype.slice.call(mutation.removedNodes));
-        }
-      });
-
-      if (removedNodes.length) {
-        // Do this after so as to help avoid infinite loops of mutations.
-        OTHelpers.callAsync(function() {
-          onChange(removedNodes);
-        });
-      }
-    });
-
-    observer.observe(element, {
-      attributes:false,
-      childList:true,
-      characterData:false,
-      subtree:true
-    });
-
-    return observer;
-  };
-
-})(window, window.OTHelpers);
+      return $(element.parentNode).isDisplayNone();
+    }
+  });
+};
+
+ElementCollection.prototype.findElementWithDisplayNone = function(element) {
+  return $.findElementWithDisplayNone(element);
+};
+
+
+
+OTHelpers.isElementNode = function(node) {
+  return node && typeof node === 'object' && node.nodeType === 1;
+};
+
+
+// @remove
+OTHelpers.removeElement = function(element) {
+  $(element).remove();
+};
+
+// @remove
+OTHelpers.removeElementById = function(elementId) {
+  return $('#'+elementId).remove();
+};
+
+// @remove
+OTHelpers.removeElementsByType = function(parentElem, type) {
+  return $(type, parentElem).remove();
+};
+
+// @remove
+OTHelpers.emptyElement = function(element) {
+  return $(element).empty();
+};
+
+
+
+
+
+// @remove
+OTHelpers.isDisplayNone = function(element) {
+  return $(element).isDisplayNone();
+};
+
+OTHelpers.findElementWithDisplayNone = function(element) {
+  if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
+            $.css(element, 'display') === 'none') return element;
+
+  if (element.parentNode && element.parentNode.style) {
+    return $.findElementWithDisplayNone(element.parentNode);
+  }
+
+  return null;
+};
 
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
+// tb_require('./environment.js')
 // tb_require('./dom.js')
 
-(function(window, OTHelpers, undefined) {
-
-  OTHelpers.Modal = function(options) {
-
-    OTHelpers.eventing(this, true);
-
-    var callback = arguments[arguments.length - 1];
-
-    if(!OTHelpers.isFunction(callback)) {
-      throw new Error('OTHelpers.Modal2 must be given a callback');
-    }
-
-    if(arguments.length < 2) {
-      options = {};
-    }
-
-    var domElement = document.createElement('iframe');
-
-    domElement.id = options.id || OTHelpers.uuid();
-    domElement.style.position = 'absolute';
-    domElement.style.position = 'fixed';
-    domElement.style.height = '100%';
-    domElement.style.width = '100%';
-    domElement.style.top = '0px';
-    domElement.style.left = '0px';
-    domElement.style.right = '0px';
-    domElement.style.bottom = '0px';
-    domElement.style.zIndex = 1000;
-    domElement.style.border = '0';
-
-    try {
-      domElement.style.backgroundColor = 'rgba(0,0,0,0.2)';
-    } catch (err) {
-      // Old IE browsers don't support rgba and we still want to show the upgrade message
-      // but we just make the background of the iframe completely transparent.
-      domElement.style.backgroundColor = 'transparent';
-      domElement.setAttribute('allowTransparency', 'true');
-    }
-
-    domElement.scrolling = 'no';
-    domElement.setAttribute('scrolling', 'no');
-
-    // This is necessary for IE, as it will not inherit it's doctype from
-    // the parent frame.
-    var frameContent = '<!DOCTYPE html><html><head>' +
-                      '<meta http-equiv="x-ua-compatible" content="IE=Edge">' +
-                      '<meta http-equiv="Content-type" content="text/html; charset=utf-8">' +
-                      '<title></title></head><body></body></html>';
-
-    var wrappedCallback = function() {
-      var doc = domElement.contentDocument || domElement.contentWindow.document;
-
-      if (OTHelpers.browserVersion().iframeNeedsLoad) {
-        doc.body.style.backgroundColor = 'transparent';
-        doc.body.style.border = 'none';
-
-        if (OTHelpers.browser() !== 'IE') {
-          // Skip this for IE as we use the bookmarklet workaround
-          // for THAT browser.
-          doc.open();
-          doc.write(frameContent);
-          doc.close();
-        }
-      }
-
-      callback(
-        domElement.contentWindow,
-        doc
-      );
-    };
-
-    document.body.appendChild(domElement);
-
-    if(OTHelpers.browserVersion().iframeNeedsLoad) {
-      if (OTHelpers.browser() === 'IE') {
-        // This works around some issues with IE and document.write.
-        // Basically this works by slightly abusing the bookmarklet/scriptlet
-        // functionality that all browsers support.
-        domElement.contentWindow.contents = frameContent;
-        /*jshint scripturl:true*/
-        domElement.src = 'javascript:window["contents"]';
-        /*jshint scripturl:false*/
-      }
-
-      OTHelpers.on(domElement, 'load', wrappedCallback);
-    } else {
-      setTimeout(wrappedCallback);
-    }
-
-    this.close = function() {
-      OTHelpers.removeElement(domElement);
-      this.trigger('closed');
-      this.element = domElement = null;
-      return this;
-    };
-
-    this.element = domElement;
-
-  };
-
-})(window, window.OTHelpers);
+OTHelpers.Modal = function(options) {
+
+  OTHelpers.eventing(this, true);
+
+  var callback = arguments[arguments.length - 1];
+
+  if(!OTHelpers.isFunction(callback)) {
+    throw new Error('OTHelpers.Modal2 must be given a callback');
+  }
+
+  if(arguments.length < 2) {
+    options = {};
+  }
+
+  var domElement = document.createElement('iframe');
+
+  domElement.id = options.id || OTHelpers.uuid();
+  domElement.style.position = 'absolute';
+  domElement.style.position = 'fixed';
+  domElement.style.height = '100%';
+  domElement.style.width = '100%';
+  domElement.style.top = '0px';
+  domElement.style.left = '0px';
+  domElement.style.right = '0px';
+  domElement.style.bottom = '0px';
+  domElement.style.zIndex = 1000;
+  domElement.style.border = '0';
+
+  try {
+    domElement.style.backgroundColor = 'rgba(0,0,0,0.2)';
+  } catch (err) {
+    // Old IE browsers don't support rgba and we still want to show the upgrade message
+    // but we just make the background of the iframe completely transparent.
+    domElement.style.backgroundColor = 'transparent';
+    domElement.setAttribute('allowTransparency', 'true');
+  }
+
+  domElement.scrolling = 'no';
+  domElement.setAttribute('scrolling', 'no');
+
+  // This is necessary for IE, as it will not inherit it's doctype from
+  // the parent frame.
+  var frameContent = '<!DOCTYPE html><html><head>' +
+                    '<meta http-equiv="x-ua-compatible" content="IE=Edge">' +
+                    '<meta http-equiv="Content-type" content="text/html; charset=utf-8">' +
+                    '<title></title></head><body></body></html>';
+
+  var wrappedCallback = function() {
+    var doc = domElement.contentDocument || domElement.contentWindow.document;
+
+    if (OTHelpers.env.iframeNeedsLoad) {
+      doc.body.style.backgroundColor = 'transparent';
+      doc.body.style.border = 'none';
+
+      if (OTHelpers.env.name !== 'IE') {
+        // Skip this for IE as we use the bookmarklet workaround
+        // for THAT browser.
+        doc.open();
+        doc.write(frameContent);
+        doc.close();
+      }
+    }
+
+    callback(
+      domElement.contentWindow,
+      doc
+    );
+  };
+
+  document.body.appendChild(domElement);
+
+  if(OTHelpers.env.iframeNeedsLoad) {
+    if (OTHelpers.env.name === 'IE') {
+      // This works around some issues with IE and document.write.
+      // Basically this works by slightly abusing the bookmarklet/scriptlet
+      // functionality that all browsers support.
+      domElement.contentWindow.contents = frameContent;
+      /*jshint scripturl:true*/
+      domElement.src = 'javascript:window["contents"]';
+      /*jshint scripturl:false*/
+    }
+
+    OTHelpers.on(domElement, 'load', wrappedCallback);
+  } else {
+    setTimeout(wrappedCallback, 0);
+  }
+
+  this.close = function() {
+    OTHelpers.removeElement(domElement);
+    this.trigger('closed');
+    this.element = domElement = null;
+    return this;
+  };
+
+  this.element = domElement;
+
+};
 
 /*
  * getComputedStyle from
  * https://github.com/jonathantneal/Polyfills-for-IE8/blob/master/getComputedStyle.js
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
 
 /*jshint strict: false, eqnull: true, browser:true, smarttabs:true*/
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   /*jshint eqnull: true, browser: true */
 
 
   function getPixelSize(element, style, property, fontSize) {
     var sizeWithSuffix = style[property],
         size = parseFloat(sizeWithSuffix),
         suffix = sizeWithSuffix.split(/\d/)[0],
@@ -2459,320 +3104,817 @@
         element.ownerDocument.defaultView &&
         element.ownerDocument.defaultView.getComputedStyle) {
       return element.ownerDocument.defaultView.getComputedStyle(element);
     } else {
       return getComputedStyle(element);
     }
   };
 
-})(window, window.OTHelpers);
-
-// DOM Attribute helpers helpers
-
-/*jshint browser:true, smarttabs:true*/
+})();
+
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+// tb_require('./callbacks.js')
+// tb_require('./dom.js')
+
+var observeStyleChanges = function observeStyleChanges (element, stylesToObserve, onChange) {
+  var oldStyles = {};
+
+  var getStyle = function getStyle(style) {
+    switch (style) {
+    case 'width':
+      return $(element).width();
+
+    case 'height':
+      return $(element).height();
+
+    default:
+      return $(element).css(style);
+    }
+  };
+
+  // get the inital values
+  $.forEach(stylesToObserve, function(style) {
+    oldStyles[style] = getStyle(style);
+  });
+
+  var observer = new MutationObserver(function(mutations) {
+    var changeSet = {};
+
+    $.forEach(mutations, function(mutation) {
+      if (mutation.attributeName !== 'style') return;
+
+      var isHidden = $.isDisplayNone(element);
+
+      $.forEach(stylesToObserve, function(style) {
+        if(isHidden && (style === 'width' || style === 'height')) return;
+
+        var newValue = getStyle(style);
+
+        if (newValue !== oldStyles[style]) {
+          changeSet[style] = [oldStyles[style], newValue];
+          oldStyles[style] = newValue;
+        }
+      });
+    });
+
+    if (!$.isEmpty(changeSet)) {
+      // Do this after so as to help avoid infinite loops of mutations.
+      $.callAsync(function() {
+        onChange.call(null, changeSet);
+      });
+    }
+  });
+
+  observer.observe(element, {
+    attributes:true,
+    attributeFilter: ['style'],
+    childList:false,
+    characterData:false,
+    subtree:false
+  });
+
+  return observer;
+};
+
+var observeNodeOrChildNodeRemoval = function observeNodeOrChildNodeRemoval (element, onChange) {
+  var observer = new MutationObserver(function(mutations) {
+    var removedNodes = [];
+
+    $.forEach(mutations, function(mutation) {
+      if (mutation.removedNodes.length) {
+        removedNodes = removedNodes.concat(prototypeSlice.call(mutation.removedNodes));
+      }
+    });
+
+    if (removedNodes.length) {
+      // Do this after so as to help avoid infinite loops of mutations.
+      $.callAsync(function() {
+        onChange($(removedNodes));
+      });
+    }
+  });
+
+  observer.observe(element, {
+    attributes:false,
+    childList:true,
+    characterData:false,
+    subtree:true
+  });
+
+  return observer;
+};
+
+// Allows an +onChange+ callback to be triggered when specific style properties
+// of +element+ are notified. The callback accepts a single parameter, which is
+// a hash where the keys are the style property that changed and the values are
+// an array containing the old and new values ([oldValue, newValue]).
+//
+// Width and Height changes while the element is display: none will not be
+// fired until such time as the element becomes visible again.
+//
+// This function returns the MutationObserver itself. Once you no longer wish
+// to observe the element you should call disconnect on the observer.
+//
+// Observing changes:
+//  // observe changings to the width and height of object
+//  dimensionsObserver = OTHelpers(object).observeStyleChanges(,
+//                                                    ['width', 'height'], function(changeSet) {
+//      OT.debug("The new width and height are " +
+//                      changeSet.width[1] + ',' + changeSet.height[1]);
+//  });
+//
+// Cleaning up
+//  // stop observing changes
+//  dimensionsObserver.disconnect();
+//  dimensionsObserver = null;
+//
+ElementCollection.prototype.observeStyleChanges = function(stylesToObserve, onChange) {
+  var observers = [];
+
+  this.forEach(function(element) {
+    observers.push(
+      observeStyleChanges(element, stylesToObserve, onChange)
+    );
+  });
+
+  return observers;
+};
+
+// trigger the +onChange+ callback whenever
+// 1. +element+ is removed
+// 2. or an immediate child of +element+ is removed.
+//
+// This function returns the MutationObserver itself. Once you no longer wish
+// to observe the element you should call disconnect on the observer.
+//
+// Observing changes:
+//  // observe changings to the width and height of object
+//  nodeObserver = OTHelpers(object).observeNodeOrChildNodeRemoval(function(removedNodes) {
+//      OT.debug("Some child nodes were removed");
+//      removedNodes.forEach(function(node) {
+//          OT.debug(node);
+//      });
+//  });
+//
+// Cleaning up
+//  // stop observing changes
+//  nodeObserver.disconnect();
+//  nodeObserver = null;
+//
+ElementCollection.prototype.observeNodeOrChildNodeRemoval = function(onChange) {
+  var observers = [];
+
+  this.forEach(function(element) {
+    observers.push(
+      observeNodeOrChildNodeRemoval(element, onChange)
+    );
+  });
+
+  return observers;
+};
+
+
+// @remove
+OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
+  return $(element).observeStyleChanges(stylesToObserve, onChange)[0];
+};
+
+// @remove
+OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
+  return $(element).observeNodeOrChildNodeRemoval(onChange)[0];
+};
+
+/*jshint browser:true, smarttabs:true */
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
-
-(function(window, OTHelpers, undefined) {
-
-  OTHelpers.addClass = function(element, value) {
-    // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
-    if (element.nodeType !== 1) {
-      return;
-    }
-
-    var classNames = OTHelpers.trim(value).split(/\s+/),
-        i, l;
-
-    if (OTHelpers.supportsClassList()) {
-      for (i=0, l=classNames.length; i<l; ++i) {
-        element.classList.add(classNames[i]);
-      }
-
-      return;
-    }
-
-    // Here's our fallback to browsers that don't support element.classList
-
-    if (!element.className && classNames.length === 1) {
-      element.className = value;
+// tb_require('./capabilities.js')
+
+// Returns true if the client supports element.classList
+OTHelpers.registerCapability('classList', function() {
+  return (typeof document !== 'undefined') && ('classList' in document.createElement('a'));
+});
+
+
+function hasClass (element, className) {
+  if (!className) return false;
+
+  if ($.hasCapabilities('classList')) {
+    return element.classList.contains(className);
+  }
+
+  return element.className.indexOf(className) > -1;
+}
+
+function toggleClasses (element, classNames) {
+  if (!classNames || classNames.length === 0) return;
+
+  // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
+  if (element.nodeType !== 1) {
+    return;
+  }
+
+  var numClasses = classNames.length,
+      i = 0;
+
+  if ($.hasCapabilities('classList')) {
+    for (; i<numClasses; ++i) {
+      element.classList.toggle(classNames[i]);
+    }
+
+    return;
+  }
+
+  var className = (' ' + element.className + ' ').replace(/[\s+]/, ' ');
+
+
+  for (; i<numClasses; ++i) {
+    if (hasClass(element, classNames[i])) {
+      className = className.replace(' ' + classNames[i] + ' ', ' ');
     }
     else {
-      var setClass = ' ' + element.className + ' ';
-
-      for (i=0, l=classNames.length; i<l; ++i) {
-        if ( !~setClass.indexOf( ' ' + classNames[i] + ' ')) {
-          setClass += classNames[i] + ' ';
-        }
-      }
-
-      element.className = OTHelpers.trim(setClass);
-    }
-
-    return this;
-  };
-
-  OTHelpers.removeClass = function(element, value) {
-    if (!value) return;
-
-    // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
-    if (element.nodeType !== 1) {
-      return;
-    }
-
-    var newClasses = OTHelpers.trim(value).split(/\s+/),
-        i, l;
-
-    if (OTHelpers.supportsClassList()) {
-      for (i=0, l=newClasses.length; i<l; ++i) {
-        element.classList.remove(newClasses[i]);
-      }
-
-      return;
-    }
-
-    var className = (' ' + element.className + ' ').replace(/[\s+]/, ' ');
-
-    for (i=0,l=newClasses.length; i<l; ++i) {
-      className = className.replace(' ' + newClasses[i] + ' ', ' ');
-    }
-
-    element.className = OTHelpers.trim(className);
-
-    return this;
-  };
-
+      className += classNames[i] + ' ';
+    }
+  }
+
+  element.className = $.trim(className);
+}
+
+function addClass (element, classNames) {
+  if (!classNames || classNames.length === 0) return;
+
+  // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
+  if (element.nodeType !== 1) {
+    return;
+  }
+
+  var numClasses = classNames.length,
+      i = 0;
+
+  if ($.hasCapabilities('classList')) {
+    for (; i<numClasses; ++i) {
+      element.classList.add(classNames[i]);
+    }
+
+    return;
+  }
+
+  // Here's our fallback to browsers that don't support element.classList
+
+  if (!element.className && classNames.length === 1) {
+    element.className = classNames.join(' ');
+  }
+  else {
+    var setClass = ' ' + element.className + ' ';
+
+    for (; i<numClasses; ++i) {
+      if ( !~setClass.indexOf( ' ' + classNames[i] + ' ')) {
+        setClass += classNames[i] + ' ';
+      }
+    }
+
+    element.className = $.trim(setClass);
+  }
+}
+
+function removeClass (element, classNames) {
+  if (!classNames || classNames.length === 0) return;
+
+  // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
+  if (element.nodeType !== 1) {
+    return;
+  }
+
+  var numClasses = classNames.length,
+      i = 0;
+
+  if ($.hasCapabilities('classList')) {
+    for (; i<numClasses; ++i) {
+      element.classList.remove(classNames[i]);
+    }
+
+    return;
+  }
+
+  var className = (' ' + element.className + ' ').replace(/[\s+]/, ' ');
+
+  for (; i<numClasses; ++i) {
+    className = className.replace(' ' + classNames[i] + ' ', ' ');
+  }
+
+  element.className = $.trim(className);
+}
+
+ElementCollection.prototype.addClass = function (value) {
+  if (value) {
+    var classNames = $.trim(value).split(/\s+/);
+
+    this.forEach(function(element) {
+      addClass(element, classNames);
+    });
+  }
+
+  return this;
+};
+
+ElementCollection.prototype.removeClass = function (value) {
+  if (value) {
+    var classNames = $.trim(value).split(/\s+/);
+
+    this.forEach(function(element) {
+      removeClass(element, classNames);
+    });
+  }
+
+  return this;
+};
+
+ElementCollection.prototype.toggleClass = function (value) {
+  if (value) {
+    var classNames = $.trim(value).split(/\s+/);
+
+    this.forEach(function(element) {
+      toggleClasses(element, classNames);
+    });
+  }
+
+  return this;
+};
+
+ElementCollection.prototype.hasClass = function (value) {
+  return this.some(function(element) {
+    return hasClass(element, value);
+  });
+};
+
+
+// @remove
+OTHelpers.addClass = function(element, className) {
+  return $(element).addClass(className);
+};
+
+// @remove
+OTHelpers.removeClass = function(element, value) {
+  return $(element).removeClass(value);
+};
+
+
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+// tb_require('./dom.js')
+// tb_require('./capabilities.js')
+
+
+// Gets or sets the attribute called +name+ for the first element in the collection
+ElementCollection.prototype.attr = function (name, value) {
+  if (OTHelpers.isObject(name)) {
+    for (var key in name) {
+      this.first.setAttribute(key, name[key]);
+    }
+  }
+  else if (value === void 0) {
+    return this.first.getAttribute(name);
+  }
+  else {
+    this.first.setAttribute(name, value);
+  }
+
+  return this;
+};
+
+// Gets, and optionally sets, the html body of the first element
+// in the collection. If the +html+ is provided then the first
+// element's html body will be replaced with it.
+//
+ElementCollection.prototype.html = function (html) {
+  if (html !== void 0) {
+    this.first.innerHTML = html;
+  }
+
+  return this.first.innerHTML;
+};
+
+
+// Centers +element+ within the window. You can pass through the width and height
+// if you know it, if you don't they will be calculated for you.
+ElementCollection.prototype.center = function (width, height) {
+  var $element;
+
+  this.forEach(function(element) {
+    $element = $(element);
+    if (!width) width = parseInt($element.width(), 10);
+    if (!height) height = parseInt($element.height(), 10);
+
+    var marginLeft = -0.5 * width + 'px';
+    var marginTop = -0.5 * height + 'px';
+
+    $element.css('margin', marginTop + ' 0 0 ' + marginLeft)
+            .addClass('OT_centered');
+  });
+
+  return this;
+};
+
+
+// @remove
+// Centers +element+ within the window. You can pass through the width and height
+// if you know it, if you don't they will be calculated for you.
+OTHelpers.centerElement = function(element, width, height) {
+  return $(element).center(width, height);
+};
 
   /**
    * Methods to calculate element widths and heights.
    */
+(function() {
 
   var _width = function(element) {
         if (element.offsetWidth > 0) {
           return element.offsetWidth + 'px';
         }
 
-        return OTHelpers.css(element, 'width');
+        return $(element).css('width');
       },
 
       _height = function(element) {
         if (element.offsetHeight > 0) {
           return element.offsetHeight + 'px';
         }
 
-        return OTHelpers.css(element, 'height');
+        return $(element).css('height');
       };
 
-  OTHelpers.width = function(element, newWidth) {
+  ElementCollection.prototype.width = function (newWidth) {
     if (newWidth) {
-      OTHelpers.css(element, 'width', newWidth);
+      this.css('width', newWidth);
+      return this;
+    }
+    else {
+      if (this.isDisplayNone()) {
+        return this.makeVisibleAndYield(function(element) {
+          return _width(element);
+        })[0];
+      }
+      else {
+        return _width(this.get(0));
+      }
+    }
+  };
+
+  ElementCollection.prototype.height = function (newHeight) {
+    if (newHeight) {
+      this.css('height', newHeight);
       return this;
     }
     else {
-      if (OTHelpers.isDisplayNone(element)) {
-        // We can't get the width, probably since the element is hidden.
-        return OTHelpers.makeVisibleAndYield(element, function() {
-          return _width(element);
-        });
+      if (this.isDisplayNone()) {
+        // We can't get the height, probably since the element is hidden.
+        return this.makeVisibleAndYield(function(element) {
+          return _height(element);
+        })[0];
       }
       else {
-        return _width(element);
-      }
-    }
-  };
-
+        return _height(this.get(0));
+      }
+    }
+  };
+
+  // @remove
+  OTHelpers.width = function(element, newWidth) {
+    var ret = $(element).width(newWidth);
+    return newWidth ? OTHelpers : ret;
+  };
+
+  // @remove
   OTHelpers.height = function(element, newHeight) {
-    if (newHeight) {
-      OTHelpers.css(element, 'height', newHeight);
-      return this;
-    }
-    else {
-      if (OTHelpers.isDisplayNone(element)) {
-        // We can't get the height, probably since the element is hidden.
-        return OTHelpers.makeVisibleAndYield(element, function() {
-          return _height(element);
-        });
-      }
-      else {
-        return _height(element);
-      }
-    }
-  };
-
-  // Centers +element+ within the window. You can pass through the width and height
-  // if you know it, if you don't they will be calculated for you.
-  OTHelpers.centerElement = function(element, width, height) {
-    if (!width) width = parseInt(OTHelpers.width(element), 10);
-    if (!height) height = parseInt(OTHelpers.height(element), 10);
-
-    var marginLeft = -0.5 * width + 'px';
-    var marginTop = -0.5 * height + 'px';
-    OTHelpers.css(element, 'margin', marginTop + ' 0 0 ' + marginLeft);
-    OTHelpers.addClass(element, 'OT_centered');
-  };
-
-})(window, window.OTHelpers);
+    var ret = $(element).height(newHeight);
+    return newHeight ? OTHelpers : ret;
+  };
+
+})();
+
 
 // CSS helpers helpers
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 // tb_require('./dom.js')
 // tb_require('./getcomputedstyle.js')
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   var displayStateCache = {},
       defaultDisplays = {};
 
-  var defaultDisplayValueForElement = function(element) {
+  var defaultDisplayValueForElement = function (element) {
     if (defaultDisplays[element.ownerDocument] &&
       defaultDisplays[element.ownerDocument][element.nodeName]) {
       return defaultDisplays[element.ownerDocument][element.nodeName];
     }
 
     if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
 
     // We need to know what display value to use for this node. The easiest way
     // is to actually create a node and read it out.
     var testNode = element.ownerDocument.createElement(element.nodeName),
         defaultDisplay;
 
     element.ownerDocument.body.appendChild(testNode);
     defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] =
-      OTHelpers.css(testNode, 'display');
-
-    OTHelpers.removeElement(testNode);
+    $(testNode).css('display');
+
+    $(testNode).remove();
     testNode = null;
 
     return defaultDisplay;
   };
 
-  var isHidden = function(element) {
-    var computedStyle = OTHelpers.getComputedStyle(element);
+  var isHidden = function (element) {
+    var computedStyle = $.getComputedStyle(element);
     return computedStyle.getPropertyValue('display') === 'none';
   };
 
-  OTHelpers.show = function(element) {
-    var display = element.style.display;
-
-    if (display === '' || display === 'none') {
-      element.style.display = displayStateCache[element] || '';
-      delete displayStateCache[element];
-    }
-
-    if (isHidden(element)) {
-      // It's still hidden so there's probably a stylesheet that declares this
-      // element as display:none;
-      displayStateCache[element] = 'none';
-
-      element.style.display = defaultDisplayValueForElement(element);
-    }
-
-    return this;
-  };
-
-  OTHelpers.hide = function(element) {
-    if (element.style.display === 'none') return;
-
-    displayStateCache[element] = element.style.display;
-    element.style.display = 'none';
-
-    return this;
-  };
-
-  OTHelpers.css = function(element, nameOrHash, value) {
-    if (typeof(nameOrHash) !== 'string') {
-      var style = element.style;
-
-      for (var cssName in nameOrHash) {
-        if (nameOrHash.hasOwnProperty(cssName)) {
-          style[cssName] = nameOrHash[cssName];
-        }
-      }
-
-      return this;
-
-    } else if (value !== undefined) {
-      element.style[nameOrHash] = value;
-      return this;
-
-    } else {
-      // Normalise vendor prefixes from the form MozTranform to -moz-transform
-      // except for ms extensions, which are weird...
-
-      var name = nameOrHash.replace( /([A-Z]|^ms)/g, '-$1' ).toLowerCase(),
-          computedStyle = OTHelpers.getComputedStyle(element),
-          currentValue = computedStyle.getPropertyValue(name);
-
-      if (currentValue === '') {
-        currentValue = element.style[name];
-      }
-
-      return currentValue;
-    }
-  };
-
-
-// Apply +styles+ to +element+ while executing +callback+, restoring the previous
-// styles after the callback executes.
-  OTHelpers.applyCSS = function(element, styles, callback) {
+  var setCssProperties = function (element, hash) {
+    var style = element.style;
+
+    for (var cssName in hash) {
+      if (hash.hasOwnProperty(cssName)) {
+        style[cssName] = hash[cssName];
+      }
+    }
+  };
+
+  var setCssProperty = function (element, name, value) {
+    element.style[name] = value;
+  };
+
+  var getCssProperty = function (element, unnormalisedName) {
+    // Normalise vendor prefixes from the form MozTranform to -moz-transform
+    // except for ms extensions, which are weird...
+
+    var name = unnormalisedName.replace( /([A-Z]|^ms)/g, '-$1' ).toLowerCase(),
+        computedStyle = $.getComputedStyle(element),
+        currentValue = computedStyle.getPropertyValue(name);
+
+    if (currentValue === '') {
+      currentValue = element.style[name];
+    }
+
+    return currentValue;
+  };
+
+  var applyCSS = function(element, styles, callback) {
     var oldStyles = {},
         name,
         ret;
 
     // Backup the old styles
     for (name in styles) {
       if (styles.hasOwnProperty(name)) {
         // We intentionally read out of style here, instead of using the css
         // helper. This is because the css helper uses querySelector and we
         // only want to pull values out of the style (domeElement.style) hash.
         oldStyles[name] = element.style[name];
 
-        OTHelpers.css(element, name, styles[name]);
-      }
-    }
-
-    ret = callback();
+        $(element).css(name, styles[name]);
+      }
+    }
+
+    ret = callback(element);
 
     // Restore the old styles
     for (name in styles) {
       if (styles.hasOwnProperty(name)) {
-        OTHelpers.css(element, name, oldStyles[name] || '');
+        $(element).css(name, oldStyles[name] || '');
       }
     }
 
     return ret;
   };
 
+  ElementCollection.prototype.show = function() {
+    return this.forEach(function(element) {
+      var display = element.style.display;
+
+      if (display === '' || display === 'none') {
+        element.style.display = displayStateCache[element] || '';
+        delete displayStateCache[element];
+      }
+
+      if (isHidden(element)) {
+        // It's still hidden so there's probably a stylesheet that declares this
+        // element as display:none;
+        displayStateCache[element] = 'none';
+
+        element.style.display = defaultDisplayValueForElement(element);
+      }
+    });
+  };
+
+  ElementCollection.prototype.hide = function() {
+    return this.forEach(function(element) {
+      if (element.style.display === 'none') return;
+
+      displayStateCache[element] = element.style.display;
+      element.style.display = 'none';
+    });
+  };
+
+  ElementCollection.prototype.css = function(nameOrHash, value) {
+    if (this.length === 0) return;
+
+    if (typeof(nameOrHash) !== 'string') {
+
+      return this.forEach(function(element) {
+        setCssProperties(element, nameOrHash);
+      });
+
+    } else if (value !== undefined) {
+
+      return this.forEach(function(element) {
+        setCssProperty(element, nameOrHash, value);
+      });
+
+    } else {
+      return getCssProperty(this.first, nameOrHash, value);
+    }
+  };
+
+  // Apply +styles+ to +element+ while executing +callback+, restoring the previous
+  // styles after the callback executes.
+  ElementCollection.prototype.applyCSS = function (styles, callback) {
+    var results = [];
+
+    this.forEach(function(element) {
+      results.push(applyCSS(element, styles, callback));
+    });
+
+    return results;
+  };
+
+
   // Make +element+ visible while executing +callback+.
+  ElementCollection.prototype.makeVisibleAndYield = function (callback) {
+    var hiddenVisually = {
+        display: 'block',
+        visibility: 'hidden'
+      },
+      results = [];
+
+    this.forEach(function(element) {
+      // find whether it's the element or an ancestor that's display none and
+      // then apply to whichever it is
+      var targetElement = $.findElementWithDisplayNone(element);
+      if (!targetElement) {
+        results.push(void 0);
+      }
+      else {
+        results.push(
+          applyCSS(targetElement, hiddenVisually, callback)
+        );
+      }
+    });
+
+    return results;
+  };
+
+
+  // @remove
+  OTHelpers.show = function(element) {
+    return $(element).show();
+  };
+
+  // @remove
+  OTHelpers.hide = function(element) {
+    return $(element).hide();
+  };
+
+  // @remove
+  OTHelpers.css = function(element, nameOrHash, value) {
+    return $(element).css(nameOrHash, value);
+  };
+
+  // @remove
+  OTHelpers.applyCSS = function(element, styles, callback) {
+    return $(element).applyCSS(styles, callback);
+  };
+
+  // @remove
   OTHelpers.makeVisibleAndYield = function(element, callback) {
-    // find whether it's the element or an ancester that's display none and
-    // then apply to whichever it is
-    var targetElement = OTHelpers.findElementWithDisplayNone(element);
-    if (!targetElement) return;
-
-    return OTHelpers.applyCSS(targetElement, {
-      display: 'block',
-      visibility: 'hidden'
-    }, callback);
-  };
-
-})(window, window.OTHelpers);
-
-// AJAX helpers
+    return $(element).makeVisibleAndYield(callback);
+  };
+
+})();
+
+// tb_require('../helpers.js')
+
+/**@licence
+ * Copyright (c) 2010 Caolan McMahon
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ **/
+
+
+(function() {
+
+  OTHelpers.setImmediate = (function() {
+    if (typeof process === 'undefined' || !(process.nextTick)) {
+      if (typeof setImmediate === 'function') {
+        return function (fn) {
+          // not a direct alias for IE10 compatibility
+          setImmediate(fn);
+        };
+      }
+      return function (fn) {
+        setTimeout(fn, 0);
+      };
+    }
+    if (typeof setImmediate !== 'undefined') {
+      return setImmediate;
+    }
+    return process.nextTick;
+  })();
+
+  OTHelpers.iterator = function(tasks) {
+    var makeCallback = function (index) {
+      var fn = function () {
+        if (tasks.length) {
+          tasks[index].apply(null, arguments);
+        }
+        return fn.next();
+      };
+      fn.next = function () {
+        return (index < tasks.length - 1) ? makeCallback(index + 1) : null;
+      };
+      return fn;
+    };
+    return makeCallback(0);
+  };
+
+  OTHelpers.waterfall = function(array, done) {
+    done = done || function () {};
+    if (array.constructor !== Array) {
+      return done(new Error('First argument to waterfall must be an array of functions'));
+    }
+
+    if (!array.length) {
+      return done();
+    }
+
+    var next = function(iterator) {
+      return function (err) {
+        if (err) {
+          done.apply(null, arguments);
+          done = function () {};
+        } else {
+          var args = prototypeSlice.call(arguments, 1),
+              nextFn = iterator.next();
+          if (nextFn) {
+            args.push(next(nextFn));
+          } else {
+            args.push(done);
+          }
+          OTHelpers.setImmediate(function() {
+            iterator.apply(null, args);
+          });
+        }
+      };
+    };
+
+    next(OTHelpers.iterator(array))();
+  };
+
+})();
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
 
-(function(window, OTHelpers, undefined) {
+(function() {
 
   var requestAnimationFrame = window.requestAnimationFrame ||
                               window.mozRequestAnimationFrame ||
                               window.webkitRequestAnimationFrame ||
                               window.msRequestAnimationFrame;
 
   if (requestAnimationFrame) {
     requestAnimationFrame = OTHelpers.bind(requestAnimationFrame, window);
@@ -2786,916 +3928,291 @@
       var timeToCall = Math.max(0, 16 - (currTime - lastTime));
       var id = window.setTimeout(function() { callback(currTime - startTime); }, timeToCall);
       lastTime = currTime + timeToCall;
       return id;
     };
   }
 
   OTHelpers.requestAnimationFrame = requestAnimationFrame;
-})(window, window.OTHelpers);
+})();
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+(function() {
+
+  // Singleton interval
+  var logQueue = [],
+      queueRunning = false;
+
+  OTHelpers.Analytics = function(loggingUrl, debugFn) {
+
+    var endPoint = loggingUrl + '/logging/ClientEvent',
+        endPointQos = loggingUrl + '/logging/ClientQos',
+
+        reportedErrors = {},
+
+        send = function(data, isQos, callback) {
+          OTHelpers.post((isQos ? endPointQos : endPoint) + '?_=' + OTHelpers.uuid.v4(), {
+            body: data,
+            xdomainrequest: ($.env.name === 'IE' && $.env.version < 10),
+            headers: {
+              'Content-Type': 'application/json'
+            }
+          }, callback);
+        },
+
+        throttledPost = function() {
+          // Throttle logs so that they only happen 1 at a time
+          if (!queueRunning && logQueue.length > 0) {
+            queueRunning = true;
+            var curr = logQueue[0];
+
+            // Remove the current item and send the next log
+            var processNextItem = function() {
+              logQueue.shift();
+              queueRunning = false;
+              throttledPost();
+            };
+
+            if (curr) {
+              send(curr.data, curr.isQos, function(err) {
+                if (err) {
+                  var debugMsg = 'Failed to send ClientEvent, moving on to the next item.';
+                  if (debugFn) {
+                    debugFn(debugMsg);
+                  } else {
+                    console.log(debugMsg);
+                  }
+                  // There was an error, move onto the next item
+                } else {
+                  curr.onComplete();
+                }
+                setTimeout(processNextItem, 50);
+              });
+            }
+          }
+        },
+
+        post = function(data, onComplete, isQos) {
+          logQueue.push({
+            data: data,
+            onComplete: onComplete,
+            isQos: isQos
+          });
+
+          throttledPost();
+        },
+
+        shouldThrottleError = function(code, type, partnerId) {
+          if (!partnerId) return false;
+
+          var errKey = [partnerId, type, code].join('_'),
+          //msgLimit = DynamicConfig.get('exceptionLogging', 'messageLimitPerPartner', partnerId);
+            msgLimit = 100;
+          if (msgLimit === null || msgLimit === undefined) return false;
+          return (reportedErrors[errKey] || 0) <= msgLimit;
+        };
+
+    // Log an error via ClientEvents.
+    //
+    // @param [String] code
+    // @param [String] type
+    // @param [String] message
+    // @param [Hash] details additional error details
+    //
+    // @param [Hash] options the options to log the client event with.
+    // @option options [String] action The name of the Event that we are logging. E.g.
+    //  'TokShowLoaded'. Required.
+    // @option options [String] variation Usually used for Split A/B testing, when you
+    //  have multiple variations of the +_action+.
+    // @option options [String] payload The payload. Required.
+    // @option options [String] sessionId The active OpenTok session, if there is one
+    // @option options [String] connectionId The active OpenTok connectionId, if there is one
+    // @option options [String] partnerId
+    // @option options [String] guid ...
+    // @option options [String] streamId ...
+    // @option options [String] section ...
+    // @option options [String] clientVersion ...
+    //
+    // Reports will be throttled to X reports (see exceptionLogging.messageLimitPerPartner
+    // from the dynamic config for X) of each error type for each partner. Reports can be
+    // disabled/enabled globally or on a per partner basis (per partner settings
+    // take precedence) using exceptionLogging.enabled.
+    //
+    this.logError = function(code, type, message, details, options) {
+      if (!options) options = {};
+      var partnerId = options.partnerId;
+
+      if (shouldThrottleError(code, type, partnerId)) {
+        //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' +
+        // code + ' for partner ' + (partnerId || 'No Partner Id'));
+        return;
+      }
+
+      var errKey = [partnerId, type, code].join('_'),
+      payload =  details ? details : null;
+
+      reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ?
+        reportedErrors[errKey] + 1 : 1;
+      this.logEvent(OTHelpers.extend(options, {
+        action: type + '.' + code,
+        payload: payload
+      }), false);
+    };
+
+    // Log a client event to the analytics backend.
+    //
+    // @example Logs a client event called 'foo'
+    //  this.logEvent({
+    //      action: 'foo',
+    //      payload: 'bar',
+    //      sessionId: sessionId,
+    //      connectionId: connectionId
+    //  }, false)
+    //
+    // @param [Hash] data the data to log the client event with.
+    // @param [Boolean] qos Whether this is a QoS event.
+    //
+    this.logEvent = function(data, qos, throttle) {
+      if (!qos) qos = false;
+
+      if (throttle && !isNaN(throttle)) {
+        if (Math.random() > throttle) {
+          return;
+        }
+      }
+
+      // remove properties that have null values:
+      for (var key in data) {
+        if (data.hasOwnProperty(key) && data[key] === null) {
+          delete data[key];
+        }
+      }
+
+      // TODO: catch error when stringifying an object that has a circular reference
+      data = JSON.stringify(data);
+
+      var onComplete = function() {
+        //  OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation']
+        //  + ', payload: ' + data['payload'] + '}');
+      };
+
+      post(data, onComplete, qos);
+    };
+
+    // Log a client QOS to the analytics backend.
+    // Log a client QOS to the analytics backend.
+    // @option options [String] action The name of the Event that we are logging.
+    //  E.g. 'TokShowLoaded'. Required.
+    // @option options [String] variation Usually used for Split A/B testing, when
+    //  you have multiple variations of the +_action+.
+    // @option options [String] payload The payload. Required.
+    // @option options [String] sessionId The active OpenTok session, if there is one
+    // @option options [String] connectionId The active OpenTok connectionId, if there is one
+    // @option options [String] partnerId
+    // @option options [String] guid ...
+    // @option options [String] streamId ...
+    // @option options [String] section ...
+    // @option options [String] clientVersion ...
+    //
+    this.logQOS = function(options) {
+      this.logEvent(options, true);
+    };
+  };
+
+})();
+
 // AJAX helpers
 
 /*jshint browser:true, smarttabs:true*/
 
 // tb_require('../helpers.js')
-
-(function(window, OTHelpers, undefined) {
-
-  function formatPostData(data) { //, contentType
-    // If it's a string, we assume it's properly encoded
-    if (typeof(data) === 'string') return data;
-
-    var queryString = [];
-
-    for (var key in data) {
-      queryString.push(
-        encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
-      );
-    }
-
-    return queryString.join('&').replace(/\+/g, '%20');
-  }
-
-  OTHelpers.getJSON = function(url, options, callback) {
-    options = options || {};
-
-    var done = function(error, event) {
-      if(error) {
-        callback(error, event && event.target && event.target.responseText);
-      } else {
-        var response;
-
-        try {
-          response = JSON.parse(event.target.responseText);
-        } catch(e) {
-          // Badly formed JSON
-          callback(e, event && event.target && event.target.responseText);
-          return;
-        }
-
-        callback(null, response, event);
-      }
-    };
-
-    if(options.xdomainrequest) {
-      OTHelpers.xdomainRequest(url, { method: 'GET' }, done);
-    } else {
-      var extendedHeaders = OTHelpers.extend({
-        'Accept': 'application/json'
-      }, options.headers || {});
-
-      OTHelpers.get(url, OTHelpers.extend(options || {}, {
-        headers: extendedHeaders
-      }), done);
-    }
-
-  };
-
-  OTHelpers.xdomainRequest = function(url, options, callback) {
-    /*global XDomainRequest*/
-    var xdr = new XDomainRequest(),
-        _options = options || {},
-        _method = _options.method;
-
-    if(!_method) {
-      callback(new Error('No HTTP method specified in options'));
-      return;
-    }
-
-    _method = _method.toUpperCase();
-
-    if(!(_method === 'GET' || _method === 'POST')) {
-      callback(new Error('HTTP method can only be '));
-      return;
-    }
-
-    function done(err, event) {
-      xdr.onload = xdr.onerror = xdr.ontimeout = function() {};
-      xdr = void 0;
-      callback(err, event);
-    }
-
-
-    xdr.onload = function() {
-      done(null, {
-        target: {
-          responseText: xdr.responseText,
-          headers: {
-            'content-type': xdr.contentType
-          }
-        }
-      });
-    };
-
-    xdr.onerror = function() {
-      done(new Error('XDomainRequest of ' + url + ' failed'));
-    };
-
-    xdr.ontimeout = function() {
-      done(new Error('XDomainRequest of ' + url + ' timed out'));
-    };
-
-    xdr.open(_method, url);
-    xdr.send(options.body && formatPostData(options.body));
-
-  };
-
-  OTHelpers.request = function(url, options, callback) {
-    var request = new XMLHttpRequest(),
-        _options = options || {},
-        _method = _options.method;
-
-    if(!_method) {
-      callback(new Error('No HTTP method specified in options'));
-      return;
-    }
-
-    // Setup callbacks to correctly respond to success and error callbacks. This includes
-    // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore
-    // by default.
-    if(callback) {
-      OTHelpers.on(request, 'load', function(event) {
-        var status = event.target.status;
-
-        // We need to detect things that XMLHttpRequest considers a success,
-        // but we consider to be failures.
-        if ( status >= 200 && status < 300 || status === 304 ) {
-          callback(null, event);
-        } else {
-          callback(event);
-        }
-      });
-
-      OTHelpers.on(request, 'error', callback);
-    }
-
-    request.open(options.method, url, true);
-
-    if (!_options.headers) _options.headers = {};
-
-    for (var name in _options.headers) {
-      request.setRequestHeader(name, _options.headers[name]);
-    }
-
-    request.send(options.body && formatPostData(options.body));
-  };
-
-  OTHelpers.get = function(url, options, callback) {
-    var _options = OTHelpers.extend(options || {}, {
-      method: 'GET'
-    });
+// tb_require('./ajax/node.js')
+// tb_require('./ajax/browser.js')
+
+OTHelpers.get = function(url, options, callback) {
+  var _options = OTHelpers.extend(options || {}, {
+    method: 'GET'
+  });
+  OTHelpers.request(url, _options, callback);
+};
+
+
+OTHelpers.post = function(url, options, callback) {
+  var _options = OTHelpers.extend(options || {}, {
+    method: 'POST'
+  });
+
+  if(_options.xdomainrequest) {
+    OTHelpers.xdomainRequest(url, _options, callback);
+  } else {
     OTHelpers.request(url, _options, callback);
-  };
-
-  OTHelpers.post = function(url, options, callback) {
-    var _options = OTHelpers.extend(options || {}, {
-      method: 'POST'
-    });
-
-    if(_options.xdomainrequest) {
-      OTHelpers.xdomainRequest(url, _options, callback);
-    } else {
-      OTHelpers.request(url, _options, callback);
-    }
-  };
+  }
+};
+
 
 })(window, window.OTHelpers);
-!(function(window) {
-
-  /* global OTHelpers */
-
-  if (!window.OT) window.OT = {};
-
-  // Bring OTHelpers in as OT.$
-  OT.$ = OTHelpers.noConflict();
-
-  // Allow events to be bound on OT
-  OT.$.eventing(OT);
-
-  // REMOVE THIS POST IE MERGE
-
-  OT.$.defineGetters = function(self, getters, enumerable) {
-    var propsDefinition = {};
-
-    if (enumerable === void 0) enumerable = false;
-
-    for (var key in getters) {
-      if(!getters.hasOwnProperty(key)) {
-        continue;
-      }
-      propsDefinition[key] = {
-        get: getters[key],
-        enumerable: enumerable
-      };
-    }
-
-    Object.defineProperties(self, propsDefinition);
-  };
-
-  // STOP REMOVING HERE
-
-  // OT.$.Modal was OT.Modal before the great common-js-helpers move
-  OT.Modal = OT.$.Modal;
-
-  // Add logging methods
-  OT.$.useLogHelpers(OT);
-
-  var _debugHeaderLogged = false,
-      _setLogLevel = OT.setLogLevel;
-
-  // On the first time log level is set to DEBUG (or higher) show version info.
-  OT.setLogLevel = function(level) {
-    // Set OT.$ to the same log level
-    OT.$.setLogLevel(level);
-    var retVal = _setLogLevel.call(OT, level);
-    if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) {
-      OT.debug('OpenTok JavaScript library ' + OT.properties.version);
-      OT.debug('Release notes: ' + OT.properties.websiteURL +
-        '/opentok/webrtc/docs/js/release-notes.html');
-      OT.debug('Known issues: ' + OT.properties.websiteURL +
-        '/opentok/webrtc/docs/js/release-notes.html#knownIssues');
-      _debugHeaderLogged = true;
-    }
-    OT.debug('OT.setLogLevel(' + retVal + ')');
-    return retVal;
-  };
-
-  var debugTrue = OT.properties.debug === 'true' || OT.properties.debug === true;
-  OT.setLogLevel(debugTrue ? OT.DEBUG : OT.ERROR);
-
-  OT.$.userAgent = function() {
-    var userAgent = navigator.userAgent;
-    if (TBPlugin.isInstalled()) userAgent += '; TBPlugin ' + TBPlugin.version();
-    return userAgent;
-  };
-
-  /**
-  * Sets the API log level.
-  * <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() {
-
-  var adjustModal = function(callback) {
-    return function setFullHeightDocument(window, document) {
-      // required in IE8
-      document.querySelector('html').style.height = document.body.style.height = '100%';
-      callback(window, document);
-    };
-  };
-
-  var addCss = function(document, url, callback) {
-    var head = document.head || document.getElementsByTagName('head')[0];
-    var cssTag = OT.$.createElement('link', {
-      type: 'text/css',
-      media: 'screen',
-      rel: 'stylesheet',
-      href: url
-    });
-    head.appendChild(cssTag);
-    OT.$.on(cssTag, 'error', function(error) {
-      OT.error('Could not load CSS for dialog', url, error && error.message || error);
-    });
-    OT.$.on(cssTag, 'load', callback);
-  };
-
-  var addDialogCSS = function(document, urls, callback) {
-    var allURLs = [
-      '//fonts.googleapis.com/css?family=Didact+Gothic',
-      OT.properties.cssURL
-    ].concat(urls);
-    var remainingStylesheets = allURLs.length;
-    OT.$.forEach(allURLs, function(stylesheetUrl) {
-      addCss(document, stylesheetUrl, function() {
-        if(--remainingStylesheets <= 0) {
-          callback();
-        }
-      });
-    });
-
-  };
-
-  var templateElement = function(classes, children, tagName) {
-    var el = OT.$.createElement(tagName || 'div', { 'class': classes }, children, this);
-    el.on = OT.$.bind(OT.$.on, OT.$, el);
-    el.off = OT.$.bind(OT.$.off, OT.$, el);
-    return el;
-  };
-
-  var checkBoxElement = function (classes, nameAndId, onChange) {
-    var checkbox = templateElement.call(this, '', null, 'input').on('change', onChange);
-
-    if (OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 8) {
-      // Fix for IE8 not triggering the change event
-      checkbox.on('click', function() {
-        checkbox.blur();
-        checkbox.focus();
-      });
-    }
-
-    checkbox.setAttribute('name', nameAndId);
-    checkbox.setAttribute('id', nameAndId);
-    checkbox.setAttribute('type', 'checkbox');
-
-    return checkbox;
-  };
-
-  var linkElement = function(children, href, classes) {
-    var link = templateElement.call(this, classes || '', children, 'a');
-    link.setAttribute('href', href);
-    return link;
-  };
-
-  OT.Dialogs = {};
-
-  OT.Dialogs.Plugin = {};
-
-  OT.Dialogs.Plugin.promptToInstall = function() {
-    var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
-      var el = OT.$.bind(templateElement, document),
-          btn = function(children, size) {
-            var classes = 'OT_dialog-button ' +
-                          (size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large'),
-                b = el(classes, children);
-
-            b.enable = function() {
-              OT.$.removeClass(this, 'OT_dialog-button-disabled');
-              return this;
-            };
-
-            b.disable = function() {
-              OT.$.addClass(this, 'OT_dialog-button-disabled');
-              return this;
-            };
-
-            return b;
-          },
-          downloadButton = btn('Download plugin'),
-          cancelButton = btn('cancel', 'small'),
-          refreshButton = btn('Refresh browser'),
-          acceptEULA,
-          checkbox,
-          close,
-          root;
-
-      OT.$.addClass(cancelButton, 'OT_dialog-no-natural-margin OT_dialog-button-block');
-      OT.$.addClass(refreshButton, 'OT_dialog-no-natural-margin');
-
-      function onDownload() {
-        modal.trigger('download');
-        setTimeout(function() {
-          root.querySelector('.OT_dialog-messages-main').innerHTML =
-                                              'Plugin installation successful';
-          var sections = root.querySelectorAll('.OT_dialog-section');
-          OT.$.addClass(sections[0], 'OT_dialog-hidden');
-          OT.$.removeClass(sections[1], 'OT_dialog-hidden');
-        }, 3000);
-      }
-
-      function onRefresh() {
-        modal.trigger('refresh');
-      }
-
-      function onToggleEULA() {
-        if (checkbox.checked) {
-          enableButtons();
-        }
-        else {
-          disableButtons();
-        }
-      }
-
-      function enableButtons() {
-        downloadButton.enable();
-        downloadButton.on('click', onDownload);
-
-        refreshButton.enable();
-        refreshButton.on('click', onRefresh);
-      }
-
-      function disableButtons() {
-        downloadButton.disable();
-        downloadButton.off('click', onDownload);
-
-        refreshButton.disable();
-        refreshButton.off('click', onRefresh);
-      }
-
-      downloadButton.disable();
-      refreshButton.disable();
-
-      cancelButton.on('click', function() {
-        modal.trigger('cancelButtonClicked');
-        modal.close();
-      });
-
-      close = el('OT_closeButton', '&times;')
-        .on('click', function() {
-          modal.trigger('closeButtonClicked');
-          modal.close();
-        });
-
-      acceptEULA = linkElement.call(document,
-                                    'end-user license agreement',
-                                    'http://tokbox.com/support/ie-eula');
-
-      checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA);
-
-      root = el('OT_dialog-centering', [
-        el('OT_dialog-centering-child', [
-          el('OT_root OT_dialog OT_dialog-plugin-prompt', [
-            close,
-            el('OT_dialog-messages', [
-              el('OT_dialog-messages-main', 'This app requires real-time communication')
-            ]),
-            el('OT_dialog-section', [
-              el('OT_dialog-single-button-with-title', [
-                el('OT_dialog-button-title', [
-                  checkbox,
-                  (function() {
-                    var x = el('', 'accept', 'label');
-                    x.setAttribute('for', checkbox.id);
-                    x.style.margin = '0 5px';
-                    return x;
-                  })(),
-                  acceptEULA
-                ]),
-                el('OT_dialog-actions-card', [
-                  downloadButton,
-                  cancelButton
-                ])
-              ])
-            ]),
-            el('OT_dialog-section OT_dialog-hidden', [
-              el('OT_dialog-button-title', [
-                'You can now enjoy webRTC enabled video via Internet Explorer.'
-              ]),
-              refreshButton
-            ])
-          ])
-        ])
-      ]);
-
-      addDialogCSS(document, [], function() {
-        document.body.appendChild(root);
-      });
-
-    }));
-    return modal;
-  };
-
-  OT.Dialogs.Plugin.promptToReinstall = function() {
-    var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
-      var el = OT.$.bind(templateElement, document),
-          close,
-          okayButton,
-          root;
-
-      close = el('OT_closeButton', '&times;');
-      okayButton =
-        el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Okay');
-
-      OT.$.on(okayButton, 'click', function() {
-        modal.trigger('okay');
-      });
-
-      OT.$.on(close, 'click', function() {
-        modal.trigger('closeButtonClicked');
-        modal.close();
-      });
-
-      root = el('OT_dialog-centering', [
-        el('OT_dialog-centering-child', [
-          el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [
-            close,
-            el('OT_dialog-messages', [
-              el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'),
-              el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin ' +
-                'again to enable real-time video communication for Internet Explorer.')
-            ]),
-            el('OT_dialog-section', [
-              el('OT_dialog-single-button', okayButton)
-            ])
-          ])
-        ])
-      ]);
-
-      addDialogCSS(document, [], function() {
-        document.body.appendChild(root);
-      });
-
-    }));
-
-    return modal;
-  };
-
-  OT.Dialogs.Plugin.updateInProgress = function() {
-
-    var progressBar,
-        progressText,
-        progressValue = 0;
-
-    var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
-      var el = OT.$.bind(templateElement, document),
-          root;
-
-      progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong');
-
-      progressBar = el('OT_dialog-progress-bar-fill');
-
-      root = el('OT_dialog-centering', [
-        el('OT_dialog-centering-child', [
-          el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [
-            el('OT_dialog-messages', [
-              el('OT_dialog-messages-main', [
-                'One moment please... ',
-                progressText
-              ]),
-              el('OT_dialog-progress-bar', progressBar),
-              el('OT_dialog-messages-minor OT_dialog-no-natural-margin',
-                'Please wait while the OpenTok plugin is updated')
-            ])
-          ])
-        ])
-      ]);
-
-      addDialogCSS(document, [], function() {
-        document.body.appendChild(root);
-        if(progressValue != null) {
-          modal.setUpdateProgress(progressValue);
-        }
-      });
-    }));
-
-    modal.setUpdateProgress = function(newProgress) {
-      if(progressBar && progressText) {
-        if(newProgress > 99) {
-          OT.$.css(progressBar, 'width', '');
-          progressText.innerHTML = '100%';
-        } else if(newProgress < 1) {
-          OT.$.css(progressBar, 'width', '0%');
-          progressText.innerHTML = '0%';
-        } else {
-          OT.$.css(progressBar, 'width', newProgress + '%');
-          progressText.innerHTML = newProgress + '%';
-        }
-      } else {
-        progressValue = newProgress;
-      }
-    };
-
-    return modal;
-  };
-
-  OT.Dialogs.Plugin.updateComplete = function(error) {
-    var modal = new OT.$.Modal(adjustModal(function(window, document) {
-      var el = OT.$.bind(templateElement, document),
-          reloadButton,
-          root;
-
-      reloadButton =
-        el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Reload')
-          .on('click', function() {
-            modal.trigger('reload');
-          });
-
-      var msgs;
-
-      if(error) {
-        msgs = ['Update Failed.', error + '' || 'NO ERROR'];
-      } else {
-        msgs = ['Update Complete.',
-          'The OpenTok plugin has been succesfully updated. ' +
-          'Please reload your browser.'];
-      }
-
-      root = el('OT_dialog-centering', [
-        el('OT_dialog-centering-child', [
-          el('OT_root OT_dialog OT_dialog-plugin-upgraded', [
-            el('OT_dialog-messages', [
-              el('OT_dialog-messages-main', msgs[0]),
-              el('OT_dialog-messages-minor', msgs[1])
-            ]),
-            el('OT_dialog-single-button', reloadButton)
-          ])
-        ])
-      ]);
-
-      addDialogCSS(document, [], function() {
-        document.body.appendChild(root);
-      });
-
-    }));
-
-    return modal;
-
-  };
-
-
-})();
-!(function(window) {
-
-  // IMPORTANT This file should be included straight after helpers.js
-  if (!window.OT) window.OT = {};
-
-  if (!OT.properties) {
-    throw new Error('OT.properties does not exist, please ensure that you include a valid ' +
-      'properties file.');
-  }
-
-  OT.useSSL = function () {
-    return OT.properties.supportSSL && (window.location.protocol.indexOf('https') >= 0 ||
-          window.location.protocol.indexOf('chrome-extension') >= 0);
-  };
-
-  // Consumes and overwrites OT.properties. Makes it better and stronger!
-  OT.properties = function(properties) {
-    var props = OT.$.clone(properties);
-
-    props.debug = properties.debug === 'true' || properties.debug === true;
-    props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
-
-    if (window.OTProperties) {
-      // Allow window.OTProperties to override cdnURL, configURL, assetURL and cssURL
-      if (window.OTProperties.cdnURL) props.cdnURL = window.OTProperties.cdnURL;
-      if (window.OTProperties.cdnURLSSL) props.cdnURLSSL = window.OTProperties.cdnURLSSL;
-      if (window.OTProperties.configURL) props.configURL = window.OTProperties.configURL;
-      if (window.OTProperties.assetURL) props.assetURL = window.OTProperties.assetURL;
-      if (window.OTProperties.cssURL) props.cssURL = window.OTProperties.cssURL;
-    }
-
-    if (!props.assetURL) {
-      if (OT.useSSL()) {
-        props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version;
-      } else {
-        props.assetURL = props.cdnURL + '/webrtc/' + props.version;
-      }
-    }
-
-    var isIE89 = OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 9;
-    if (!(isIE89 && window.location.protocol.indexOf('https') < 0)) {
-      props.apiURL = props.apiURLSSL;
-      props.loggingURL = props.loggingURLSSL;
-    }
-
-    if (!props.configURL) props.configURL = props.assetURL + '/js/dynamic_config.min.js';
-    if (!props.cssURL) props.cssURL = props.assetURL + '/css/ot.min.css';
-
-    return props;
-  }(OT.properties);
-})(window);
-!(function() {
-
-//--------------------------------------
-// JS Dynamic Config
-//--------------------------------------
-
-
-  OT.Config = (function() {
-    var _loaded = false,
-        _global = {},
-        _partners = {},
-        _script,
-        _head = document.head || document.getElementsByTagName('head')[0],
-        _loadTimer,
-
-        _clearTimeout = function() {
-          if (_loadTimer) {
-            clearTimeout(_loadTimer);
-            _loadTimer = null;
-          }
-        },
-
-        _cleanup = function() {
-          _clearTimeout();
-
-          if (_script) {
-            _script.onload = _script.onreadystatechange = null;
-
-            if ( _head && _script.parentNode ) {
-              _head.removeChild( _script );
-            }
-
-            _script = undefined;
-          }
-        },
-
-        _onLoad = function() {
-          // Only IE and Opera actually support readyState on Script elements.
-          if (_script.readyState && !/loaded|complete/.test( _script.readyState )) {
-              // Yeah, we're not ready yet...
-            return;
-          }
-
-          _clearTimeout();
-
-          if (!_loaded) {
-            // Our config script is loaded but there is not config (as
-            // replaceWith wasn't called). Something went wrong. Possibly
-            // the file we loaded wasn't actually a valid config file.
-            _this._onLoadTimeout();
-          }
-        },
-
-        _getModule = function(moduleName, apiKey) {
-          if (apiKey && _partners[apiKey] && _partners[apiKey][moduleName]) {
-            return _partners[apiKey][moduleName];
-          }
-
-          return _global[moduleName];
-        },
-
-        _this;
-
-    _this = {
-      // In ms
-      loadTimeout: 4000,
-
-      load: function(configUrl) {
-        if (!configUrl) throw new Error('You must pass a valid configUrl to Config.load');
-
-        _loaded = false;
-
-        setTimeout(function() {
-          _script = document.createElement( 'script' );
-          _script.async = 'async';
-          _script.src = configUrl;
-          _script.onload = _script.onreadystatechange = OT.$.bind(_onLoad, this);
-          _head.appendChild(_script);
-        },1);
-
-        _loadTimer = setTimeout(function() {
-          _this._onLoadTimeout();
-        }, this.loadTimeout);
-      },
-
-      _onLoadTimeout: function() {
-        _cleanup();
-
-        OT.warn('TB DynamicConfig failed to load in ' + _this.loadTimeout + ' ms');
-        this.trigger('dynamicConfigLoadFailed');
-      },
-
-      isLoaded: function() {
-        return _loaded;
-      },
-
-      reset: function() {
-        _cleanup();
-        _loaded = false;
-        _global = {};
-        _partners = {};
-      },
-
-      // This is public so that the dynamic config file can load itself.
-      // Using it for other purposes is discouraged, but not forbidden.
-      replaceWith: function(config) {
-        _cleanup();
-
-        if (!config) config = {};
-
-        _global = config.global || {};
-        _partners = config.partners || {};
-
-        if (!_loaded) _loaded = true;
-        this.trigger('dynamicConfigChanged');
-      },
-
-      // @example Get the value that indicates whether exceptionLogging is enabled
-      //  OT.Config.get('exceptionLogging', 'enabled');
-      //
-      // @example Get a key for a specific partner, fallback to the default if there is
-      // no key for that partner
-      //  OT.Config.get('exceptionLogging', 'enabled', 'apiKey');
-      //
-      get: function(moduleName, key, apiKey) {
-        var module = _getModule(moduleName, apiKey);
-        return module ? module[key] : null;
-      }
-    };
-
-    OT.$.eventing(_this);
-
-    return _this;
-  })();
-
-})(window);
-/**
- * @license  TB Plugin 0.4.0.8 59e99bc HEAD
+
+
+/**
+ * @license  TB Plugin 0.4.0.9 88af499 2014Q4-2.2
  * http://www.tokbox.com/
  *
  * Copyright (c) 2015 TokBox, Inc.
  *
- * Date: January 26 03:18:16 2015
+ * Date: January 08 08:54:38 2015
  *
  */
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: false,
           trailing: true, browser: true, smarttabs:true */
-/* global scope:true, OT:true */
-/* exported TBPlugin */
+/* global scope:true, OT:true, OTHelpers:true */
+/* exported OTPlugin */
 
 /* jshint ignore:start */
 (function(scope) {
 /* jshint ignore:end */
 
 // If we've already be setup, bail
-if (scope.TBPlugin !== void 0) return;
+if (scope.OTPlugin !== void 0) return;
+
 
 // TB must exist first, otherwise we can't do anything
-if (scope.OT === void 0) return;
+// if (scope.OT === void 0) return;
 
 // Establish the environment that we're running in
-var env = OT.$.browserVersion(),
-    isSupported = env.browser === 'IE' && env.version >= 8,
-    pluginReady = false;
-
-var TBPlugin = {
+// Note: we don't currently support 64bit IE
+var isSupported = (OTHelpers.env.name === 'IE' && OTHelpers.env.version >= 8 &&
+                    OTHelpers.env.userAgent.indexOf('x64') === -1),
+    pluginIsReady = false;
+
+
+var OTPlugin = {
   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; };
+  isReady: function() { return pluginIsReady; },
+  meta: {
+    mimeType: 'application/x-opentokie,version=0.4.0.9',
+    activeXName: 'TokBox.OpenTokIE.0.4.0.9',
+    version: '0.4.0.9'
+  }
+};
+
+
+
+// Add logging methods
+OTHelpers.useLogHelpers(OTPlugin);
+
+scope.OTPlugin = OTPlugin;
+
+// If this client isn't supported we still make sure that OTPlugin is defined
+// and the basic API (isSupported() and isInstalled()) is created.
+if (!OTPlugin.isSupported()) {
+  OTPlugin.isInstalled = function isInstalled () { return false; };
   return;
 }
 
 // tb_require('./header.js')
 
 /* exported shim */
 
 // Shims for various missing things from JS
@@ -3788,104 +4305,113 @@ var shim = function shim () {
 
       return res;
     };
   }
 };
 // tb_require('./header.js')
 // tb_require('./shims.js')
 
-/* global OT:true */
-/* exported PluginRumorSocket */
-
-var PluginRumorSocket = function(plugin, server) {
+/* exported RumorSocket */
+
+var RumorSocket = function(plugin, server) {
   var connected = false,
       rumorID;
 
+  var _onOpen,
+      _onClose;
+
+
   try {
     rumorID = plugin._.RumorInit(server, '');
   }
   catch(e) {
-    OT.error('Error creating the Rumor Socket: ', e.message);
+    OTPlugin.error('Error creating the Rumor Socket: ', e.message);
   }
 
   if(!rumorID) {
-    throw new Error('Could not initialise plugin rumor connection');
-  }
-
-  var socket = {
+    throw new Error('Could not initialise OTPlugin rumor connection');
+  }
+
+  plugin._.SetOnRumorOpen(rumorID, function() {
+    if (_onOpen && OTHelpers.isFunction(_onOpen)) {
+      _onOpen.call(null);
+    }
+  });
+
+  plugin._.SetOnRumorClose(rumorID, function(code) {
+    _onClose(code);
+
+    // We're done. Clean up ourselves
+    plugin.removeRef(this);
+  });
+
+  var api = {
     open: function() {
       connected = true;
       plugin._.RumorOpen(rumorID);
     },
 
     close: function(code, reason) {
-      if (!connected) return;
-      connected = false;
-
-      plugin._.RumorClose(rumorID, code, reason);
-      plugin.removeRef(this);
+      if (connected) {
+        connected = false;
+        plugin._.RumorClose(rumorID, code, reason);
+      }
     },
 
     destroy: function() {
       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);
+      _onOpen = callback;
     },
 
     onClose: function(callback) {
-      plugin._.SetOnRumorClose(rumorID, callback);
+      _onClose = callback;
     },
 
     onError: function(callback) {
       plugin._.SetOnRumorError(rumorID, callback);
     },
 
     onMessage: function(callback) {
       plugin._.SetOnRumorMessage(rumorID, callback);
     }
   };
 
-  plugin.addRef(socket);
-  return socket;
-
+  plugin.addRef(api);
+  return api;
 };
 
 // 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 */
+/* global OT:true, scope:true, injectObject: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, '');
+    OTHelpers.callAsync.apply(OTHelpers, args);
+  };
 };
 
 
 var clearObjectLoadTimeout = function clearObjectLoadTimeout (callbackId) {
   if (!callbackId) return;
 
   if (objectTimeouts[callbackId]) {
     clearTimeout(objectTimeouts[callbackId]);
@@ -3897,42 +4423,42 @@ var clearObjectLoadTimeout = function cl
       delete scope[callbackId];
     } catch (err) {
       scope[callbackId] = void 0;
     }
   }
 };
 
 var removeObjectFromDom = function removeObjectFromDom (object) {
-  clearObjectLoadTimeout(object.getAttribute('tb_callbackId'));
+  clearObjectLoadTimeout(object.getAttribute('tbCallbackId'));
 
   if (mediaCaptureObject && mediaCaptureObject.id === object.id) {
     mediaCaptureObject = null;
   }
   else if (plugins.hasOwnProperty(object.id)) {
     delete plugins[object.id];
   }
 
-  object.parentNode.removeChild(object);
+  OTHelpers.removeElement(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 PluginProxy = function PluginProxy (plugin) {
   var _plugin = plugin,
       _liveObjects = [];
 
   this._ = _plugin;
 
   this.addRef = function(ref) {
     _liveObjects.push(ref);
     return this;
@@ -3956,25 +4482,25 @@ var PluginObject = function PluginObject
   this.isValid = function() {
     return _plugin.valid;
   };
 
   // Event Handling Mechanisms
 
   var eventHandlers = {};
 
-  var onCustomEvent = OT.$.bind(curryCallAsync(function onCustomEvent() {
+  var onCustomEvent = OTHelpers.bind(curryCallAsync(function onCustomEvent() {
     var args = Array.prototype.slice.call(arguments),
         name = args.shift();
 
     if (!eventHandlers.hasOwnProperty(name) && eventHandlers[name].length) {
       return;
     }
 
-    OT.$.forEach(eventHandlers[name], function(handler) {
+    OTHelpers.forEach(eventHandlers[name], function(handler) {
       handler[0].apply(handler[1], args);
     });
   }), this);
 
 
   this.on = function (name, callback, context) {
     if (!eventHandlers.hasOwnProperty(name)) {
       eventHandlers[name] = [];
@@ -3985,17 +4511,17 @@ var PluginObject = function PluginObject
   };
 
   this.off = function (name, callback, context) {
     if (!eventHandlers.hasOwnProperty(name) ||
         eventHandlers[name].length === 0) {
       return;
     }
 
-    OT.$.filter(eventHandlers[name], function(listener) {
+    OTHelpers.filter(eventHandlers[name], function(listener) {
       return listener[0] === callback &&
               listener[1] === context;
     });
 
     return this;
   };
 
   this.once = function (name, callback, context) {
@@ -4012,39 +4538,41 @@ var PluginObject = function PluginObject
   this.onReady = function(readyCallback) {
     if (_plugin.on) {
       // If the plugin supports custom events we'll use them
       _plugin.on(-1, {customEvent: curryCallAsync(onCustomEvent, this)});
     }
 
     // Only the main plugin has an initialise method
     if (_plugin.initialise) {
-      this.on('ready', OT.$.bind(curryCallAsync(readyCallback), this));
+      this.on('ready', OTHelpers.bind(curryCallAsync(readyCallback), this));
       _plugin.initialise();
     }
     else {
       readyCallback.call(null);
     }
   };
 
   this.destroy = function() {
     while (_liveObjects.length) {
       _liveObjects.shift().destroy();
     }
 
-    removeObjectFromDom(_plugin);
+    if (_plugin) removeObjectFromDom(_plugin);
     _plugin = null;
   };
 
   this.setStream = function(stream, completion) {
     if (completion) {
       if (stream.hasVideo()) {
         // FIX ME renderingStarted currently doesn't first
         // this.once('renderingStarted', completion);
         var verifyStream = function() {
+          if (!_plugin) return;
+
           if (_plugin.videoWidth > 0) {
             // This fires a little too soon.
             setTimeout(completion, 200);
           }
           else {
             setTimeout(verifyStream, 500);
           }
         };
@@ -4056,188 +4584,64 @@ var PluginObject = function PluginObject
         // 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, debug:true */
+// tb_require('./proxy.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs: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);
+      OTPlugin.debug('VideoContainer appendTo', parentDomElement);
       parentDomElement.appendChild(plugin._);
       this.parentElement = parentDomElement;
     }
   };
 
   this.show = function (completion) {
-    debug('VideoContainer show');
+    OTPlugin.debug('VideoContainer show');
     plugin._.removeAttribute('width');
     plugin._.removeAttribute('height');
     plugin.setStream(stream, completion);
-    OT.$.show(plugin._);
+    OTHelpers.show(plugin._);
   };
 
   this.setWidth = function (width) {
-    debug('VideoContainer setWidth to ' + width);
+    OTPlugin.debug('VideoContainer setWidth to ' + width);
     plugin._.setAttribute('width', width);
   };
 
   this.setHeight = function (height) {
-    debug('VideoContainer setHeight to ' + height);
+    OTPlugin.debug('VideoContainer setHeight to ' + height);
     plugin._.setAttribute('height', height);
   };
 
   this.setVolume = function (value) {
     // TODO
-    debug('VideoContainer setVolume not implemented: called with ' + value);
+    OTPlugin.debug('VideoContainer setVolume not implemented: called with ' + value);
   };
 
   this.getVolume = function () {
     // TODO
-    debug('VideoContainer getVolume not implemented');
+    OTPlugin.debug('VideoContainer getVolume not implemented');
     return 0.5;
   };
 
   this.getImgData = function () {
     return plugin._.getImgData('image/png');
   };
 
   this.getVideoWidth = function () {
@@ -4251,264 +4655,49 @@ var VideoContainer = function VideoConta
   this.destroy = function () {
     plugin._.setStream(null);
     plugin.removeRef(this);
   };
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
-// tb_require('./plugin_object.js')
+// tb_require('./proxy.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* exported RTCStatsReport */
 
 var RTCStatsReport = function (reports) {
   this.forEach = function (callback, context) {
     for (var id in reports) {
       callback.call(context, reports[id]);
     }
   };
 };
 
 // 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('./proxy.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 */
+/* global 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.enabled = OTHelpers.castToBoolean(options.enabled);
   this.streamId = mediaStreamId;
 
   this.setEnabled = function (enabled) {
-    this.enabled = OT.$.castToBoolean(enabled);
+    this.enabled = OTHelpers.castToBoolean(enabled);
 
     if (this.enabled) {
       plugin._.enableMediaStreamTrack(mediaStreamId, this.id);
     }
     else {
       plugin._.disableMediaStreamTrack(mediaStreamId, this.id);
     }
   };
@@ -4535,17 +4724,17 @@ var MediaStream = function MediaStream (
     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 OTHelpers.some(tracks, function(track) {
       return track.enabled;
     });
   };
 
   this.getVideoTracks = function () { return videoTracks; };
   this.getAudioTracks = function () { return audioTracks; };
 
   this.getTrackById = function (id) {
@@ -4581,40 +4770,337 @@ var MediaStream = function MediaStream (
     this.stop();
   };
 
   // Private MediaStream API
   this._ = {
     plugin: plugin,
 
     // Get a VideoContainer to render the stream in.
-    render: OT.$.bind(function() {
+    render: OTHelpers.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')
+// tb_require('./proxy.js')
+// tb_require('./stats.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global 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, ready) {
+  var id = OTHelpers.uuid(),
+      hasLocalDescription = false,
+      hasRemoteDescription = false,
+      candidates = [],
+      inited = false,
+      deferMethods = [],
+      events,
+      _this = this;
+
+  plugin.addRef(this);
+
+  events = {
+    addstream: [],
+    removestream: [],
+    icecandidate: [],
+    signalingstatechange: [],
+    iceconnectionstatechange: []
+  };
+
+  var onAddIceCandidate = function onAddIceCandidate () {/* success */},
+
+      onAddIceCandidateFailed = function onAddIceCandidateFailed (err) {
+        OTPlugin.error('Failed to process candidate');
+        OTPlugin.error(err);
+      },
+
+      processPendingCandidates = function processPendingCandidates () {
+        for (var i=0; i<candidates.length; ++i) {
+          plugin._.addIceCandidate(id, candidates[i], onAddIceCandidate, onAddIceCandidateFailed);
+        }
+      },
+
+
+      deferMethod = function (method) {
+        return function() {
+          if (inited === true) {
+            return method.apply(_this, arguments);
+          }
+
+          deferMethods.push([method, arguments]);
+        };
+      },
+
+      processDeferredMethods = function () {
+        var m;
+        while ( (m = deferMethods.shift()) ) {
+          m[0].apply(_this, m[1]);
+        }
+      },
+
+      triggerEvent = function (/* eventName [, arg1, arg2, ..., argN] */) {
+        var args = Array.prototype.slice.call(arguments),
+            eventName = args.shift();
+
+        if (!events.hasOwnProperty(eventName)) {
+          OTPlugin.error('PeerConnection does not have an event called: ' + eventName);
+          return;
+        }
+
+        OTHelpers.forEach(events[eventName], function(listener) {
+          listener.apply(null, args);
+        });
+      },
+
+      bindAndDelegateEvents = function () {
+        plugin._.on(id, {
+          addStream: function(streamJson) {
+            setTimeout(function() {
+              var stream = MediaStream.fromJson(streamJson, plugin),
+                  event = {stream: stream, target: _this};
+
+              if (_this.onaddstream && OTHelpers.isFunction(_this.onaddstream)) {
+                OTHelpers.callAsync(_this.onaddstream, event);
+              }
+
+              triggerEvent('addstream', event);
+            }, 3000);
+          },
+
+          removeStream: function(streamJson) {
+            var stream = MediaStream.fromJson(streamJson, plugin),
+                event = {stream: stream, target: _this};
+
+            if (_this.onremovestream && OTHelpers.isFunction(_this.onremovestream)) {
+              OTHelpers.callAsync(_this.onremovestream, event);
+            }
+
+            triggerEvent('removestream', event);
+          },
+
+          iceCandidate: function(candidateSdp, sdpMid, sdpMLineIndex) {
+            var candidate = new OTPlugin.RTCIceCandidate({
+              candidate: candidateSdp,
+              sdpMid: sdpMid,
+              sdpMLineIndex: sdpMLineIndex
+            });
+
+            var event = {candidate: candidate, target: _this};
+
+            if (_this.onicecandidate && OTHelpers.isFunction(_this.onicecandidate)) {
+              OTHelpers.callAsync(_this.onicecandidate, event);
+            }
+
+            triggerEvent('icecandidate', event);
+          },
+
+          signalingStateChange: function(state) {
+            _this.signalingState = state;
+            var event = {state: state, target: _this};
+
+            if (_this.onsignalingstatechange &&
+                    OTHelpers.isFunction(_this.onsignalingstatechange)) {
+              OTHelpers.callAsync(_this.onsignalingstatechange, event);
+            }
+
+            triggerEvent('signalingstate', event);
+          },
+
+          iceConnectionChange: function(state) {
+            _this.iceConnectionState = state;
+            var event = {state: state, target: _this};
+
+            if (_this.oniceconnectionstatechange &&
+                    OTHelpers.isFunction(_this.oniceconnectionstatechange)) {
+              OTHelpers.callAsync(_this.oniceconnectionstatechange, event);
+            }
+
+            triggerEvent('iceconnectionstatechange', event);
+          }
+        });
+      };
+
+  this.createOffer = deferMethod(function (success, error, constraints) {
+    OTPlugin.debug('createOffer', constraints);
+    plugin._.createOffer(id, function(type, sdp) {
+      success(new OTPlugin.RTCSessionDescription({
+        type: type,
+        sdp: sdp
+      }));
+    }, error, constraints || {});
+  });
+
+  this.createAnswer = deferMethod(function (success, error, constraints) {
+    OTPlugin.debug('createAnswer', constraints);
+    plugin._.createAnswer(id, function(type, sdp) {
+      success(new OTPlugin.RTCSessionDescription({
+        type: type,
+        sdp: sdp
+      }));
+    }, error, constraints || {});
+  });
+
+  this.setLocalDescription = deferMethod( function (description, success, error) {
+    OTPlugin.debug('setLocalDescription');
+
+    plugin._.setLocalDescription(id, description, function() {
+      hasLocalDescription = true;
+
+      if (hasRemoteDescription) processPendingCandidates();
+
+      if (success) success.call(null);
+    }, error);
+  });
+
+  this.setRemoteDescription = deferMethod( function (description, success, error) {
+    OTPlugin.debug('setRemoteDescription');
+
+    plugin._.setRemoteDescription(id, description, function() {
+      hasRemoteDescription = true;
+
+      if (hasLocalDescription) processPendingCandidates();
+      if (success) success.call(null);
+    }, error);
+  });
+
+  this.addIceCandidate = deferMethod( function (candidate) {
+    OTPlugin.debug('addIceCandidate');
+
+    if (hasLocalDescription && hasRemoteDescription) {
+      plugin._.addIceCandidate(id, candidate, onAddIceCandidate, onAddIceCandidateFailed);
+    }
+    else {
+      candidates.push(candidate);
+    }
+  });
+
+  this.addStream = deferMethod( function (stream) {
+    var constraints = {};
+    plugin._.addStream(id, stream, constraints);
+  });
+
+  this.removeStream = deferMethod( function (stream) {
+    plugin._.removeStream(id, stream);
+  });
+
+  this.getRemoteStreams = function () {
+    return OTHelpers.map(plugin._.getRemoteStreams(id), function(stream) {
+      return MediaStream.fromJson(stream, plugin);
+    });
+  };
+
+  this.getLocalStreams = function () {
+    return OTHelpers.map(plugin._.getLocalStreams(id), function(stream) {
+      return MediaStream.fromJson(stream, plugin);
+    });
+  };
+
+  this.getStreamById = function (streamId) {
+    return MediaStream.fromJson(plugin._.getStreamById(id, streamId), plugin);
+  };
+
+  this.getStats = deferMethod( function (mediaStreamTrack, success, error) {
+    plugin._.getStats(id, mediaStreamTrack || null, function(statsReportJson) {
+      var report = new RTCStatsReport(JSON.parse(statsReportJson));
+      OTHelpers.callAsync(success, report);
+    }, error);
+  });
+
+  this.close = function () {
+    plugin._.destroyPeerConnection(id);
+    plugin.removeRef(this);
+  };
+
+  this.destroy = function () {
+    this.close();
+  };
+
+  this.addEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
+    if (events[event] === void 0) {
+      OTPlugin.error('Could not bind invalid event "' + event + '" to PeerConnection. ' +
+                      'The valid event types are:');
+      OTPlugin.error('\t' + OTHelpers.keys(events).join(', '));
+      return;
+    }
+
+    events[event].push(handler);
+  };
+
+  this.removeEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
+    if (events[event] === void 0) {
+      OTPlugin.error('Could not unbind invalid event "' + event + '" to PeerConnection. ' +
+                      'The valid event types are:');
+      OTPlugin.error('\t' + OTHelpers.keys(events).join(', '));
+      return;
+    }
+
+    events[event] = OTHelpers.filter(events[event], handler);
+  };
+
+  // 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
+  OTHelpers.forEach(iceServers.iceServers, function(iceServer) {
+    if (!iceServer.username) iceServer.username = '';
+    if (!iceServer.credential) iceServer.credential = '';
+  });
+
+  if (!plugin._.initPeerConnection(id, iceServers, options)) {
+    OTPlugin.error('Failed to initialise PeerConnection');
+    ready(new OTHelpers.error('Failed to initialise PeerConnection'));
+    return;
+  }
+
+  // This will make sense
+  inited = true;
+  bindAndDelegateEvents();
+  processDeferredMethods();
+  ready(void 0, this);
+};
+
+PeerConnection.create = function (iceServers, options, plugin, ready) {
+  new PeerConnection(iceServers, options, plugin, ready);
+};
+
+
+
 // tb_require('./header.js')
 // tb_require('./shims.js')
-// tb_require('./plugin_object.js')
+// tb_require('./proxy.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);
+  var constraints = OTHelpers.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) {
@@ -4644,76 +5130,284 @@ var MediaConstraints = function(userCons
 
   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 */
-/* global OT:true, TBPlugin:true, pluginInfo:true, ActiveXObject:true,
+// tb_require('./proxy.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global PluginProxy:true, scope:true */
+/* exported  injectObject, clearObjectLoadTimeout */
+
+var objectTimeouts = {};
+
+var lastElementChild = function lastElementChild(parent) {
+  var node = parent.lastChild;
+
+  while(node && node.nodeType !== 1) {
+    node = node.previousSibling;
+  }
+
+  return node;
+};
+
+var generateCallbackUUID = function generateCallbackUUID () {
+  return 'OTPlugin_loaded_' + OTHelpers.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 createPluginProxy = function createPluginProxy (callbackId, mimeType, params, isVisible) {
+  var objBits = [],
+      extraAttributes = ['width="0" height="0"'],
+      plugin;
+
+  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>');
+
+  scope.document.body.insertAdjacentHTML('beforeend', objBits.join(''));
+  plugin = new PluginProxy(lastElementChild(scope.document.body));
+  plugin._.setAttribute('tbCallbackId', callbackId);
+
+  return plugin;
+};
+
+
+// Stops and cleans up after the plugin object load timeout.
+var injectObject = function injectObject (mimeType, isVisible, params, completion) {
+  var callbackId = generateCallbackUUID(),
+      plugin;
+
+  params.onload = callbackId;
+  params.userAgent = OTHelpers.env.userAgent.toLowerCase();
+
+  scope[callbackId] = function() {
+    clearObjectLoadTimeout(callbackId);
+
+    plugin._.setAttribute('id', 'tb_plugin_' + plugin._.uuid);
+
+    if (plugin._.removeAttribute !== void 0) {
+      plugin._.removeAttribute('tbCallbackId');
+    }
+    else {
+      // Plugin is some kind of weird object that doesn't have removeAttribute on Safari?
+      plugin._.tbCallbackId = null;
+    }
+
+    plugin.uuid = plugin._.uuid;
+    plugin.id = plugin._.id;
+
+    plugin.onReady(function(err) {
+      if (err) {
+        OTPlugin.error('Error while starting up plugin ' + plugin.uuid + ': ' + err);
+        return;
+      }
+
+      OTPlugin.debug('Plugin ' + plugin.id + ' is loaded');
+
+      if (completion && OTHelpers.isFunction(completion)) {
+        completion.call(OTPlugin, null, plugin);
+      }
+    });
+  };
+
+  plugin = createPluginProxy(callbackId, mimeType, params, isVisible);
+
+  objectTimeouts[callbackId] = setTimeout(function() {
+    clearObjectLoadTimeout(callbackId);
+
+    completion.call(OTPlugin, 'The object with the mimeType of ' +
+                                mimeType + ' timed out while loading.');
+
+    scope.document.body.removeChild(plugin._);
+  }, 3000);
+
+  return plugin;
+};
+
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./proxy.js')
+// tb_require('./embedding.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global injectObject, scope:true */
+/* exported createMediaCaptureController:true, createPeerController:true,
+            injectObject:true, plugins:true, mediaCaptureObject:true,
+            removeAllObjects:true */
+
+var objectTimeouts = {},
+    mediaCaptureObject,
+    plugins = {};
+
+
+// @todo bind destroy to unload, may need to coordinate with TB
+// jshint -W098
+var removeAllObjects = function removeAllObjects () {
+  if (mediaCaptureObject) mediaCaptureObject.destroy();
+
+  for (var id in plugins) {
+    if (plugins.hasOwnProperty(id)) {
+      plugins[id].destroy();
+    }
+  }
+};
+
+// Creates the Media Capture controller. This exposes selectSources and is
+// used in the private API.
+//
+// Only one Media Capture controller can exist at once, calling this method
+// more than once will raise an exception.
+//
+var createMediaCaptureController = function createMediaCaptureController (completion) {
+  if (mediaCaptureObject) {
+    throw new Error('OTPlugin.createMediaCaptureController called multiple times!');
+  }
+
+  mediaCaptureObject = injectObject(OTPlugin.meta.mimeType, false, {windowless: false}, completion);
+
+  mediaCaptureObject.selectSources = function() {
+    return this._.selectSources.apply(this._, arguments);
+  };
+
+  return mediaCaptureObject;
+};
+
+// Create an instance of the publisher/subscriber/peerconnection object.
+// Many of these can exist at once, but the +id+ of each must be unique
+// within a single instance of scope (window or window-like thing).
+//
+var createPeerController = function createPeerController (completion) {
+  var o = injectObject(OTPlugin.meta.mimeType, true, {windowless: true}, function(err, plugin) {
+    if (err) {
+      completion.call(OTPlugin, err);
+      return;
+    }
+
+    plugins[plugin.id] = plugin;
+    completion.call(OTPlugin, null, plugin);
+  });
+
+  return o;
+};
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./proxy.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global OT:true, OTPlugin:true, ActiveXObject:true,
           injectObject:true, curryCallAsync:true */
 
 /* exported AutoUpdater:true */
 var AutoUpdater;
 
 (function() {
 
   var autoUpdaterController,
       updaterMimeType,        // <- cached version, use getInstallerMimeType instead
       installedVersion = -1;  // <- cached version, use getInstallerMimeType instead
 
-
-  var versionGreaterThan = function versionGreaterThan (version1,version2) {
+  var versionGreaterThan = function versionGreaterThan (version1, version2) {
     if (version1 === version2) return false;
-
+    if (version1 === -1) return version2;
+    if (version2 === -1) return version1;
+
+    if (version1.indexOf('.') === -1 && version2.indexOf('.') === -1) {
+      return version1 > version2;
+    }
+
+    // The versions have multiple components (i.e. 0.10.30) and
+    // must be compared piecewise.
+    // Note: I'm ignoring the case where one version has multiple
+    // components and the other doesn't.
     var v1 = version1.split('.'),
-        v2 = version2.split('.');
-
-    v1 = parseFloat(parseInt(v1.shift(), 10) + '.' +
-                      v1.map(function(vcomp) { return parseInt(vcomp, 10); }).join(''));
-
-    v2 = parseFloat(parseInt(v2.shift(), 10) + '.' +
-                      v2.map(function(vcomp) { return parseInt(vcomp, 10); }).join(''));
-
-
-    return v1 > v2;
+        v2 = version2.split('.'),
+        versionLength = (v1.length > v2.length ? v2 : v1).length;
+
+
+    for (var i = 0; i < versionLength; ++i) {
+      if (parseInt(v1[i], 10) > parseInt(v2[i], 10)) {
+        return true;
+      }
+    }
+
+    // Special case, v1 has extra components but the initial components
+    // were identical, we assume this means newer but it might also mean
+    // that someone changed versioning systems.
+    if (i < v1.length) {
+      return true;
+    }
+
+    return false;
   };
 
 
   // 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',
+        installPluginName = 'otiePluginInstaller',
         unversionedMimeType = 'application/x-otieplugininstaller',
-        plugin = navigator.plugins[activeXControlId];
+        plugin = navigator.plugins[activeXControlId] || navigator.plugins[installPluginName];
 
     installedVersion = -1;
 
-
     if (plugin) {
       // Look through the supported mime-types for the version
       // There should only be one mime-type in our use case, and
       // 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'),
+                                                      ',version=([0-9a-zA-Z-_.]+)', '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) {
@@ -4721,17 +5415,17 @@ var AutoUpdater;
           bits = extractVersion.exec(mimeType.type);
 
           if (bits !== null && versionGreaterThan(bits[1], installedVersion)) {
             installedVersion = bits[1];
           }
         }
       }
     }
-    else {
+    else if (OTHelpers.env.name === 'IE') {
       // 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) {
@@ -4765,31 +5459,51 @@ var AutoUpdater;
   var hasBrokenUpdater = function () {
     var _broken = !versionGreaterThan(getInstalledVersion(), '0.4.0.4');
 
     hasBrokenUpdater = function() { return _broken; };
     return _broken;
   };
 
 
-  AutoUpdater = function (plugin) {
+  AutoUpdater = function () {
+    var plugin;
+
+    var getControllerCurry = function getControllerFirstCurry (fn) {
+      return function() {
+        if (plugin) {
+          return fn(void 0, arguments);
+        }
+
+        injectObject(getInstallerMimeType(), false, {windowless: false}, function(err, p) {
+          plugin = p;
+
+          if (err) {
+            OTPlugin.error('Error while loading the AutoUpdater: ' + err);
+            return;
+          }
+
+          return fn.apply(void 0, arguments);
+        });
+      };
+    };
 
     // Returns true if the version of the plugin installed on this computer
-    // does not match the one expected by this version of TBPlugin.
+    // does not match the one expected by this version of OTPlugin.
     this.isOutOfDate = function () {
-      return versionGreaterThan(pluginInfo.version, getInstalledVersion());
-    };
-
-    this.autoUpdate = function () {
+      return versionGreaterThan(OTPlugin.meta.version, getInstalledVersion());
+    };
+
+    this.autoUpdate = getControllerCurry(function () {
       var modal = OT.Dialogs.Plugin.updateInProgress(),
           analytics = new OT.Analytics(),
         payload = {
-          ieVersion: OT.$.browserVersion().version,
-          pluginOldVersion: TBPlugin.installedVersion(),
-          pluginNewVersion: TBPlugin.version()
+          ieVersion: OTHelpers.env.version,
+          pluginOldVersion: OTPlugin.installedVersion(),
+          pluginNewVersion: OTPlugin.version()
         };
 
       var success = curryCallAsync(function() {
             analytics.logEvent({
               action: 'OTPluginAutoUpdate',
               variation: 'Success',
               partnerId: OT.APIKEY,
               payload: JSON.stringify(payload)
@@ -4823,279 +5537,264 @@ var AutoUpdater;
                                       '). Please restart your browser and try again.';
 
             modal = OT.Dialogs.Plugin.updateComplete(updateMessage).on({
               'reload': function() {
                 modal.close();
               }
             });
 
-            OT.error('autoUpdate failed: ' + errorMessage + ' (' + errorCode +
+            OTPlugin.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);
-    };
+      plugin._.updatePlugin(OTPlugin.pathToInstaller(), success, error, progress);
+    });
 
     this.destroy = function() {
-      plugin.destroy();
-    };
+      if (plugin) plugin.destroy();
+    };
+
+    // Refresh the plugin list so that we'll hopefully detect newer versions
+    if (navigator.plugins) {
+      navigator.plugins.refresh(false);
+    }
   };
 
   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);
-    });
+    if (!autoUpdaterController) {
+      if (!this.isinstalled()) {
+        completion.call(null, 'Plugin was not installed');
+        return;
+      }
+
+      autoUpdaterController = new AutoUpdater();
+    }
+
+    completion.call(null, void 0, 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')
+// tb_require('./proxy.js')
 // tb_require('./auto_updater.js')
 // tb_require('./media_constraints.js')
 // tb_require('./peer_connection.js')
 // tb_require('./media_stream.js')
 // tb_require('./video_container.js')
 // tb_require('./rumor.js')
+// tb_require('./controllers.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.8',
-    activeXName: 'TokBox.OpenTokIE.0.4.0.8',
-    version: '0.4.0.8'
-  },
-  _document = scope.document,
-  readyCallbacks = [];
-
-var debug = function (message, object) {
-  if (object) {
-    scope.OT.info('TB Plugin - ' + message + ' => ', object);
-  }
-  else {
-    scope.OT.info('TB Plugin - ' + message);
-  }
-};
-
-
-/// Private API
-
-var isDomReady = function isDomReady () {
-      return (_document.readyState === 'complete' ||
-             (_document.readyState === 'interactive' && _document.body));
+/* global scope, shim, pluginIsReady:true, mediaCaptureObject, plugins,
+          createMediaCaptureController, removeAllObjects, AutoUpdater */
+/* export registerReadyListener, notifyReadyListeners, onDomReady */
+
+var readyCallbacks = [];
+
+var // jshint -W098
+    destroy = function destroy () {
+      removeAllObjects();
+    },
+
+    registerReadyListener = function registerReadyListener (callback) {
+      readyCallbacks.push(callback);
+    },
+
+    notifyReadyListeners = function notifyReadyListeners (err) {
+      var callback;
+
+      while ( (callback = readyCallbacks.pop()) && OTHelpers.isFunction(callback) ) {
+        callback.call(OTPlugin, err);
+      }
     },
 
     onDomReady = function onDomReady () {
-      var callCompletionHandlers = function(err) {
-        var callback;
-
-        while ( (callback = readyCallbacks.pop()) && OT.$.isFunction(callback) ) {
-          callback.call(TBPlugin, err);
-        }
-      };
-
       AutoUpdater.get(function(err, updater) {
         if (err) {
-          OT.error('Error while loading the AutoUpdater: ' + err);
-          callCompletionHandlers('Error while loading the AutoUpdater: ' + err);
+          OTPlugin.error('Error while loading the AutoUpdater: ' + err);
+          notifyReadyListeners('Error while loading the AutoUpdater: ' + err);
           return;
         }
 
         // 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);
+          pluginIsReady = true;
+          notifyReadyListeners(err);
+
+          OTHelpers.onDOMUnload(destroy);
         });
       });
-    },
-
-    waitForDomReady = function waitForDomReady () {
-      if (isDomReady()) {
-        onDomReady();
-      }
-      else if (_document.addEventListener) {
-        _document.addEventListener('DOMContentLoaded', onDomReady, false);
-      } else if (_document.attachEvent) {
-        _document.attachEvent('onreadystatechange', function() {
-          if (_document.readyState === 'complete') onDomReady();
-        });
-      }
-    },
-
-    // @todo bind destroy to unload, may need to coordinate with TB
-    // jshint -W098
-    destroy = function destroy () {
-      removeAllObjects();
-    };
-
-
-/// Public API
-
-TBPlugin.isInstalled = function isInstalled () {
+    };
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./proxy.js')
+// tb_require('./auto_updater.js')
+// tb_require('./media_constraints.js')
+// tb_require('./peer_connection.js')
+// tb_require('./media_stream.js')
+// tb_require('./video_container.js')
+// tb_require('./rumor.js')
+// tb_require('./lifecycle.js')
+
+/* jshint globalstrict: true, strict: false, undef: true,
+          unused: true, trailing: true, browser: true, smarttabs:true */
+/* global AutoUpdater,
+          RumorSocket,
+          MediaConstraints, PeerConnection, MediaStream,
+          registerReadyListener,
+          mediaCaptureObject, createPeerController */
+
+OTPlugin.isInstalled = function isInstalled () {
   if (!this.isSupported()) return false;
   return AutoUpdater.isinstalled();
 };
 
-TBPlugin.version = function version () {
-  return pluginInfo.version;
-};
-
-TBPlugin.installedVersion = function installedVersion () {
+OTPlugin.version = function version () {
+  return OTPlugin.meta.version;
+};
+
+OTPlugin.installedVersion = function installedVersion () {
   return AutoUpdater.installedVersion();
 };
 
-// Returns a URI to the TBPlugin installer that is paired with
-// this version of TBPlugin.js.
-TBPlugin.pathToInstaller = function pathToInstaller () {
+// Returns a URI to the OTPlugin installer that is paired with
+// this version of OTPlugin.js.
+OTPlugin.pathToInstaller = function pathToInstaller () {
   return 'https://s3.amazonaws.com/otplugin.tokbox.com/v' +