Bug 1210865 - Update OpenTok library to version 2.6.8. a=lizzard
authorMark Banner <standard8@mozilla.com>
Thu, 29 Oct 2015 05:13:00 +0100
changeset 298549 880b47c05d428a8b4e1f3a35b9a089de1c4de7cf
parent 298548 634347ba438378e44d5e8ffdf3404bd46ea18316
child 298550 cf88e13a483f73be67ee82b742e79415b8f94de1
push id962
push userjlund@mozilla.com
push dateFri, 04 Dec 2015 23:28:54 +0000
treeherdermozilla-release@23a2d286e80f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslizzard
bugs1210865
milestone43.0
Bug 1210865 - Update OpenTok library to version 2.6.8. a=lizzard
browser/components/loop/content/shared/libs/sdk.js
--- a/browser/components/loop/content/shared/libs/sdk.js
+++ b/browser/components/loop/content/shared/libs/sdk.js
@@ -1,31 +1,28 @@
 /**
- * @license  OpenTok JavaScript Library v2.5.2 f4508e1 2015Q1.patch.1
- * http://www.tokbox.com/
- *
- * Copyright (c) 2014 TokBox, Inc.
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- * Date: July 13 05:38:08 2015
+ * @license OpenTok.js v2.6.8 fae7901 HEAD
+ *
+ * Copyright (c) 2010-2015 TokBox, Inc.
+ * Subject to the applicable Software Development Kit (SDK) License Agreement:
+ * https://tokbox.com/support/sdk_license
+ *
+ * Date: October 28 03:45:23 2015
  */
-
-
 !(function(window) {
 
 !(function(window, OTHelpers, undefined) {
 
 /**
- * @license  Common JS Helpers on OpenTok 0.3.0 f151b47 HEAD
+ * @license  Common JS Helpers on OpenTok 0.4.1 259ca46 v0.4.1-branch
  * http://www.tokbox.com/
  *
  * Copyright (c) 2015 TokBox, Inc.
  *
- * Date: July 13 05:37:51 2015
+ * Date: October 28 03:45:12 2015
  *
  */
 
 
 // OT Helper Methods
 //
 // helpers.js                           <- the root file
 // helpers/lib/{helper topic}.js        <- specialised helpers for specific tasks/topics
@@ -551,16 +548,32 @@ if (!Object.create) {
 
 // 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;
 };
 
+
+// A helper for the common case of making a simple promise that is either
+// resolved or rejected straight away.
+//
+// If the +err+ param is provide then the promise will be rejected, otherwise
+// it will resolve.
+//
+OTHelpers.makeSimplePromise = function(err) {
+  return new OTHelpers.RSVP.Promise(function(resolve, reject) {
+    if (err === void 0) {
+      resolve();
+    } else {
+      reject(err);
+    }
+  });
+};
 // tb_require('../../../helpers.js')
 
 /* exported EventableEvent */
 
 OTHelpers.Event = function() {
   return function (type, cancelable) {
     this.type = type;
     this.cancelable = cancelable !== undefined ? cancelable : true;
@@ -629,16 +642,1929 @@ OTHelpers.statable = function(self, poss
   //
   self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) {
     return OTHelpers.arrayIndexOf(arguments, currentState) === -1;
   };
 
   return setState;
 };
 
+/*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;
+  };
+
+})();
+// tb_require('../../environment.js')
+// tb_require('./event.js')
+
+var nodeEventing;
+
+if($.env.name === 'Node') {
+  (function() {
+    var EventEmitter = require('events').EventEmitter,
+        util = require('util');
+
+    // container for the EventEmitter behaviour. This prevents tight coupling
+    // caused by accidentally bleeding implementation details and API into whatever
+    // objects nodeEventing is applied to.
+    var NodeEventable = function NodeEventable () {
+      EventEmitter.call(this);
+
+      this.events = {};
+    };
+    util.inherits(NodeEventable, EventEmitter);
+
+
+    nodeEventing = function nodeEventing (/* self */) {
+      var api = new NodeEventable(),
+          _on = api.on,
+          _off = api.removeListener;
+
+
+      api.addListeners = function (eventNames, handler, context, closure) {
+        var listener = {handler: handler};
+        if (context) listener.context = context;
+        if (closure) listener.closure = closure;
+
+        $.forEach(eventNames, function(name) {
+          if (!api.events[name]) api.events[name] = [];
+          api.events[name].push(listener);
+
+          _on(name, handler);
+
+          var addedListener = name + ':added';
+          if (api.events[addedListener]) {
+            api.emit(addedListener, api.events[name].length);
+          }
+        });
+      };
+
+      api.removeAllListenersNamed = function (eventNames) {
+        var _eventNames = eventNames.split(' ');
+        api.removeAllListeners(_eventNames);
+
+        $.forEach(_eventNames, function(name) {
+          if (api.events[name]) delete api.events[name];
+        });
+      };
+
+      api.removeListeners = function (eventNames, handler, closure) {
+        function filterHandlers(listener) {
+          return !(listener.handler === handler && listener.closure === closure);
+        }
+
+        $.forEach(eventNames.split(' '), function(name) {
+          if (api.events[name]) {
+            _off(name, handler);
+            api.events[name] = $.filter(api.events[name], filterHandlers);
+            if (api.events[name].length === 0) delete api.events[name];
+
+            var removedListener = name + ':removed';
+            if (api.events[removedListener]) {
+              api.emit(removedListener, api.events[name] ? api.events[name].length : 0);
+            }
+          }
+        });
+      };
+
+      api.removeAllListeners = function () {
+        api.events = {};
+        api.removeAllListeners();
+      };
+
+      api.dispatchEvent = function(event, defaultAction) {
+        this.emit(event.type, event);
+
+        if (defaultAction) {
+          defaultAction.call(null, event);
+        }
+      };
+
+      api.trigger = $.bind(api.emit, api);
+
+
+      return api;
+    };
+  })();
+}
+
+// tb_require('../../environment.js')
+// tb_require('./event.js')
+
+var browserEventing;
+
+if($.env.name !== 'Node') {
+
+  browserEventing = function browserEventing (self, syncronous) {
+    var api = {
+      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 = api.events[name];
+      if (!listeners || listeners.length === 0) return;
+
+      var listenerAcks = listeners.length;
+
+      $.forEach(listeners, function(listener) { // , index
+        function filterHandlers(_listener) {
+          return _listener.handler === listener.handler;
+        }
+
+        // We run this asynchronously so that it doesn't interfere with execution if an
+        // error happens
+        $.callAsync(function() {
+          try {
+            // have to check if the listener has not been removed
+            if (api.events[name] && $.some(api.events[name], filterHandlers)) {
+              (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 = api.events[name];
+      if (!listeners || listeners.length === 0) return;
+
+      $.forEach(listeners, function(listener) { // index
+        (listener.closure || listener.handler).apply(listener.context || null, args);
+      });
+    }
+
+    var executeListeners = syncronous === true ?
+      executeListenersSyncronously : executeListenersAsyncronously;
+
+
+    api.addListeners = function (eventNames, handler, context, closure) {
+      var listener = {handler: handler};
+      if (context) listener.context = context;
+      if (closure) listener.closure = closure;
+
+      $.forEach(eventNames, function(name) {
+        if (!api.events[name]) api.events[name] = [];
+        api.events[name].push(listener);
+
+        var addedListener = name + ':added';
+        if (api.events[addedListener]) {
+          executeListeners(addedListener, [api.events[name].length]);
+        }
+      });
+    };
+
+    api.removeListeners = function(eventNames, handler, context) {
+      function filterListeners(listener) {
+        var isCorrectHandler = (
+          listener.handler.originalHandler === handler ||
+          listener.handler === handler
+        );
+
+        return !(isCorrectHandler && listener.context === context);
+      }
+
+      $.forEach(eventNames, function(name) {
+        if (api.events[name]) {
+          api.events[name] = $.filter(api.events[name], filterListeners);
+          if (api.events[name].length === 0) delete api.events[name];
+
+          var removedListener = name + ':removed';
+          if (api.events[ removedListener]) {
+            executeListeners(removedListener, [api.events[name] ? api.events[name].length : 0]);
+          }
+        }
+      });
+    };
+
+    api.removeAllListenersNamed = function (eventNames) {
+      $.forEach(eventNames, function(name) {
+        if (api.events[name]) {
+          delete api.events[name];
+        }
+      });
+    };
+
+    api.removeAllListeners = function () {
+      api.events = {};
+    };
+
+    api.dispatchEvent = function(event, defaultAction) {
+      if (!api.events[event.type] || api.events[event.type].length === 0) {
+        executeDefaultAction(defaultAction, [event]);
+        return;
+      }
+
+      executeListeners(event.type, [event], defaultAction);
+    };
+
+    api.trigger = function(eventName, args) {
+      if (!api.events[eventName] || api.events[eventName].length === 0) {
+        return;
+      }
+
+      executeListeners(eventName, args);
+    };
+
+
+    return api;
+  };
+}
+
+/*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;
+    }
+
+    if (options.overrideMimeType) {
+      if (request.overrideMimeType) {
+        request.overrideMimeType(options.overrideMimeType);
+      }
+      delete options.overrideMimeType;
+    }
+
+    // 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) {
+      if (!Object.prototype.hasOwnProperty.call(_options.headers, name)) {
+        continue;
+      }
+      request.setRequestHeader(name, _options.headers[name]);
+    }
+
+    request.send(options.body && formatPostData(options.body));
+  };
+
+
+  OTHelpers.getJSON = function(url, options, callback) {
+    options = options || {};
+
+    var done = function(error, event) {
+      if(error) {
+        callback(error, event && event.target && event.target.responseText);
+      } else {
+        var response;
+
+        try {
+          response = JSON.parse(event.target.responseText);
+        } catch(e) {
+          // Badly formed JSON
+          callback(e, event && event.target && event.target.responseText);
+          return;
+        }
+
+        callback(null, response, event);
+      }
+    };
+
+    if(options.xdomainrequest) {
+      OTHelpers.xdomainRequest(url, { method: 'GET' }, done);
+    } else {
+      var extendedHeaders = OTHelpers.extend({
+        'Accept': 'application/json'
+      }, options.headers || {});
+
+      OTHelpers.get(url, OTHelpers.extend(options || {}, {
+        headers: extendedHeaders
+      }), done);
+    }
+
+  };
+
+}
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+// tb_require('./environment.js')
+
+
+// Log levels for OTLog.setLogLevel
+var LOG_LEVEL_DEBUG = 5,
+    LOG_LEVEL_LOG   = 4,
+    LOG_LEVEL_INFO  = 3,
+    LOG_LEVEL_WARN  = 2,
+    LOG_LEVEL_ERROR = 1,
+    LOG_LEVEL_NONE  = 0;
+
+
+// There is a single global log level for every component that uses
+// the logs.
+var _logLevel = LOG_LEVEL_NONE;
+
+var setLogLevel = function setLogLevel (level) {
+  _logLevel = typeof(level) === 'number' ? level : 0;
+  return _logLevel;
+};
+
+
+OTHelpers.useLogHelpers = function(on){
+
+  // Log levels for OTLog.setLogLevel
+  on.DEBUG    = LOG_LEVEL_DEBUG;
+  on.LOG      = LOG_LEVEL_LOG;
+  on.INFO     = LOG_LEVEL_INFO;
+  on.WARN     = LOG_LEVEL_WARN;
+  on.ERROR    = LOG_LEVEL_ERROR;
+  on.NONE     = LOG_LEVEL_NONE;
+
+  var _logs = [],
+      _canApplyConsole = true;
+
+  try {
+    Function.prototype.bind.call(window.console.log, window.console);
+  } catch (err) {
+    _canApplyConsole = false;
+  }
+
+  // Some objects can't be logged in the console, mostly these are certain
+  // types of native objects that are exposed to JS. This is only really a
+  // problem with IE, hence only the IE version does anything.
+  var makeLogArgumentsSafe = function(args) { return args; };
+
+  if (OTHelpers.env.name === 'IE') {
+    makeLogArgumentsSafe = function(args) {
+      return [toDebugString(prototypeSlice.apply(args))];
+    };
+  }
+
+  // Generates a logging method for a particular method and log level.
+  //
+  // Attempts to handle the following cases:
+  // * the desired log method doesn't exist, call fallback (if available) instead
+  // * the console functionality isn't available because the developer tools (in IE)
+  // aren't open, call fallback (if available)
+  // * attempt to deal with weird IE hosted logging methods as best we can.
+  //
+  function generateLoggingMethod(method, level, fallback) {
+    return function() {
+      if (on.shouldLog(level)) {
+        var cons = window.console,
+            args = makeLogArgumentsSafe(arguments);
+
+        // In IE, window.console may not exist if the developer tools aren't open
+        // This also means that cons and cons[method] can appear at any moment
+        // hence why we retest this every time.
+        if (cons && cons[method]) {
+          // the desired console method isn't a real object, which means
+          // that we can't use apply on it. We force it to be a real object
+          // using Function.bind, assuming that's available.
+          if (cons[method].apply || _canApplyConsole) {
+            if (!cons[method].apply) {
+              cons[method] = Function.prototype.bind.call(cons[method], cons);
+            }
+
+            cons[method].apply(cons, args);
+          }
+          else {
+            // This isn't the same result as the above, but it's better
+            // than nothing.
+            cons[method](args);
+          }
+        }
+        else if (fallback) {
+          fallback.apply(on, args);
+
+          // Skip appendToLogs, we delegate entirely to the fallback
+          return;
+        }
+
+        appendToLogs(method, makeLogArgumentsSafe(arguments));
+      }
+    };
+  }
+
+  on.log = generateLoggingMethod('log', on.LOG);
+
+  // Generate debug, info, warn, and error logging methods, these all fallback to on.log
+  on.debug = generateLoggingMethod('debug', on.DEBUG, on.log);
+  on.info = generateLoggingMethod('info', on.INFO, on.log);
+  on.warn = generateLoggingMethod('warn', on.WARN, on.log);
+  on.error = generateLoggingMethod('error', on.ERROR, on.log);
+
+
+  on.setLogLevel = function(level) {
+    on.debug('TB.setLogLevel(' + _logLevel + ')');
+    return setLogLevel(level);
+  };
+
+  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 {
+        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);
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+// DOM helpers
+
+// 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);
+      };
+    }
+  });
+};
+
+// 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);
+    }
+  });
+};
+
+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() {
+
+  var _domReady = typeof(document) === 'undefined' ||
+                    document.readyState === 'complete' ||
+                   (document.readyState === 'interactive' && document.body),
+
+      _loadCallbacks = [],
+      _unloadCallbacks = [],
+      _domUnloaded = false,
+
+      onDomReady = function() {
+        _domReady = true;
+
+        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 = [];
+      },
+
+      onDomUnload = function() {
+        _domUnloaded = true;
+
+        OTHelpers.forEach(_unloadCallbacks, function(listener) {
+          listener[0].call(listener[1]);
+        });
+
+        _unloadCallbacks = [];
+      };
+
+
+  OTHelpers.onDOMLoad = function(cb, context) {
+    if (OTHelpers.isReady()) {
+      cb.call(context);
+      return;
+    }
+
+    _loadCallbacks.push([cb, context]);
+  };
+
+  OTHelpers.onDOMUnload = function(cb, context) {
+    if (this.isDOMUnloaded()) {
+      cb.call(context);
+      return;
+    }
+
+    _unloadCallbacks.push([cb, context]);
+  };
+
+  OTHelpers.isReady = function() {
+    return !_domUnloaded && _domReady;
+  };
+
+  OTHelpers.isDOMUnloaded = function() {
+    return _domUnloaded;
+  };
+
+  if (_domReady) {
+    onDomReady();
+  } else if(typeof(document) !== 'undefined') {
+    if (document.addEventListener) {
+      document.addEventListener('DOMContentLoaded', onDomReady, false);
+
+      // fallback
+      window.addEventListener( 'load', onDomReady, false );
+
+    } else if (document.attachEvent) {
+      document.attachEvent('onreadystatechange', function() {
+        if (document.readyState === 'complete') onDomReady();
+      });
+
+      // fallback
+      window.attachEvent( 'onload', onDomReady );
+    }
+  }
+
+})();
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+
+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.forEach = function(fn, context) {
+    OTHelpers.forEach(_models, fn, context);
+    return this;
+  };
+
+  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')
+
+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.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;
+    }
+
+    if (!OTHelpers.isFunction(callback)) {
+      OTHelpers.error('Attempted to register', name,
+                              'capability with a callback that isn\' a function');
+      return;
+    }
+
+    memoriseCapabilityTest(_name, callback);
+  };
+
+
+  // Wrap up a capability test in a function that memorises the
+  // result.
+  var memoriseCapabilityTest = function (name, callback) {
+    capabilities[name] = function() {
+      var result = callback();
+      capabilities[name] = function() {
+        return result;
+      };
+
+      return result;
+    };
+  };
+
+  var testCapability = function (name) {
+    return capabilities[name]();
+  };
+
+
+  // Returns true if all of the capability names passed in
+  // exist and are met.
+  //
+  //  OTHelpers.hasCapabilities('bundle', 'rtcpMux')
+  //
+  OTHelpers.hasCapabilities = function(/* capability1, capability2, ..., capabilityN  */) {
+    var capNames = 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;
+      }
+      else if (testCapability(name) === false) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+})();
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+// tb_require('./capabilities.js')
+
+// Indicates if the client supports WebSockets.
+OTHelpers.registerCapability('websockets', function() {
+  return 'WebSocket' in window && window.WebSocket !== void 0;
+});
+// 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() {
+
+  var requestAnimationFrame = window.requestAnimationFrame ||
+                              window.mozRequestAnimationFrame ||
+                              window.webkitRequestAnimationFrame ||
+                              window.msRequestAnimationFrame;
+
+  if (requestAnimationFrame) {
+    requestAnimationFrame = OTHelpers.bind(requestAnimationFrame, window);
+  }
+  else {
+    var lastTime = 0;
+    var startTime = OTHelpers.now();
+
+    requestAnimationFrame = function(callback){
+      var currTime = OTHelpers.now();
+      var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+      var id = window.setTimeout(function() { callback(currTime - startTime); }, timeToCall);
+      lastTime = currTime + timeToCall;
+      return id;
+    };
+  }
+
+  OTHelpers.requestAnimationFrame = requestAnimationFrame;
+})();
+/*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),
+            overrideMimeType: 'text/plain',
+            headers: {
+              'Accept': 'text/plain',
+              '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);
+                  }
+                  if (curr.onComplete) {
+                    curr.onComplete(err);
+                  }
+                  // There was an error, move onto the next item
+                }
+                if (curr.onComplete) {
+                  curr.onComplete(err);
+                }
+                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.
+    // @param [Boolean] throttle A number specifying the ratio of events to be sent
+    //        out of the total number of events (other events are not ignored). If not
+    //        set to a number, all events are sent.
+    // @param [Number] completionHandler A completion handler function to call when the
+    //                 client event POST request succeeds or fails. If it fails, an error
+    //                 object is passed into the function. (See throttledPost().)
+    //
+    this.logEvent = function(data, qos, throttle, completionHandler) {
+      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);
+
+      post(data, completionHandler, qos);
+    };
+
+    // Log a client QOS to the analytics backend.
+    // Log a client QOS to the analytics backend.
+    // @option options [String] action The name of the Event that we are logging.
+    //  E.g. 'TokShowLoaded'. Required.
+    // @option options [String] variation Usually used for Split A/B testing, when
+    //  you have multiple variations of the +_action+.
+    // @option options [String] payload The payload. Required.
+    // @option options [String] sessionId The active OpenTok session, if there is one
+    // @option options [String] connectionId The active OpenTok connectionId, if there is one
+    // @option options [String] partnerId
+    // @option options [String] guid ...
+    // @option options [String] streamId ...
+    // @option options [String] section ...
+    // @option options [String] clientVersion ...
+    //
+    this.logQOS = function(options) {
+      this.logEvent(options, true);
+    };
+  };
+
+})();
+
+// AJAX helpers
+
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+// tb_require('./ajax/node.js')
+// tb_require('./ajax/browser.js')
+
+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);
+  }
+};
+
 /*!
  *  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
  *
@@ -770,16 +2696,1370 @@ OTHelpers.statable = function(self, poss
 
   // Export RNG options
   uuid.mathRNG = mathRNG;
   uuid.whatwgRNG = whatwgRNG;
 
   OTHelpers.uuid = uuid;
 
 }());
+/*jshint browser:true, smarttabs:true*/
+
+// tb_require('../helpers.js')
+// tb_require('../vendor/uuid.js')
+// tb_require('./dom_events.js')
+
+(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.
+  //
+  var supportsPostMessage = (function () {
+    if (window.postMessage) {
+      // Check to see if postMessage fires synchronously,
+      // if it does, then the implementation of postMessage
+      // is broken.
+      var postMessageIsAsynchronous = true;
+      var oldOnMessage = window.onmessage;
+      window.onmessage = function() {
+        postMessageIsAsynchronous = false;
+      };
+      window.postMessage('', '*');
+      window.onmessage = oldOnMessage;
+      return postMessageIsAsynchronous;
+    }
+  })();
+
+  if (supportsPostMessage) {
+    var timeouts = [],
+        messageName = 'OTHelpers.' + OTHelpers.uuid.v4() + '.zero-timeout';
+
+    var removeMessageHandler = function() {
+      timeouts = [];
+
+      if(window.removeEventListener) {
+        window.removeEventListener('message', handleMessage);
+      } else if(window.detachEvent) {
+        window.detachEvent('onmessage', handleMessage);
+      }
+    };
+
+    var handleMessage = function(event) {
+      if (event.source === window &&
+          event.data === messageName) {
+
+        if(OTHelpers.isFunction(event.stopPropagation)) {
+          event.stopPropagation();
+        }
+        event.cancelBubble = true;
+
+        if (!window.___othelpers) {
+          removeMessageHandler();
+          return;
+        }
+
+        if (timeouts.length > 0) {
+          var args = timeouts.shift(),
+              fn = args.shift();
+
+          fn.apply(null, args);
+        }
+      }
+    };
+
+    // Ensure that we don't receive messages after unload
+    // Yes, this seems to really happen in IE sometimes, usually
+    // when iFrames are involved.
+    OTHelpers.on(window, 'unload', removeMessageHandler);
+
+    if(window.addEventListener) {
+      window.addEventListener('message', handleMessage, true);
+    } else if(window.attachEvent) {
+      window.attachEvent('onmessage', handleMessage);
+    }
+
+    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
+      timeouts.push(prototypeSlice.call(arguments));
+      window.postMessage(messageName, '*');
+    };
+  }
+  else {
+    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
+      var args = prototypeSlice.call(arguments),
+          fn = args.shift();
+
+      setTimeout(function() {
+        fn.apply(null, args);
+      }, 0);
+    };
+  }
+
+
+  // Calls the function +fn+ asynchronously with the current execution.
+  // This is most commonly used to execute something straight after
+  // the current function.
+  //
+  // Any arguments in addition to +fn+ will be passed to +fn+ when it's
+  // called.
+  //
+  // You would use this inplace of setTimeout(fn, 0) type constructs. callAsync
+  // is preferable as it executes in a much more predictable time window,
+  // unlike setTimeout which could execute anywhere from 2ms to several thousand
+  // depending on the browser/context.
+  //
+  // It does this using window.postMessage, although if postMessage won't
+  // work it will fallback to setTimeout.
+  //
+  OTHelpers.callAsync = _callAsync;
+
+
+  // Wraps +handler+ in a function that will execute it asynchronously
+  // so that it doesn't interfere with it's exceution context if it raises
+  // an exception.
+  OTHelpers.createAsyncHandler = function(handler) {
+    return function() {
+      var args = prototypeSlice.call(arguments);
+
+      OTHelpers.callAsync(function() {
+        handler.apply(null, args);
+      });
+    };
+  };
+
+})();
+
+/*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
+
+var firstElementChild;
+
+// This mess is for IE8
+if( typeof(document) !== 'undefined' &&
+      document.createElement('div').firstElementChild !== void 0 ){
+  firstElementChild = function firstElementChild (parentElement) {
+    return parentElement.firstElementChild;
+  };
+}
+else {
+  firstElementChild = function firstElementChild (parentElement) {
+    var el = parentElement.firstChild;
+
+    do {
+      if(el.nodeType===1){
+        return el;
+      }
+      el = el.nextSibling;
+    } while(el);
+
+    return null;
+  };
+}
+
+
+ElementCollection.prototype.appendTo = function(parentElement) {
+  if (!parentElement) throw new Error('appendTo requires a DOMElement to append to.');
+
+  return this.forEach(function(child) {
+    parentElement.appendChild(child);
+  });
+};
+
+ElementCollection.prototype.append = function() {
+  var parentElement = this.first;
+  if (!parentElement) return this;
+
+  $.forEach(prototypeSlice.call(arguments), function(child) {
+    parentElement.appendChild(child);
+  });
+
+  return this;
+};
+
+ElementCollection.prototype.prepend = function() {
+  if (arguments.length === 0) return this;
+
+  var parentElement = this.first,
+      elementsToPrepend;
+
+  if (!parentElement) return this;
+
+  elementsToPrepend = prototypeSlice.call(arguments);
+
+  if (!firstElementChild(parentElement)) {
+    parentElement.appendChild(elementsToPrepend.shift());
+  }
+
+  $.forEach(elementsToPrepend, function(element) {
+    parentElement.insertBefore(element, firstElementChild(parentElement));
+  });
+
+  return this;
+};
+
+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.insertBefore(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.insertBefore(element, nextElement);
+    }
+  });
+};
+
+ElementCollection.prototype.remove = function () {
+  return this.forEach(function(element) {
+    if (element.parentNode) {
+      element.parentNode.removeChild(element);
+    }
+  });
+};
+
+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 (element.firstChild) {
+      element.removeChild(element.firstChild);
+    }
+  });
+};
+
+
+// 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) &&
+                $(element).css('display') === 'none') return true;
+
+    if (element.parentNode && element.parentNode.style) {
+      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')
+
+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() {
+
+  /*jshint eqnull: true, browser: true */
+
+
+  function getPixelSize(element, style, property, fontSize) {
+    var sizeWithSuffix = style[property],
+        size = parseFloat(sizeWithSuffix),
+        suffix = sizeWithSuffix.split(/\d/)[0],
+        rootSize;
+
+    fontSize = fontSize != null ?
+      fontSize : /%|em/.test(suffix) && element.parentElement ?
+        getPixelSize(element.parentElement, element.parentElement.currentStyle, 'fontSize', null) :
+        16;
+    rootSize = property === 'fontSize' ?
+      fontSize : /width/i.test(property) ? element.clientWidth : element.clientHeight;
+
+    return (suffix === 'em') ?
+      size * fontSize : (suffix === 'in') ?
+        size * 96 : (suffix === 'pt') ?
+          size * 96 / 72 : (suffix === '%') ?
+            size / 100 * rootSize : size;
+  }
+
+  function setShortStyleProperty(style, property) {
+    var
+    borderSuffix = property === 'border' ? 'Width' : '',
+    t = property + 'Top' + borderSuffix,
+    r = property + 'Right' + borderSuffix,
+    b = property + 'Bottom' + borderSuffix,
+    l = property + 'Left' + borderSuffix;
+
+    style[property] = (style[t] === style[r] === style[b] === style[l] ? [style[t]]
+    : style[t] === style[b] && style[l] === style[r] ? [style[t], style[r]]
+    : style[l] === style[r] ? [style[t], style[r], style[b]]
+    : [style[t], style[r], style[b], style[l]]).join(' ');
+  }
+
+  function CSSStyleDeclaration(element) {
+    var currentStyle = element.currentStyle,
+        style = this,
+        fontSize = getPixelSize(element, currentStyle, 'fontSize', null),
+        property;
+
+    for (property in currentStyle) {
+      if (/width|height|margin.|padding.|border.+W/.test(property) && style[property] !== 'auto') {
+        style[property] = getPixelSize(element, currentStyle, property, fontSize) + 'px';
+      } else if (property === 'styleFloat') {
+        /*jshint -W069 */
+        style['float'] = currentStyle[property];
+      } else {
+        style[property] = currentStyle[property];
+      }
+    }
+
+    setShortStyleProperty(style, 'margin');
+    setShortStyleProperty(style, 'padding');
+    setShortStyleProperty(style, 'border');
+
+    style.fontSize = fontSize + 'px';
+
+    return style;
+  }
+
+  CSSStyleDeclaration.prototype = {
+    constructor: CSSStyleDeclaration,
+    getPropertyPriority: function () {},
+    getPropertyValue: function ( prop ) {
+      return this[prop] || '';
+    },
+    item: function () {},
+    removeProperty: function () {},
+    setProperty: function () {},
+    getPropertyCSSValue: function () {}
+  };
+
+  function getComputedStyle(element) {
+    return new CSSStyleDeclaration(element);
+  }
+
+
+  OTHelpers.getComputedStyle = function(element) {
+    if(element &&
+        element.ownerDocument &&
+        element.ownerDocument.defaultView &&
+        element.ownerDocument.defaultView.getComputedStyle) {
+      return element.ownerDocument.defaultView.getComputedStyle(element);
+    } else {
+      return getComputedStyle(element);
+    }
+  };
+
+})();
+
+/*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;
+};
+
+var observeSize = function (element, onChange) {
+  var previousSize = {
+    width: 0,
+    height: 0
+  };
+
+  var interval = setInterval(function() {
+    var rect = element.getBoundingClientRect();
+    if (previousSize.width !== rect.width || previousSize.height !== rect.height) {
+      onChange(rect, previousSize);
+      previousSize = {
+        width: rect.width,
+        height: rect.height
+      };
+    }
+  }, 1000 / 5);
+
+  return {
+    disconnect: function() {
+      clearInterval(interval);
+    }
+  };
+};
+
+// 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;
+};
+
+// trigger the +onChange+ callback whenever the width or the height of the element changes
+//
+// 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
+//  sizeObserver = OTHelpers(object).observeSize(function(newSize, previousSize) {
+//      OT.debug("The new width and height are " +
+//                      newSize.width + ',' + newSize.height);
+//  });
+//
+// Cleaning up
+//  // stop observing changes
+//  sizeObserver.disconnect();
+//  sizeObserver = null;
+//
+ElementCollection.prototype.observeSize = function(onChange) {
+  var observers = [];
+
+  this.forEach(function(element) {
+    observers.push(
+      observeSize(element, onChange)
+    );
+  });
+
+  return observers;
+};
+
+
+// @remove
+OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
+  return $(element).observeStyleChanges(stylesToObserve, onChange)[0];
+};
+
+// @remove
+OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
+  return $(element).observeNodeOrChildNodeRemoval(onChange)[0];
+};
+
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+// tb_require('./dom.js')
+// tb_require('./capabilities.js')
+
+// 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 {
+      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')
+
+var specialDomProperties = {
+  'for': 'htmlFor',
+  'class': 'className'
+};
+
+
+// Gets or sets the attribute called +name+ for the first element in the collection
+ElementCollection.prototype.attr = function (name, value) {
+  if (OTHelpers.isObject(name)) {
+    var actualName;
+
+    for (var key in name) {
+      actualName = specialDomProperties[key] || key;
+      this.first.setAttribute(actualName, name[key]);
+    }
+  }
+  else if (value === void 0) {
+    return this.first.getAttribute(specialDomProperties[name] || name);
+  }
+  else {
+    this.first.setAttribute(specialDomProperties[name] || name, value);
+  }
+
+  return this;
+};
+
+
+// Removes an attribute called +name+ for the every element in the collection.
+ElementCollection.prototype.removeAttr = function (name) {
+  var actualName = specialDomProperties[name] || name;
+
+  this.forEach(function(element) {
+    element.removeAttribute(actualName);
+  });
+
+  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 $(element).css('width');
+      },
+
+      _height = function(element) {
+        if (element.offsetHeight > 0) {
+          return element.offsetHeight + 'px';
+        }
+
+        return $(element).css('height');
+      };
+
+  ElementCollection.prototype.width = function (newWidth) {
+    if (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 (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 _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) {
+    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() {
+
+  var displayStateCache = {},
+      defaultDisplays = {};
+
+  var defaultDisplayValueForElement = function (element) {
+    if (defaultDisplays[element.ownerDocument] &&
+      defaultDisplays[element.ownerDocument][element.nodeName]) {
+      return defaultDisplays[element.ownerDocument][element.nodeName];
+    }
+
+    if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
+
+    // We need to know what display value to use for this node. The easiest way
+    // is to actually create a node and read it out.
+    var testNode = element.ownerDocument.createElement(element.nodeName),
+        defaultDisplay;
+
+    element.ownerDocument.body.appendChild(testNode);
+    defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] =
+    $(testNode).css('display');
+
+    $(testNode).remove();
+    testNode = null;
+
+    return defaultDisplay;
+  };
+
+  var isHidden = function (element) {
+    var computedStyle = $.getComputedStyle(element);
+    return computedStyle.getPropertyValue('display') === 'none';
+  };
+
+  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];
+
+        $(element).css(name, styles[name]);
+      }
+    }
+
+    ret = callback(element);
+
+    // Restore the old styles
+    for (name in styles) {
+      if (styles.hasOwnProperty(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) {
+    return $(element).makeVisibleAndYield(callback);
+  };
+
+})();
+
 // tb_require('../helpers.js')
 
 /*!
  * @overview RSVP - a tiny implementation of Promises/A+.
  * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
  * @license   Licensed under MIT license
  *            See https://raw.githubusercontent.com/tildeio/rsvp.js/master/LICENSE
  * @version   3.0.16
@@ -2440,1695 +5720,16 @@ OTHelpers.statable = function(self, poss
       'async': lib$rsvp$$async
     };
 
 
     OTHelpers.RSVP = lib$rsvp$umd$$RSVP;
 }).call(this);
 /* jshint ignore:end */
 
-/*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;
-  };
-
-})();
-// tb_require('../../environment.js')
-// tb_require('./event.js')
-
-var nodeEventing;
-
-if($.env.name === 'Node') {
-  (function() {
-    var EventEmitter = require('events').EventEmitter,
-        util = require('util');
-
-    // container for the EventEmitter behaviour. This prevents tight coupling
-    // caused by accidentally bleeding implementation details and API into whatever
-    // objects nodeEventing is applied to.
-    var NodeEventable = function NodeEventable () {
-      EventEmitter.call(this);
-
-      this.events = {};
-    };
-    util.inherits(NodeEventable, EventEmitter);
-
-
-    nodeEventing = function nodeEventing (/* self */) {
-      var api = new NodeEventable(),
-          _on = api.on,
-          _off = api.removeListener;
-
-
-      api.addListeners = function (eventNames, handler, context, closure) {
-        var listener = {handler: handler};
-        if (context) listener.context = context;
-        if (closure) listener.closure = closure;
-
-        $.forEach(eventNames, function(name) {
-          if (!api.events[name]) api.events[name] = [];
-          api.events[name].push(listener);
-
-          _on(name, handler);
-
-          var addedListener = name + ':added';
-          if (api.events[addedListener]) {
-            api.emit(addedListener, api.events[name].length);
-          }
-        });
-      };
-
-      api.removeAllListenersNamed = function (eventNames) {
-        var _eventNames = eventNames.split(' ');
-        api.removeAllListeners(_eventNames);
-
-        $.forEach(_eventNames, function(name) {
-          if (api.events[name]) delete api.events[name];
-        });
-      };
-
-      api.removeListeners = function (eventNames, handler, closure) {
-        function filterHandlers(listener) {
-          return !(listener.handler === handler && listener.closure === closure);
-        }
-
-        $.forEach(eventNames.split(' '), function(name) {
-          if (api.events[name]) {
-            _off(name, handler);
-            api.events[name] = $.filter(api.events[name], filterHandlers);
-            if (api.events[name].length === 0) delete api.events[name];
-
-            var removedListener = name + ':removed';
-            if (api.events[removedListener]) {
-              api.emit(removedListener, api.events[name] ? api.events[name].length : 0);
-            }
-          }
-        });
-      };
-
-      api.removeAllListeners = function () {
-        api.events = {};
-        api.removeAllListeners();
-      };
-
-      api.dispatchEvent = function(event, defaultAction) {
-        this.emit(event.type, event);
-
-        if (defaultAction) {
-          defaultAction.call(null, event);
-        }
-      };
-
-      api.trigger = $.bind(api.emit, api);
-
-
-      return api;
-    };
-  })();
-}
-
-// tb_require('../../environment.js')
-// tb_require('./event.js')
-
-var browserEventing;
-
-if($.env.name !== 'Node') {
-
-  browserEventing = function browserEventing (self, syncronous) {
-    var api = {
-      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 = api.events[name];
-      if (!listeners || listeners.length === 0) return;
-
-      var listenerAcks = listeners.length;
-
-      $.forEach(listeners, function(listener) { // , index
-        function filterHandlers(_listener) {
-          return _listener.handler === listener.handler;
-        }
-
-        // We run this asynchronously so that it doesn't interfere with execution if an
-        // error happens
-        $.callAsync(function() {
-          try {
-            // have to check if the listener has not been removed
-            if (api.events[name] && $.some(api.events[name], filterHandlers)) {
-              (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 = api.events[name];
-      if (!listeners || listeners.length === 0) return;
-
-      $.forEach(listeners, function(listener) { // index
-        (listener.closure || listener.handler).apply(listener.context || null, args);
-      });
-    }
-
-    var executeListeners = syncronous === true ?
-      executeListenersSyncronously : executeListenersAsyncronously;
-
-
-    api.addListeners = function (eventNames, handler, context, closure) {
-      var listener = {handler: handler};
-      if (context) listener.context = context;
-      if (closure) listener.closure = closure;
-
-      $.forEach(eventNames, function(name) {
-        if (!api.events[name]) api.events[name] = [];
-        api.events[name].push(listener);
-
-        var addedListener = name + ':added';
-        if (api.events[addedListener]) {
-          executeListeners(addedListener, [api.events[name].length]);
-        }
-      });
-    };
-
-    api.removeListeners = function(eventNames, handler, context) {
-      function filterListeners(listener) {
-        var isCorrectHandler = (
-          listener.handler.originalHandler === handler ||
-          listener.handler === handler
-        );
-
-        return !(isCorrectHandler && listener.context === context);
-      }
-
-      $.forEach(eventNames, function(name) {
-        if (api.events[name]) {
-          api.events[name] = $.filter(api.events[name], filterListeners);
-          if (api.events[name].length === 0) delete api.events[name];
-
-          var removedListener = name + ':removed';
-          if (api.events[ removedListener]) {
-            executeListeners(removedListener, [api.events[name] ? api.events[name].length : 0]);
-          }
-        }
-      });
-    };
-
-    api.removeAllListenersNamed = function (eventNames) {
-      $.forEach(eventNames, function(name) {
-        if (api.events[name]) {
-          delete api.events[name];
-        }
-      });
-    };
-
-    api.removeAllListeners = function () {
-      api.events = {};
-    };
-
-    api.dispatchEvent = function(event, defaultAction) {
-      if (!api.events[event.type] || api.events[event.type].length === 0) {
-        executeDefaultAction(defaultAction, [event]);
-        return;
-      }
-
-      executeListeners(event.type, [event], defaultAction);
-    };
-
-    api.trigger = function(eventName, args) {
-      if (!api.events[eventName] || api.events[eventName].length === 0) {
-        return;
-      }
-
-      executeListeners(eventName, args);
-    };
-
-
-    return api;
-  };
-}
-
-/*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')
-// tb_require('./environment.js')
-
-
-// Log levels for OTLog.setLogLevel
-var LOG_LEVEL_DEBUG = 5,
-    LOG_LEVEL_LOG   = 4,
-    LOG_LEVEL_INFO  = 3,
-    LOG_LEVEL_WARN  = 2,
-    LOG_LEVEL_ERROR = 1,
-    LOG_LEVEL_NONE  = 0;
-
-
-// There is a single global log level for every component that uses
-// the logs.
-var _logLevel = LOG_LEVEL_NONE;
-
-var setLogLevel = function setLogLevel (level) {
-  _logLevel = typeof(level) === 'number' ? level : 0;
-  return _logLevel;
-};
-
-
-OTHelpers.useLogHelpers = function(on){
-
-  // Log levels for OTLog.setLogLevel
-  on.DEBUG    = LOG_LEVEL_DEBUG;
-  on.LOG      = LOG_LEVEL_LOG;
-  on.INFO     = LOG_LEVEL_INFO;
-  on.WARN     = LOG_LEVEL_WARN;
-  on.ERROR    = LOG_LEVEL_ERROR;
-  on.NONE     = LOG_LEVEL_NONE;
-
-  var _logs = [],
-      _canApplyConsole = true;
-
-  try {
-    Function.prototype.bind.call(window.console.log, window.console);
-  } catch (err) {
-    _canApplyConsole = false;
-  }
-
-  // Some objects can't be logged in the console, mostly these are certain
-  // types of native objects that are exposed to JS. This is only really a
-  // problem with IE, hence only the IE version does anything.
-  var makeLogArgumentsSafe = function(args) { return args; };
-
-  if (OTHelpers.env.name === 'IE') {
-    makeLogArgumentsSafe = function(args) {
-      return [toDebugString(prototypeSlice.apply(args))];
-    };
-  }
-
-  // Generates a logging method for a particular method and log level.
-  //
-  // Attempts to handle the following cases:
-  // * the desired log method doesn't exist, call fallback (if available) instead
-  // * the console functionality isn't available because the developer tools (in IE)
-  // aren't open, call fallback (if available)
-  // * attempt to deal with weird IE hosted logging methods as best we can.
-  //
-  function generateLoggingMethod(method, level, fallback) {
-    return function() {
-      if (on.shouldLog(level)) {
-        var cons = window.console,
-            args = makeLogArgumentsSafe(arguments);
-
-        // In IE, window.console may not exist if the developer tools aren't open
-        // This also means that cons and cons[method] can appear at any moment
-        // hence why we retest this every time.
-        if (cons && cons[method]) {
-          // the desired console method isn't a real object, which means
-          // that we can't use apply on it. We force it to be a real object
-          // using Function.bind, assuming that's available.
-          if (cons[method].apply || _canApplyConsole) {
-            if (!cons[method].apply) {
-              cons[method] = Function.prototype.bind.call(cons[method], cons);
-            }
-
-            cons[method].apply(cons, args);
-          }
-          else {
-            // This isn't the same result as the above, but it's better
-            // than nothing.
-            cons[method](args);
-          }
-        }
-        else if (fallback) {
-          fallback.apply(on, args);
-
-          // Skip appendToLogs, we delegate entirely to the fallback
-          return;
-        }
-
-        appendToLogs(method, makeLogArgumentsSafe(arguments));
-      }
-    };
-  }
-
-  on.log = generateLoggingMethod('log', on.LOG);
-
-  // Generate debug, info, warn, and error logging methods, these all fallback to on.log
-  on.debug = generateLoggingMethod('debug', on.DEBUG, on.log);
-  on.info = generateLoggingMethod('info', on.INFO, on.log);
-  on.warn = generateLoggingMethod('warn', on.WARN, on.log);
-  on.error = generateLoggingMethod('error', on.ERROR, on.log);
-
-
-  on.setLogLevel = function(level) {
-    on.debug('TB.setLogLevel(' + _logLevel + ')');
-    return setLogLevel(level);
-  };
-
-  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 {
-        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);
-
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-
-// DOM helpers
-
-// 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);
-      };
-    }
-  });
-};
-
-// 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);
-    }
-  });
-};
-
-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() {
-
-  var _domReady = typeof(document) === 'undefined' ||
-                    document.readyState === 'complete' ||
-                   (document.readyState === 'interactive' && document.body),
-
-      _loadCallbacks = [],
-      _unloadCallbacks = [],
-      _domUnloaded = false,
-
-      onDomReady = function() {
-        _domReady = true;
-
-        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 = [];
-      },
-
-      onDomUnload = function() {
-        _domUnloaded = true;
-
-        OTHelpers.forEach(_unloadCallbacks, function(listener) {
-          listener[0].call(listener[1]);
-        });
-
-        _unloadCallbacks = [];
-      };
-
-
-  OTHelpers.onDOMLoad = function(cb, context) {
-    if (OTHelpers.isReady()) {
-      cb.call(context);
-      return;
-    }
-
-    _loadCallbacks.push([cb, context]);
-  };
-
-  OTHelpers.onDOMUnload = function(cb, context) {
-    if (this.isDOMUnloaded()) {
-      cb.call(context);
-      return;
-    }
-
-    _unloadCallbacks.push([cb, context]);
-  };
-
-  OTHelpers.isReady = function() {
-    return !_domUnloaded && _domReady;
-  };
-
-  OTHelpers.isDOMUnloaded = function() {
-    return _domUnloaded;
-  };
-
-  if (_domReady) {
-    onDomReady();
-  } else if(typeof(document) !== 'undefined') {
-    if (document.addEventListener) {
-      document.addEventListener('DOMContentLoaded', onDomReady, false);
-
-      // fallback
-      window.addEventListener( 'load', onDomReady, false );
-
-    } else if (document.attachEvent) {
-      document.attachEvent('onreadystatechange', function() {
-        if (document.readyState === 'complete') onDomReady();
-      });
-
-      // fallback
-      window.attachEvent( 'onload', onDomReady );
-    }
-  }
-
-})();
-
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-
-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;
-};
-
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-
-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));
-};
-
-// 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() {
-
-  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.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;
-    }
-
-    if (!OTHelpers.isFunction(callback)) {
-      OTHelpers.error('Attempted to register', name,
-                              'capability with a callback that isn\' a function');
-      return;
-    }
-
-    memoriseCapabilityTest(_name, callback);
-  };
-
-
-  // Wrap up a capability test in a function that memorises the
-  // result.
-  var memoriseCapabilityTest = function (name, callback) {
-    capabilities[name] = function() {
-      var result = callback();
-      capabilities[name] = function() {
-        return result;
-      };
-
-      return result;
-    };
-  };
-
-  var testCapability = function (name) {
-    return capabilities[name]();
-  };
-
-
-  // Returns true if all of the capability names passed in
-  // exist and are met.
-  //
-  //  OTHelpers.hasCapabilities('bundle', 'rtcpMux')
-  //
-  OTHelpers.hasCapabilities = function(/* capability1, capability2, ..., capabilityN  */) {
-    var capNames = 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;
-      }
-      else if (testCapability(name) === false) {
-        return false;
-      }
-    }
-
-    return true;
-  };
-
-})();
-
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-// tb_require('./capabilities.js')
-
-// 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() {
-
-  var _callAsync;
-
-  // Is true if window.postMessage is supported.
-  // This is not quite as simple as just looking for
-  // window.postMessage as some older versions of IE
-  // have a broken implementation of it.
-  //
-  var supportsPostMessage = (function () {
-    if (window.postMessage) {
-      // Check to see if postMessage fires synchronously,
-      // if it does, then the implementation of postMessage
-      // is broken.
-      var postMessageIsAsynchronous = true;
-      var oldOnMessage = window.onmessage;
-      window.onmessage = function() {
-        postMessageIsAsynchronous = false;
-      };
-      window.postMessage('', '*');
-      window.onmessage = oldOnMessage;
-      return postMessageIsAsynchronous;
-    }
-  })();
-
-  if (supportsPostMessage) {
-    var timeouts = [],
-        messageName = 'OTHelpers.' + OTHelpers.uuid.v4() + '.zero-timeout';
-
-    var removeMessageHandler = function() {
-      timeouts = [];
-
-      if(window.removeEventListener) {
-        window.removeEventListener('message', handleMessage);
-      } else if(window.detachEvent) {
-        window.detachEvent('onmessage', handleMessage);
-      }
-    };
-
-    var handleMessage = function(event) {
-      if (event.source === window &&
-          event.data === messageName) {
-
-        if(OTHelpers.isFunction(event.stopPropagation)) {
-          event.stopPropagation();
-        }
-        event.cancelBubble = true;
-
-        if (!window.___othelpers) {
-          removeMessageHandler();
-          return;
-        }
-
-        if (timeouts.length > 0) {
-          var args = timeouts.shift(),
-              fn = args.shift();
-
-          fn.apply(null, args);
-        }
-      }
-    };
-
-    // Ensure that we don't receive messages after unload
-    // Yes, this seems to really happen in IE sometimes, usually
-    // when iFrames are involved.
-    OTHelpers.on(window, 'unload', removeMessageHandler);
-
-    if(window.addEventListener) {
-      window.addEventListener('message', handleMessage, true);
-    } else if(window.attachEvent) {
-      window.attachEvent('onmessage', handleMessage);
-    }
-
-    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
-      timeouts.push(prototypeSlice.call(arguments));
-      window.postMessage(messageName, '*');
-    };
-  }
-  else {
-    _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
-      var args = prototypeSlice.call(arguments),
-          fn = args.shift();
-
-      setTimeout(function() {
-        fn.apply(null, args);
-      }, 0);
-    };
-  }
-
-
-  // Calls the function +fn+ asynchronously with the current execution.
-  // This is most commonly used to execute something straight after
-  // the current function.
-  //
-  // Any arguments in addition to +fn+ will be passed to +fn+ when it's
-  // called.
-  //
-  // You would use this inplace of setTimeout(fn, 0) type constructs. callAsync
-  // is preferable as it executes in a much more predictable time window,
-  // unlike setTimeout which could execute anywhere from 2ms to several thousand
-  // depending on the browser/context.
-  //
-  // It does this using window.postMessage, although if postMessage won't
-  // work it will fallback to setTimeout.
-  //
-  OTHelpers.callAsync = _callAsync;
-
-
-  // Wraps +handler+ in a function that will execute it asynchronously
-  // so that it doesn't interfere with it's exceution context if it raises
-  // an exception.
-  OTHelpers.createAsyncHandler = function(handler) {
-    return function() {
-      var args = prototypeSlice.call(arguments);
-
-      OTHelpers.callAsync(function() {
-        handler.apply(null, args);
-      });
-    };
-  };
-
-})();
-
 /*global nodeEventing:true, browserEventing:true */
 
 // tb_require('../../helpers.js')
 // tb_require('../callbacks.js')
 // tb_require('../../vendor/rsvp.js')
 // tb_require('./eventing/event.js')
 // tb_require('./eventing/node.js')
 // tb_require('./eventing/browser.js')
@@ -4529,1693 +6130,126 @@ OTHelpers.eventing = function(self, sync
   self.removeEventListener = function(eventName, handler, context) {
     $.warn('The removeEventListener() method is deprecated. Use off() instead.');
     return self.off(eventName, handler, context);
   };
 
 
   return self;
 };
-/*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
-
-var firstElementChild;
-
-// This mess is for IE8
-if( typeof(document) !== 'undefined' &&
-      document.createElement('div').firstElementChild !== void 0 ){
-  firstElementChild = function firstElementChild (parentElement) {
-    return parentElement.firstElementChild;
-  };
-}
-else {
-  firstElementChild = function firstElementChild (parentElement) {
-    var el = parentElement.firstChild;
-
-    do {
-      if(el.nodeType===1){
-        return el;
-      }
-      el = el.nextSibling;
-    } while(el);
-
-    return null;
-  };
-}
-
-
-ElementCollection.prototype.appendTo = function(parentElement) {
-  if (!parentElement) throw new Error('appendTo requires a DOMElement to append to.');
-
-  return this.forEach(function(child) {
-    parentElement.appendChild(child);
-  });
-};
-
-ElementCollection.prototype.append = function() {
-  var parentElement = this.first;
-  if (!parentElement) return this;
-
-  $.forEach(prototypeSlice.call(arguments), function(child) {
-    parentElement.appendChild(child);
-  });
-
-  return this;
-};
-
-ElementCollection.prototype.prepend = function() {
-  if (arguments.length === 0) return this;
-
-  var parentElement = this.first,
-      elementsToPrepend;
-
-  if (!parentElement) return this;
-
-  elementsToPrepend = prototypeSlice.call(arguments);
-
-  if (!firstElementChild(parentElement)) {
-    parentElement.appendChild(elementsToPrepend.shift());
-  }
-
-  $.forEach(elementsToPrepend, function(element) {
-    parentElement.insertBefore(element, firstElementChild(parentElement));
-  });
-
-  return this;
-};
-
-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.insertBefore(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.insertBefore(element, nextElement);
-    }
-  });
-};
-
-ElementCollection.prototype.remove = function () {
-  return this.forEach(function(element) {
-    if (element.parentNode) {
-      element.parentNode.removeChild(element);
-    }
-  });
-};
-
-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 (element.firstChild) {
-      element.removeChild(element.firstChild);
-    }
-  });
-};
-
-
-// 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) &&
-                $(element).css('display') === 'none') return true;
-
-    if (element.parentNode && element.parentNode.style) {
-      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')
-
-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() {
-
-  /*jshint eqnull: true, browser: true */
-
-
-  function getPixelSize(element, style, property, fontSize) {
-    var sizeWithSuffix = style[property],
-        size = parseFloat(sizeWithSuffix),
-        suffix = sizeWithSuffix.split(/\d/)[0],
-        rootSize;
-
-    fontSize = fontSize != null ?
-      fontSize : /%|em/.test(suffix) && element.parentElement ?
-        getPixelSize(element.parentElement, element.parentElement.currentStyle, 'fontSize', null) :
-        16;
-    rootSize = property === 'fontSize' ?
-      fontSize : /width/i.test(property) ? element.clientWidth : element.clientHeight;
-
-    return (suffix === 'em') ?
-      size * fontSize : (suffix === 'in') ?
-        size * 96 : (suffix === 'pt') ?
-          size * 96 / 72 : (suffix === '%') ?
-            size / 100 * rootSize : size;
-  }
-
-  function setShortStyleProperty(style, property) {
-    var
-    borderSuffix = property === 'border' ? 'Width' : '',
-    t = property + 'Top' + borderSuffix,
-    r = property + 'Right' + borderSuffix,
-    b = property + 'Bottom' + borderSuffix,
-    l = property + 'Left' + borderSuffix;
-
-    style[property] = (style[t] === style[r] === style[b] === style[l] ? [style[t]]
-    : style[t] === style[b] && style[l] === style[r] ? [style[t], style[r]]
-    : style[l] === style[r] ? [style[t], style[r], style[b]]
-    : [style[t], style[r], style[b], style[l]]).join(' ');
-  }
-
-  function CSSStyleDeclaration(element) {
-    var currentStyle = element.currentStyle,
-        style = this,
-        fontSize = getPixelSize(element, currentStyle, 'fontSize', null),
-        property;
-
-    for (property in currentStyle) {
-      if (/width|height|margin.|padding.|border.+W/.test(property) && style[property] !== 'auto') {
-        style[property] = getPixelSize(element, currentStyle, property, fontSize) + 'px';
-      } else if (property === 'styleFloat') {
-        /*jshint -W069 */
-        style['float'] = currentStyle[property];
-      } else {
-        style[property] = currentStyle[property];
-      }
-    }
-
-    setShortStyleProperty(style, 'margin');
-    setShortStyleProperty(style, 'padding');
-    setShortStyleProperty(style, 'border');
-
-    style.fontSize = fontSize + 'px';
-
-    return style;
-  }
-
-  CSSStyleDeclaration.prototype = {
-    constructor: CSSStyleDeclaration,
-    getPropertyPriority: function () {},
-    getPropertyValue: function ( prop ) {
-      return this[prop] || '';
-    },
-    item: function () {},
-    removeProperty: function () {},
-    setProperty: function () {},
-    getPropertyCSSValue: function () {}
-  };
-
-  function getComputedStyle(element) {
-    return new CSSStyleDeclaration(element);
-  }
-
-
-  OTHelpers.getComputedStyle = function(element) {
-    if(element &&
-        element.ownerDocument &&
-        element.ownerDocument.defaultView &&
-        element.ownerDocument.defaultView.getComputedStyle) {
-      return element.ownerDocument.defaultView.getComputedStyle(element);
-    } else {
-      return getComputedStyle(element);
-    }
-  };
-
-})();
-
-/*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;
-};
-
-var observeSize = function (element, onChange) {
-  var previousSize = {
-    width: 0,
-    height: 0
-  };
-
-  var interval = setInterval(function() {
-    var rect = element.getBoundingClientRect();
-    if (previousSize.width !== rect.width || previousSize.height !== rect.height) {
-      onChange(rect, previousSize);
-      previousSize = {
-        width: rect.width,
-        height: rect.height
-      };
-    }
-  }, 1000 / 5);
-
-  return {
-    disconnect: function() {
-      clearInterval(interval);
-    }
-  };
-};
-
-// 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;
-};
-
-// trigger the +onChange+ callback whenever the width or the height of the element changes
-//
-// 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
-//  sizeObserver = OTHelpers(object).observeSize(function(newSize, previousSize) {
-//      OT.debug("The new width and height are " +
-//                      newSize.width + ',' + newSize.height);
-//  });
-//
-// Cleaning up
-//  // stop observing changes
-//  sizeObserver.disconnect();
-//  sizeObserver = null;
-//
-ElementCollection.prototype.observeSize = function(onChange) {
-  var observers = [];
-
-  this.forEach(function(element) {
-    observers.push(
-      observeSize(element, onChange)
-    );
-  });
-
-  return observers;
-};
-
-
-// @remove
-OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
-  return $(element).observeStyleChanges(stylesToObserve, onChange)[0];
-};
-
-// @remove
-OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
-  return $(element).observeNodeOrChildNodeRemoval(onChange)[0];
-};
-
-/*jshint browser:true, smarttabs:true */
-
-// tb_require('../helpers.js')
-// tb_require('./dom.js')
-// tb_require('./capabilities.js')
-
-// 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 {
-      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')
-
-var specialDomProperties = {
-  'for': 'htmlFor',
-  'class': 'className'
-};
-
-
-// Gets or sets the attribute called +name+ for the first element in the collection
-ElementCollection.prototype.attr = function (name, value) {
-  if (OTHelpers.isObject(name)) {
-    var actualName;
-
-    for (var key in name) {
-      actualName = specialDomProperties[key] || key;
-      this.first.setAttribute(actualName, name[key]);
-    }
-  }
-  else if (value === void 0) {
-    return this.first.getAttribute(specialDomProperties[name] || name);
-  }
-  else {
-    this.first.setAttribute(specialDomProperties[name] || name, value);
-  }
-
-  return this;
-};
-
-
-// Removes an attribute called +name+ for the every element in the collection.
-ElementCollection.prototype.removeAttr = function (name) {
-  var actualName = specialDomProperties[name] || name;
-
-  this.forEach(function(element) {
-    element.removeAttribute(actualName);
-  });
-
-  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 $(element).css('width');
-      },
-
-      _height = function(element) {
-        if (element.offsetHeight > 0) {
-          return element.offsetHeight + 'px';
-        }
-
-        return $(element).css('height');
-      };
-
-  ElementCollection.prototype.width = function (newWidth) {
-    if (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 (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 _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) {
-    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() {
-
-  var displayStateCache = {},
-      defaultDisplays = {};
-
-  var defaultDisplayValueForElement = function (element) {
-    if (defaultDisplays[element.ownerDocument] &&
-      defaultDisplays[element.ownerDocument][element.nodeName]) {
-      return defaultDisplays[element.ownerDocument][element.nodeName];
-    }
-
-    if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
-
-    // We need to know what display value to use for this node. The easiest way
-    // is to actually create a node and read it out.
-    var testNode = element.ownerDocument.createElement(element.nodeName),
-        defaultDisplay;
-
-    element.ownerDocument.body.appendChild(testNode);
-    defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] =
-    $(testNode).css('display');
-
-    $(testNode).remove();
-    testNode = null;
-
-    return defaultDisplay;
-  };
-
-  var isHidden = function (element) {
-    var computedStyle = $.getComputedStyle(element);
-    return computedStyle.getPropertyValue('display') === 'none';
-  };
-
-  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];
-
-        $(element).css(name, styles[name]);
-      }
-    }
-
-    ret = callback(element);
-
-    // Restore the old styles
-    for (name in styles) {
-      if (styles.hasOwnProperty(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) {
-    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() {
-
-  var requestAnimationFrame = window.requestAnimationFrame ||
-                              window.mozRequestAnimationFrame ||
-                              window.webkitRequestAnimationFrame ||
-                              window.msRequestAnimationFrame;
-
-  if (requestAnimationFrame) {
-    requestAnimationFrame = OTHelpers.bind(requestAnimationFrame, window);
-  }
-  else {
-    var lastTime = 0;
-    var startTime = OTHelpers.now();
-
-    requestAnimationFrame = function(callback){
-      var currTime = OTHelpers.now();
-      var timeToCall = Math.max(0, 16 - (currTime - lastTime));
-      var id = window.setTimeout(function() { callback(currTime - startTime); }, timeToCall);
-      lastTime = currTime + timeToCall;
-      return id;
-    };
-  }
-
-  OTHelpers.requestAnimationFrame = requestAnimationFrame;
-})();
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-
-(function() {
-
-  // Singleton interval
-  var logQueue = [],
-      queueRunning = false;
-
-  OTHelpers.Analytics = function(loggingUrl, debugFn) {
-
-    var endPoint = loggingUrl + '/logging/ClientEvent',
-        endPointQos = loggingUrl + '/logging/ClientQos',
-
-        reportedErrors = {},
-
-        send = function(data, isQos, callback) {
-          OTHelpers.post((isQos ? endPointQos : endPoint) + '?_=' + OTHelpers.uuid.v4(), {
-            body: data,
-            xdomainrequest: ($.env.name === 'IE' && $.env.version < 10),
-            headers: {
-              'Content-Type': 'application/json'
-            }
-          }, callback);
-        },
-
-        throttledPost = function() {
-          // Throttle logs so that they only happen 1 at a time
-          if (!queueRunning && logQueue.length > 0) {
-            queueRunning = true;
-            var curr = logQueue[0];
-
-            // Remove the current item and send the next log
-            var processNextItem = function() {
-              logQueue.shift();
-              queueRunning = false;
-              throttledPost();
-            };
-
-            if (curr) {
-              send(curr.data, curr.isQos, function(err) {
-                if (err) {
-                  var debugMsg = 'Failed to send ClientEvent, moving on to the next item.';
-                  if (debugFn) {
-                    debugFn(debugMsg);
-                  } else {
-                    console.log(debugMsg);
-                  }
-                  // There was an error, move onto the next item
-                } else {
-                  curr.onComplete();
-                }
-                setTimeout(processNextItem, 50);
-              });
-            }
-          }
-        },
-
-        post = function(data, onComplete, isQos) {
-          logQueue.push({
-            data: data,
-            onComplete: onComplete,
-            isQos: isQos
-          });
-
-          throttledPost();
-        },
-
-        shouldThrottleError = function(code, type, partnerId) {
-          if (!partnerId) return false;
-
-          var errKey = [partnerId, type, code].join('_'),
-          //msgLimit = DynamicConfig.get('exceptionLogging', 'messageLimitPerPartner', partnerId);
-            msgLimit = 100;
-          if (msgLimit === null || msgLimit === undefined) return false;
-          return (reportedErrors[errKey] || 0) <= msgLimit;
-        };
-
-    // Log an error via ClientEvents.
-    //
-    // @param [String] code
-    // @param [String] type
-    // @param [String] message
-    // @param [Hash] details additional error details
-    //
-    // @param [Hash] options the options to log the client event with.
-    // @option options [String] action The name of the Event that we are logging. E.g.
-    //  'TokShowLoaded'. Required.
-    // @option options [String] variation Usually used for Split A/B testing, when you
-    //  have multiple variations of the +_action+.
-    // @option options [String] payload The payload. Required.
-    // @option options [String] sessionId The active OpenTok session, if there is one
-    // @option options [String] connectionId The active OpenTok connectionId, if there is one
-    // @option options [String] partnerId
-    // @option options [String] guid ...
-    // @option options [String] streamId ...
-    // @option options [String] section ...
-    // @option options [String] clientVersion ...
-    //
-    // Reports will be throttled to X reports (see exceptionLogging.messageLimitPerPartner
-    // from the dynamic config for X) of each error type for each partner. Reports can be
-    // disabled/enabled globally or on a per partner basis (per partner settings
-    // take precedence) using exceptionLogging.enabled.
-    //
-    this.logError = function(code, type, message, details, options) {
-      if (!options) options = {};
-      var partnerId = options.partnerId;
-
-      if (shouldThrottleError(code, type, partnerId)) {
-        //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' +
-        // code + ' for partner ' + (partnerId || 'No Partner Id'));
-        return;
-      }
-
-      var errKey = [partnerId, type, code].join('_'),
-      payload =  details ? details : null;
-
-      reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ?
-        reportedErrors[errKey] + 1 : 1;
-      this.logEvent(OTHelpers.extend(options, {
-        action: type + '.' + code,
-        payload: payload
-      }), false);
-    };
-
-    // Log a client event to the analytics backend.
-    //
-    // @example Logs a client event called 'foo'
-    //  this.logEvent({
-    //      action: 'foo',
-    //      payload: 'bar',
-    //      sessionId: sessionId,
-    //      connectionId: connectionId
-    //  }, false)
-    //
-    // @param [Hash] data the data to log the client event with.
-    // @param [Boolean] qos Whether this is a QoS event.
-    //
-    this.logEvent = function(data, qos, throttle) {
-      if (!qos) qos = false;
-
-      if (throttle && !isNaN(throttle)) {
-        if (Math.random() > throttle) {
-          return;
-        }
-      }
-
-      // remove properties that have null values:
-      for (var key in data) {
-        if (data.hasOwnProperty(key) && data[key] === null) {
-          delete data[key];
-        }
-      }
-
-      // TODO: catch error when stringifying an object that has a circular reference
-      data = JSON.stringify(data);
-
-      var onComplete = function() {
-        //  OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation']
-        //  + ', payload: ' + data['payload'] + '}');
-      };
-
-      post(data, onComplete, qos);
-    };
-
-    // Log a client QOS to the analytics backend.
-    // Log a client QOS to the analytics backend.
-    // @option options [String] action The name of the Event that we are logging.
-    //  E.g. 'TokShowLoaded'. Required.
-    // @option options [String] variation Usually used for Split A/B testing, when
-    //  you have multiple variations of the +_action+.
-    // @option options [String] payload The payload. Required.
-    // @option options [String] sessionId The active OpenTok session, if there is one
-    // @option options [String] connectionId The active OpenTok connectionId, if there is one
-    // @option options [String] partnerId
-    // @option options [String] guid ...
-    // @option options [String] streamId ...
-    // @option options [String] section ...
-    // @option options [String] clientVersion ...
-    //
-    this.logQOS = function(options) {
-      this.logEvent(options, true);
-    };
-  };
-
-})();
-
-// AJAX helpers
-
-/*jshint browser:true, smarttabs:true*/
-
-// tb_require('../helpers.js')
-// tb_require('./ajax/node.js')
-// tb_require('./ajax/browser.js')
-
-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);
-  }
-};
-
 
 })(window, window.OTHelpers);
 
 
 /**
- * @license  TB Plugin 0.4.0.10 6935b20 HEAD
+ * @license  TB Plugin 0.4.0.12 6e40a4e v0.4.0.12-branch
  * http://www.tokbox.com/
  *
  * Copyright (c) 2015 TokBox, Inc.
  *
- * Date: July 13 05:38:06 2015
+ * Date: October 28 03:45:04 2015
  *
  */
 
 /* global scope:true */
 /* exported OTPlugin */
 
 /* jshint ignore:start */
 (function(scope) {
 /* jshint ignore:end */
 
 // If we've already be setup, bail
 if (scope.OTPlugin !== void 0) return;
 
 var $ = OTHelpers;
 
+// Magic number to avoid plugin crashes through a settimeout call
+window.EmpiricDelay = 3000;
+
 // TB must exist first, otherwise we can't do anything
 // if (scope.OT === void 0) return;
 
 // Establish the environment that we're running in
 // Note: we don't currently support 64bit IE
-var isSupported = $.env.name === 'Safari' ||
-                  ($.env.name === 'IE' && $.env.version >= 8 &&
+var isSupported = ($.env.name === 'IE' && $.env.version >= 8 &&
                     $.env.userAgent.indexOf('x64') === -1),
     pluginIsReady = false;
 
-
 var OTPlugin = {
-  isSupported: function () { return isSupported; },
+  isSupported: function() { return isSupported; },
   isReady: function() { return pluginIsReady; },
   meta: {
-    mimeType: 'application/x-opentokie,version=0.4.0.10',
-    activeXName: 'TokBox.OpenTokIE.0.4.0.10',
-    version: '0.4.0.10'
+    mimeType: 'application/x-opentokplugin,version=0.4.0.12',
+    activeXName: 'TokBox.OpenTokPlugin.0.4.0.12',
+    version: '0.4.0.12'
   },
 
   useLoggingFrom: function(host) {
     // TODO there's no way to revert this, should there be?
     OTPlugin.log = $.bind(host.log, host);
     OTPlugin.debug = $.bind(host.debug, host);
     OTPlugin.info = $.bind(host.info, host);
     OTPlugin.warn = $.bind(host.warn, host);
     OTPlugin.error = $.bind(host.error, host);
   }
 };
 
-
 // Add logging methods
 $.useLogHelpers(OTPlugin);
 
-
 scope.OTPlugin = OTPlugin;
 
 $.registerCapability('otplugin', function() {
   return OTPlugin.isInstalled();
 });
 
 // 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; };
+  OTPlugin.isInstalled = function isInstalled() { return false; };
   return;
 }
 
 // tb_require('./header.js')
 
 /* exported shim */
 
 // Shims for various missing things from JS
 // Applied only after init is called to prevent unnecessary polution
-var shim = function shim () {
+var shim = function shim() {
   if (!Function.prototype.bind) {
-    Function.prototype.bind = function (oThis) {
+    Function.prototype.bind = function(oThis) {
       if (typeof this !== 'function') {
         // closest thing possible to the ECMAScript 5 internal IsCallable function
         throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
       }
 
       var aArgs = Array.prototype.slice.call(arguments, 1),
           fToBind = this,
-          FNOP = function () {},
-          fBound = function () {
+          FNOP = function() {},
+          fBound = function() {
             return fToBind.apply(this instanceof FNOP && oThis ?
                           this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
           };
 
       FNOP.prototype = this.prototype;
       fBound.prototype = new FNOP();
 
       return fBound;
     };
   }
 
-  if(!Array.isArray) {
-    Array.isArray = function (vArg) {
+  if (!Array.isArray) {
+    Array.isArray = function(vArg) {
       return Object.prototype.toString.call(vArg) === '[object Array]';
     };
   }
 
   if (!Array.prototype.indexOf) {
-    Array.prototype.indexOf = function (searchElement, fromIndex) {
+    Array.prototype.indexOf = function(searchElement, fromIndex) {
       var i,
           pivot = (fromIndex) ? fromIndex : 0,
           length;
 
       if (!this) {
         throw new TypeError();
       }
 
@@ -6265,43 +6299,69 @@ var shim = function shim () {
         if (i in t)
           res[i] = fun.call(thisArg, t[i], i, t);
       }
 
       return res;
     };
   }
 };
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+
+/* exported  TaskQueue */
+
+var TaskQueue = function() {
+  var Proto = function TaskQueue() {},
+      api = new Proto(),
+      tasks = [];
+
+  api.add = function(fn, context) {
+    if (context) {
+      tasks.push($.bind(fn, context));
+    } else {
+      tasks.push(fn);
+    }
+  };
+
+  api.runAll = function() {
+    var task;
+    while ((task = tasks.shift())) {
+      task();
+    }
+  };
+
+  return api;
+};
+
 // tb_require('./header.js')
 // tb_require('./shims.js')
 
 /* global curryCallAsync:true */
 /* exported RumorSocket */
 
-var RumorSocket = function (plugin, server) {
-  var Proto = function RumorSocket () {},
+var RumorSocket = function(plugin, server) {
+  var Proto = function RumorSocket() {},
       api = new Proto(),
       connected = false,
       rumorID,
       _onOpen,
       _onClose;
 
-
   try {
     rumorID = plugin._.RumorInit(server, '');
-  }
-  catch(e) {
+  } catch (e) {
     OTPlugin.error('Error creating the Rumor Socket: ', e.message);
   }
 
-  if(!rumorID) {
+  if (!rumorID) {
     throw new Error('Could not initialise OTPlugin rumor connection');
   }
 
-
   api.open = function() {
     connected = true;
     plugin._.RumorOpen(rumorID);
   };
 
   api.close = function(code, reason) {
     if (connected) {
       connected = false;
@@ -6354,299 +6414,325 @@ var RumorSocket = function (plugin, serv
   return api;
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 
 /* exported  refCountBehaviour */
 
-var refCountBehaviour = function refCountBehaviour (api) {
+var refCountBehaviour = function refCountBehaviour(api) {
   var _liveObjects = [];
 
-  api.addRef = function (ref) {
+  api.addRef = function(ref) {
     _liveObjects.push(ref);
     return api;
   };
 
-  api.removeRef = function (ref) {
+  api.removeRef = function(ref) {
     if (_liveObjects.length === 0) return;
 
     var index = _liveObjects.indexOf(ref);
     if (index !== -1) {
       _liveObjects.splice(index, 1);
     }
 
     if (_liveObjects.length === 0) {
       api.destroy();
     }
 
     return api;
   };
 
-  api.removeAllRefs = function () {
+  api.removeAllRefs = function() {
     while (_liveObjects.length) {
       _liveObjects.shift().destroy();
     }
   };
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
-
-/* global curryCallAsync:true */
+// tb_require('./task_queue.js')
+
+/* global curryCallAsync:true, TaskQueue:true */
 /* exported  pluginEventingBehaviour */
 
-var pluginEventingBehaviour = function pluginEventingBehaviour (api) {
-  var eventHandlers = {};
+var pluginEventingBehaviour = function pluginEventingBehaviour(api) {
+  var eventHandlers = {},
+      pendingTasks = new TaskQueue(),
+      isReady = false;
 
   var onCustomEvent = function() {
     var args = Array.prototype.slice.call(arguments);
     api.emit(args.shift(), args);
   };
 
-  api.on = function (name, callback, context) {
+  var devicesChanged = function() {
+    var args = Array.prototype.slice.call(arguments);
+    OTPlugin.debug(args);
+    api.emit('devicesChanged', args);
+  };
+
+  api.on = function(name, callback, context) {
     if (!eventHandlers.hasOwnProperty(name)) {
       eventHandlers[name] = [];
     }
 
     eventHandlers[name].push([callback, context]);
     return api;
   };
 
-  api.off = function (name, callback, context) {
+  api.off = function(name, callback, context) {
     if (!eventHandlers.hasOwnProperty(name) ||
         eventHandlers[name].length === 0) {
       return;
     }
 
     $.filter(eventHandlers[name], function(listener) {
       return listener[0] === callback &&
               listener[1] === context;
     });
 
     return api;
   };
 
-  api.once = function (name, callback, context) {
-    var fn = function () {
+  api.once = function(name, callback, context) {
+    var fn = function() {
       api.off(name, fn);
       return callback.apply(context, arguments);
     };
 
     api.on(name, fn);
     return api;
   };
 
-  api.emit = function (name, args) {
+  api.emit = function(name, args) {
     $.callAsync(function() {
       if (!eventHandlers.hasOwnProperty(name) && eventHandlers[name].length) {
         return;
       }
 
       $.forEach(eventHandlers[name], function(handler) {
         handler[0].apply(handler[1], args);
       });
     });
 
     return api;
   };
 
-  var onReady = function onReady (readyCallback) {
+  // Calling this will bind a listener to the devicesChanged events that
+  // the plugin emits and then rebroadcast them.
+  api.listenForDeviceChanges = function() {
+    if (!isReady) {
+      pendingTasks.add(api.listenForDeviceChanges, api);
+      return;
+    }
+
+    setTimeout(function() {
+      api._.registerXCallback('devicesChanged', devicesChanged);
+    }, window.EmpiricDelay);
+  };
+
+  var onReady = function onReady(readyCallback) {
     if (api._.on) {
       // If the plugin supports custom events we'll use them
       api._.on(-1, {
         customEvent: curryCallAsync(onCustomEvent)
       });
     }
 
+    var internalReadyCallback = function() {
+      // It's not safe to do most plugin operations until the plugin
+      // is ready for us to do so. We use isReady as a guard in
+      isReady = true;
+
+      pendingTasks.runAll();
+      readyCallback.call(api);
+    };
+
     // Only the main plugin has an initialise method
     if (api._.initialise) {
-      api.on('ready', curryCallAsync(readyCallback));
+      api.on('ready', curryCallAsync(internalReadyCallback));
       api._.initialise();
-    }
-    else {
-      readyCallback.call(api);
-    }
-  };
-
-  return function (completion) {
+    } else {
+      internalReadyCallback.call(api);
+    }
+  };
+
+  return function(completion) {
     onReady(function(err) {
       if (err) {
         OTPlugin.error('Error while starting up plugin ' + api.uuid + ': ' + err);
         completion(err);
         return;
       }
 
       OTPlugin.debug('Plugin ' + api.id + ' is loaded');
       completion(void 0, api);
     });
   };
 };
+
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./ref_count_behaviour.js')
 // tb_require('./plugin_eventing_behaviour.js')
 
 /* global refCountBehaviour:true, pluginEventingBehaviour:true, scope:true */
 /* exported createPluginProxy, curryCallAsync, makeMediaPeerProxy, makeMediaCapturerProxy */
 
 var PROXY_LOAD_TIMEOUT = 5000;
 
 var objectTimeouts = {};
 
-var curryCallAsync = function curryCallAsync (fn) {
+var curryCallAsync = function curryCallAsync(fn) {
   return function() {
     var args = Array.prototype.slice.call(arguments);
     args.unshift(fn);
     $.callAsync.apply($, args);
   };
 };
 
-var clearGlobalCallback = function clearGlobalCallback (callbackId) {
+var clearGlobalCallback = function clearGlobalCallback(callbackId) {
   if (!callbackId) return;
 
   if (objectTimeouts[callbackId]) {
     clearTimeout(objectTimeouts[callbackId]);
     objectTimeouts[callbackId] = null;
   }
 
   if (scope[callbackId]) {
     try {
       delete scope[callbackId];
     } catch (err) {
       scope[callbackId] = void 0;
     }
   }
 };
 
-var waitOnGlobalCallback = function waitOnGlobalCallback (callbackId, completion) {
+var waitOnGlobalCallback = function waitOnGlobalCallback(callbackId, completion) {
   objectTimeouts[callbackId] = setTimeout(function() {
     clearGlobalCallback(callbackId);
     completion('The object timed out while loading.');
   }, PROXY_LOAD_TIMEOUT);
 
   scope[callbackId] = function() {
     clearGlobalCallback(callbackId);
 
     var args = Array.prototype.slice.call(arguments);
     args.unshift(null);
     completion.apply(null, args);
   };
 };
 
-var generateCallbackID = function generateCallbackID () {
+var generateCallbackID = function generateCallbackID() {
   return 'OTPlugin_loaded_' + $.uuid().replace(/\-+/g, '');
 };
 
-var generateObjectHtml = function generateObjectHtml (callbackId, options) {
+var generateObjectHtml = function generateObjectHtml(callbackId, options) {
   options = options || {};
 
   var objBits = [],
     attrs = [
       'type="' + options.mimeType + '"',
       'id="' + callbackId + '_obj"',
       'tb_callback_id="' + callbackId + '"',
       'width="0" height="0"'
     ],
     params = {
       userAgent: $.env.userAgent.toLowerCase(),
       windowless: options.windowless,
       onload: callbackId
     };
 
-
   if (options.isVisible !== true) {
     attrs.push('visibility="hidden"');
   }
 
   objBits.push('<object ' + attrs.join(' ') + '>');
 
   for (var name in params) {
     if (params.hasOwnProperty(name)) {
       objBits.push('<param name="' + name + '" value="' + params[name] + '" />');
     }
   }
 
   objBits.push('</object>');
   return objBits.join('');
 };
 
-
-
-var createObject = function createObject (callbackId, options, completion) {
+var createObject = function createObject(callbackId, options, completion) {
   options = options || {};
 
   var html = generateObjectHtml(callbackId, options),
       doc = options.doc || scope.document;
 
   // if (options.robust !== false) {
   //   new createFrame(html, callbackId, scope[callbackId], function(frame, win, doc) {
   //     var object = doc.getElementById(callbackId+'_obj');
   //     object.removeAttribute('id');
   //     completion(void 0, object, frame);
   //   });
   // }
   // else {
 
-
   doc.body.insertAdjacentHTML('beforeend', html);
-  var object = doc.body.querySelector('#'+callbackId+'_obj');
+  var object = doc.body.querySelector('#' + callbackId + '_obj');
 
   // object.setAttribute('type', options.mimeType);
 
   completion(void 0, object);
   // }
 };
 
 // Reference counted wrapper for a plugin object
-var createPluginProxy = function (options, completion) {
+var createPluginProxy = function(options, completion) {
   var Proto = function PluginProxy() {},
       api = new Proto(),
       waitForReadySignal = pluginEventingBehaviour(api);
 
   refCountBehaviour(api);
 
   // Assign +plugin+ to this object and setup all the public
   // accessors that relate to the DOM Object.
   //
-  var setPlugin = function setPlugin (plugin) {
+  var setPlugin = function setPlugin(plugin) {
         if (plugin) {
           api._ = plugin;
           api.parentElement = plugin.parentElement;
           api.$ = $(plugin);
-        }
-        else {
+        } else {
           api._ = null;
           api.parentElement = null;
           api.$ = $();
         }
       };
 
-
   api.uuid = generateCallbackID();
 
   api.isValid = function() {
     return api._.valid;
   };
 
   api.destroy = function() {
     api.removeAllRefs();
     setPlugin(null);
 
     // Let listeners know that they should do any final book keeping
     // that relates to us.
     api.emit('destroy');
   };
 
-
+  api.enumerateDevices = function(completion) {
+    api._.enumerateDevices(completion);
+  };
 
   /// Initialise
 
-
   // The next statement creates the raw plugin object accessor on the Proxy.
   // This is null until we actually have created the Object.
   setPlugin(null);
 
   waitOnGlobalCallback(api.uuid, function(err) {
     if (err) {
       completion('The plugin with the mimeType of ' +
                       options.mimeType + ' timed out while loading: ' + err);
@@ -6673,244 +6759,231 @@ var createPluginProxy = function (option
 
   createObject(api.uuid, options, function(err, plugin) {
     setPlugin(plugin);
   });
 
   return api;
 };
 
-
-
-
 // Specialisation for the MediaCapturer API surface
-var makeMediaCapturerProxy = function makeMediaCapturerProxy (api) {
-
+var makeMediaCapturerProxy = function makeMediaCapturerProxy(api) {
   api.selectSources = function() {
     return api._.selectSources.apply(api._, arguments);
   };
 
+  api.listenForDeviceChanges();
   return api;
 };
 
-
 // Specialisation for the MediaPeer API surface
-var makeMediaPeerProxy = function makeMediaPeerProxy (api) {
+var makeMediaPeerProxy = function makeMediaPeerProxy(api) {
   api.setStream = function(stream, completion) {
     api._.setStream(stream);
 
     if (completion) {
       if (stream.hasVideo()) {
         // FIX ME renderingStarted currently doesn't first
         // api.once('renderingStarted', completion);
         var verifyStream = function() {
           if (!api._) {
             completion(new $.Error('The plugin went away before the stream could be bound.'));
             return;
           }
 
           if (api._.videoWidth > 0) {
             // This fires a little too soon.
             setTimeout(completion, 200);
-          }
-          else {
+          } else {
             setTimeout(verifyStream, 500);
           }
         };
 
         setTimeout(verifyStream, 500);
-      }
-      else {
+      } else {
         // TODO Investigate whether there is a good way to detect
         // when the audio is ready. Does it even matter?
 
         // This fires a little too soon.
         setTimeout(completion, 200);
       }
     }
 
     return api;
   };
 
   return api;
 };
 
-
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 
 /* exported VideoContainer */
 
-var VideoContainer = function (plugin, stream) {
-  var Proto = function VideoContainer () {},
+var VideoContainer = function(plugin, stream) {
+  var Proto = function VideoContainer() {},
       api = new Proto();
 
   api.domElement = plugin._;
   api.$ = $(plugin._);
   api.parentElement = plugin._.parentNode;
 
   plugin.addRef(api);
 
-  api.appendTo = function (parentDomElement) {
+  api.appendTo = function(parentDomElement) {
     if (parentDomElement && plugin._.parentNode !== parentDomElement) {
       OTPlugin.debug('VideoContainer appendTo', parentDomElement);
       parentDomElement.appendChild(plugin._);
       api.parentElement = parentDomElement;
     }
   };
 
-  api.show = function (completion) {
+  api.show = function(completion) {
     OTPlugin.debug('VideoContainer show');
     plugin._.removeAttribute('width');
     plugin._.removeAttribute('height');
     plugin.setStream(stream, completion);
     $.show(plugin._);
     return api;
   };
 
   api.setSize = function(width, height) {
     plugin._.setAttribute('width', width);
     plugin._.setAttribute('height', height);
     return api;
   };
 
-  api.width = function (newWidth) {
+  api.width = function(newWidth) {
     if (newWidth !== void 0) {
       OTPlugin.debug('VideoContainer set width to ' + newWidth);
       plugin._.setAttribute('width', newWidth);
     }
 
     return plugin._.getAttribute('width');
   };
 
-  api.height = function (newHeight) {
+  api.height = function(newHeight) {
     if (newHeight !== void 0) {
       OTPlugin.debug('VideoContainer set height to ' + newHeight);
       plugin._.setAttribute('height', newHeight);
     }
 
     return plugin._.getAttribute('height');
   };
 
-  api.volume = function (newVolume) {
+  api.volume = function(newVolume) {
     if (newVolume !== void 0) {
       // TODO
       OTPlugin.debug('VideoContainer setVolume not implemented: called with ' + newVolume);
-    }
-    else {
+    } else {
       OTPlugin.debug('VideoContainer getVolume not implemented');
     }
 
     return 0.5;
   };
 
-  api.getImgData = function () {
+  api.getImgData = function() {
     return plugin._.getImgData('image/png');
   };
 
-  api.videoWidth = function () {
+  api.videoWidth = function() {
     return plugin._.videoWidth;
   };
 
-  api.videoHeight = function () {
+  api.videoHeight = function() {
     return plugin._.videoHeight;
   };
 
-  api.destroy = function () {
+  api.destroy = function() {
     plugin._.setStream(null);
     plugin.removeRef(api);
   };
 
   return api;
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 
 /* exported RTCStatsReport */
 
-var RTCStatsReport = function RTCStatsReport (reports) {
+var RTCStatsReport = function RTCStatsReport(reports) {
   for (var id in reports) {
     if (reports.hasOwnProperty(id)) {
       this[id] = reports[id];
     }
   }
 };
 
-RTCStatsReport.prototype.forEach = function (callback, context) {
+RTCStatsReport.prototype.forEach = function(callback, context) {
   for (var id in this) {
     if (this.hasOwnProperty(id)) {
       callback.call(context, this[id]);
     }
   }
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 
 /* global createPluginProxy:true, makeMediaPeerProxy:true, makeMediaCapturerProxy:true */
 /* exported PluginProxies  */
 
-
 var PluginProxies = (function() {
-  var Proto = function PluginProxies () {},
+  var Proto = function PluginProxies() {},
       api = new Proto(),
       proxies = {};
 
-
   /// Private API
 
   // This is called whenever a Proxy's destroy event fires.
-  var cleanupProxyOnDestroy = function cleanupProxyOnDestroy (object) {
+  var cleanupProxyOnDestroy = function cleanupProxyOnDestroy(object) {
     if (api.mediaCapturer && api.mediaCapturer.id === object.id) {
       api.mediaCapturer = null;
-    }
-    else if (proxies.hasOwnProperty(object.id)) {
+    } else if (proxies.hasOwnProperty(object.id)) {
       delete proxies[object.id];
     }
 
     if (object.$) {
       object.$.remove();
     }
   };
 
-
   /// Public API
 
-
   // Public accessor for the MediaCapturer
   api.mediaCapturer = null;
 
-  api.removeAll = function removeAll () {
+  api.removeAll = function removeAll() {
     for (var id in proxies) {
       if (proxies.hasOwnProperty(id)) {
         proxies[id].destroy();
       }
     }
 
     if (api.mediaCapturer) api.mediaCapturer.destroy();
   };
 
-  api.create = function create (options, completion) {
+  api.create = function create(options, completion) {
     var proxy = createPluginProxy(options, completion);
 
     proxies[proxy.uuid] = proxy;
 
     // Clean up after this Proxy when it's destroyed.
     proxy.on('destroy', function() {
       cleanupProxyOnDestroy(proxy);
     });
 
     return proxy;
   };
 
-  api.createMediaPeer = function createMediaPeer (options, completion) {
+  api.createMediaPeer = function createMediaPeer(options, completion) {
     if ($.isFunction(options)) {
       completion = options;
       options = {};
     }
 
     var mediaPeer =  api.create($.extend(options || {}, {
       mimeType: OTPlugin.meta.mimeType,
       isVisible: true,
@@ -6923,17 +6996,17 @@ var PluginProxies = (function() {
 
       proxies[mediaPeer.id] = mediaPeer;
       completion.call(OTPlugin, void 0, mediaPeer);
     });
 
     makeMediaPeerProxy(mediaPeer);
   };
 
-  api.createMediaCapturer = function createMediaCapturer (completion) {
+  api.createMediaCapturer = function createMediaCapturer(completion) {
     if (api.mediaCapturer) {
       completion.call(OTPlugin, void 0, api.mediaCapturer);
       return api;
     }
 
     api.mediaCapturer = api.create({
       mimeType: OTPlugin.meta.mimeType,
       isVisible: false,
@@ -6954,18 +7027,18 @@ var PluginProxies = (function() {
 // tb_require('./stats.js')
 
 /* global MediaStream:true, RTCStatsReport:true, curryCallAsync: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 (iceServers, options, plugin, ready) {
-  var Proto = function PeerConnection () {},
+var PeerConnection = function(iceServers, options, plugin, ready) {
+  var Proto = function PeerConnection() {},
       api = new Proto(),
       id = $.uuid(),
       hasLocalDescription = false,
       hasRemoteDescription = false,
       candidates = [],
       inited = false,
       deferMethods = [],
       events;
@@ -6975,246 +7048,237 @@ var PeerConnection = function (iceServer
   events = {
     addstream: [],
     removestream: [],
     icecandidate: [],
     signalingstatechange: [],
     iceconnectionstatechange: []
   };
 
-  var onAddIceCandidate = function onAddIceCandidate () {/* success */},
-
-      onAddIceCandidateFailed = function onAddIceCandidateFailed (err) {
+  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) {
+      processPendingCandidates = function processPendingCandidates() {
+        for (var i = 0; i < candidates.length; ++i) {
           plugin._.addIceCandidate(id, candidates[i], onAddIceCandidate, onAddIceCandidateFailed);
         }
       },
 
-
-      deferMethod = function deferMethod (method) {
+      deferMethod = function deferMethod(method) {
         return function() {
           if (inited === true) {
             return method.apply(api, arguments);
           }
 
           deferMethods.push([method, arguments]);
         };
       },
 
-      processDeferredMethods = function processDeferredMethods () {
+      processDeferredMethods = function processDeferredMethods() {
         var m;
-        while ( (m = deferMethods.shift()) ) {
+        while ((m = deferMethods.shift())) {
           m[0].apply(api, m[1]);
         }
       },
 
-      triggerEvent = function triggerEvent (/* eventName [, arg1, arg2, ..., argN] */) {
+      triggerEvent = function triggerEvent(/* 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;
         }
 
         $.forEach(events[eventName], function(listener) {
           listener.apply(null, args);
         });
       },
 
-      bindAndDelegateEvents = function bindAndDelegateEvents (events) {
+      bindAndDelegateEvents = function bindAndDelegateEvents(events) {
         for (var name in events) {
           if (events.hasOwnProperty(name)) {
             events[name] = curryCallAsync(events[name]);
           }
         }
 
         plugin._.on(id, events);
       },
 
-      addStream = function addStream (streamJson) {
+      addStream = function addStream(streamJson) {
         setTimeout(function() {
           var stream = MediaStream.fromJson(streamJson, plugin),
               event = {stream: stream, target: api};
 
           if (api.onaddstream && $.isFunction(api.onaddstream)) {
             $.callAsync(api.onaddstream, event);
           }
 
           triggerEvent('addstream', event);
-        }, 3000);
-      },
-
-      removeStream = function removeStream (streamJson) {
+        }, window.EmpiricDelay);
+      },
+
+      removeStream = function removeStream(streamJson) {
         var stream = MediaStream.fromJson(streamJson, plugin),
             event = {stream: stream, target: api};
 
         if (api.onremovestream && $.isFunction(api.onremovestream)) {
           $.callAsync(api.onremovestream, event);
         }
 
         triggerEvent('removestream', event);
       },
 
-      iceCandidate = function iceCandidate (candidateSdp, sdpMid, sdpMLineIndex) {
+      iceCandidate = function iceCandidate(candidateSdp, sdpMid, sdpMLineIndex) {
         var candidate = new OTPlugin.RTCIceCandidate({
           candidate: candidateSdp,
           sdpMid: sdpMid,
           sdpMLineIndex: sdpMLineIndex
         });
 
         var event = {candidate: candidate, target: api};
 
         if (api.onicecandidate && $.isFunction(api.onicecandidate)) {
           $.callAsync(api.onicecandidate, event);
         }
 
         triggerEvent('icecandidate', event);
       },
 
-      signalingStateChange = function signalingStateChange (state) {
+      signalingStateChange = function signalingStateChange(state) {
         api.signalingState = state;
         var event = {state: state, target: api};
 
         if (api.onsignalingstatechange &&
                 $.isFunction(api.onsignalingstatechange)) {
           $.callAsync(api.onsignalingstatechange, event);
         }
 
         triggerEvent('signalingstate', event);
       },
 
-      iceConnectionChange = function iceConnectionChange (state) {
+      iceConnectionChange = function iceConnectionChange(state) {
         api.iceConnectionState = state;
         var event = {state: state, target: api};
 
         if (api.oniceconnectionstatechange &&
                 $.isFunction(api.oniceconnectionstatechange)) {
           $.callAsync(api.oniceconnectionstatechange, event);
         }
 
         triggerEvent('iceconnectionstatechange', event);
       };
 
-  api.createOffer = deferMethod(function (success, error, constraints) {
+  api.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 || {});
   });
 
-  api.createAnswer = deferMethod(function (success, error, constraints) {
+  api.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 || {});
   });
 
-  api.setLocalDescription = deferMethod( function (description, success, error) {
+  api.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);
   });
 
-  api.setRemoteDescription = deferMethod( function (description, success, error) {
+  api.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);
   });
 
-  api.addIceCandidate = deferMethod( function (candidate) {
+  api.addIceCandidate = deferMethod(function(candidate) {
     OTPlugin.debug('addIceCandidate');
 
     if (hasLocalDescription && hasRemoteDescription) {
       plugin._.addIceCandidate(id, candidate, onAddIceCandidate, onAddIceCandidateFailed);
-    }
-    else {
+    } else {
       candidates.push(candidate);
     }
   });
 
-  api.addStream = deferMethod( function (stream) {
+  api.addStream = deferMethod(function(stream) {
     var constraints = {};
     plugin._.addStream(id, stream, constraints);
   });
 
-  api.removeStream = deferMethod( function (stream) {
+  api.removeStream = deferMethod(function(stream) {
     plugin._.removeStream(id, stream);
   });
 
-
-  api.getRemoteStreams = function () {
+  api.getRemoteStreams = function() {
     return $.map(plugin._.getRemoteStreams(id), function(stream) {
       return MediaStream.fromJson(stream, plugin);
     });
   };
 
-  api.getLocalStreams = function () {
+  api.getLocalStreams = function() {
     return $.map(plugin._.getLocalStreams(id), function(stream) {
       return MediaStream.fromJson(stream, plugin);
     });
   };
 
-  api.getStreamById = function (streamId) {
+  api.getStreamById = function(streamId) {
     return MediaStream.fromJson(plugin._.getStreamById(id, streamId), plugin);
   };
 
-  api.getStats = deferMethod( function (mediaStreamTrack, success, error) {
+  api.getStats = deferMethod(function(mediaStreamTrack, success, error) {
     plugin._.getStats(id, mediaStreamTrack || null, curryCallAsync(function(statsReportJson) {
       var report = new RTCStatsReport(JSON.parse(statsReportJson));
       success(report);
     }), error);
   });
 
-  api.close = function () {
+  api.close = function() {
     plugin._.destroyPeerConnection(id);
     plugin.removeRef(this);
   };
 
-  api.destroy = function () {
+  api.destroy = function() {
     api.close();
   };
 
-  api.addEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
+  api.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' + $.keys(events).join(', '));
       return;
     }
 
     events[event].push(handler);
   };
 
-  api.removeEventListener = function  (event, handler /* [, useCapture] we ignore this */) {
+  api.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' + $.keys(events).join(', '));
       return;
     }
 
     events[event] = $.filter(events[event], handler);
   };
 
   // These should appear to be null, instead of undefined, if no
   // callbacks are assigned. This more closely matches how the native
@@ -7248,110 +7312,108 @@ var PeerConnection = function (iceServer
 
   inited = true;
   processDeferredMethods();
   ready(void 0, api);
 
   return api;
 };
 
-PeerConnection.create = function (iceServers, options, plugin, ready) {
+PeerConnection.create = function(iceServers, options, plugin, ready) {
   new PeerConnection(iceServers, options, plugin, ready);
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 // tb_require('./video_container.js')
 
 /* global VideoContainer:true */
 /* exported MediaStream */
 
-
-var MediaStreamTrack = function (mediaStreamId, options, plugin) {
-  var Proto = function MediaStreamTrack () {},
+var MediaStreamTrack = function(mediaStreamId, options, plugin) {
+  var Proto = function MediaStreamTrack() {},
       api = new Proto();
 
   api.id = options.id;
   api.kind = options.kind;
   api.label = options.label;
   api.enabled = $.castToBoolean(options.enabled);
   api.streamId = mediaStreamId;
 
-  api.setEnabled = function (enabled) {
+  api.setEnabled = function(enabled) {
     api.enabled = $.castToBoolean(enabled);
 
     if (api.enabled) {
       plugin._.enableMediaStreamTrack(mediaStreamId, api.id);
-    }
-    else {
+    } else {
       plugin._.disableMediaStreamTrack(mediaStreamId, api.id);
     }
   };
 
   return api;
 };
 
-var MediaStream = function (options, plugin) {
-  var Proto = function MediaStream () {},
+var MediaStream = function(options, plugin) {
+  var Proto = function MediaStream() {},
       api = new Proto(),
       audioTracks = [],
       videoTracks = [];
 
   api.id = options.id;
   plugin.addRef(api);
 
   // TODO
   // api.ended =
   // api.onended =
 
   if (options.videoTracks) {
     options.videoTracks.map(function(track) {
-      videoTracks.push( new MediaStreamTrack(options.id, track, plugin) );
+      videoTracks.push(new MediaStreamTrack(options.id, track, plugin));
     });
   }
 
   if (options.audioTracks) {
     options.audioTracks.map(function(track) {
-      audioTracks.push( new MediaStreamTrack(options.id, track, plugin) );
-    });
-  }
-
-  var hasTracksOfType = function (type) {
+      audioTracks.push(new MediaStreamTrack(options.id, track, plugin));
+    });
+  }
+
+  var hasTracksOfType = function(type) {
     var tracks = type === 'video' ? videoTracks : audioTracks;
 
     return $.some(tracks, function(track) {
       return track.enabled;
     });
   };
 
-  api.getVideoTracks = function () { return videoTracks; };
-  api.getAudioTracks = function () { return audioTracks; };
-
-  api.getTrackById = function (id) {
+  api.getVideoTracks = function() { return videoTracks; };
+  api.getAudioTracks = function() { return audioTracks; };
+
+  api.getTrackById = function(id) {
     videoTracks.concat(audioTracks).forEach(function(track) {
       if (track.id === id) return track;
     });
 
     return null;
   };
 
-  api.hasVideo = function () {
+  api.hasVideo = function() {
     return hasTracksOfType('video');
   };
 
-  api.hasAudio = function () {
+  api.hasAudio = function() {
     return hasTracksOfType('audio');
   };
 
-  api.addTrack = function (/* MediaStreamTrack */) {
+  api.addTrack = function(/* MediaStreamTrack */) {
     // TODO
   };
 
-  api.removeTrack = function (/* MediaStreamTrack */) {
+  api.removeTrack = function(/* MediaStreamTrack */) {
     // TODO
   };
 
   api.stop = function() {
     plugin._.stopMediaStream(api.id);
     plugin.removeRef(api);
   };
 
@@ -7367,20 +7429,19 @@ var MediaStream = function (options, plu
     render: function() {
       return new VideoContainer(plugin, api);
     }
   };
 
   return api;
 };
 
-
-MediaStream.fromJson = function (json, plugin) {
+MediaStream.fromJson = function(json, plugin) {
   if (!json) return null;
-  return new MediaStream( JSON.parse(json), plugin );
+  return new MediaStream(JSON.parse(json), plugin);
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 // tb_require('./video_container.js')
 
 /* exported MediaConstraints */
@@ -7398,18 +7459,18 @@ var MediaConstraints = function(userCons
     constraints.video.mandatory = {};
   }
 
   if (this.hasAudio && !constraints.audio.mandatory) {
     constraints.audio.mandatory = {};
   }
 
   this.screenSharing = this.hasVideo &&
-                ( constraints.video.mandatory.chromeMediaSource === 'screen' ||
-                  constraints.video.mandatory.chromeMediaSource === 'window' );
+                (constraints.video.mandatory.chromeMediaSource === 'screen' ||
+                 constraints.video.mandatory.chromeMediaSource === 'window');
 
   this.audio = constraints.audio;
   this.video = constraints.video;
 
   this.setVideoSource = function(sourceId) {
     if (sourceId !== void 0) constraints.video.mandatory.sourceId =  sourceId;
     else delete constraints.video;
   };
@@ -7421,353 +7482,22 @@ var MediaConstraints = function(userCons
 
   this.toHash = function() {
     return constraints;
   };
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
-// tb_require('./plugin_proxies.js')
-
-/* global OT:true, OTPlugin:true, ActiveXObject:true,
-          PluginProxies:true, curryCallAsync:true */
-
-/* exported AutoUpdater:true */
-var AutoUpdater;
-
-(function() {
-
-  var autoUpdaterController,
-      updaterMimeType,        // <- cached version, use getInstallerMimeType instead
-      installedVersion = -1;  // <- cached version, use getInstallerMimeType instead
-
-  var versionGreaterThan = function versionGreaterThan (version1, version2) {
-    if (version1 === version2) return false;
-    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('.'),
-        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] || 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-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) {
-
-          bits = extractVersion.exec(mimeType.type);
-
-          if (bits !== null && versionGreaterThan(bits[1], installedVersion)) {
-            installedVersion = bits[1];
-          }
-        }
-      }
-    }
-    else if ($.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) {
-      }
-    }
-
-    updaterMimeType = installedVersion !== -1 ?
-                              unversionedMimeType + ',version=' + installedVersion :
-                              null;
-  };
-
-  var getInstallerMimeType = function getInstallerMimeType () {
-    if (updaterMimeType === void 0) {
-      findMimeTypeAndVersion();
-    }
-
-    return updaterMimeType;
-  };
-
-  var getInstalledVersion = function getInstalledVersion () {
-    if (installedVersion === void 0) {
-      findMimeTypeAndVersion();
-    }
-
-    return installedVersion;
-  };
-
-  // Version 0.4.0.4 autoupdate was broken. We want to prompt
-  // for install on 0.4.0.4 or earlier. We're also including
-  // earlier versions just in case. Version 0.4.0.10 also
-  // had a broken updater, we'll treat that version the same
-  // way.
-  var hasBrokenUpdater = function () {
-    var _broken = getInstalledVersion() === '0.4.0.9' ||
-                  !versionGreaterThan(getInstalledVersion(), '0.4.0.4');
-
-    hasBrokenUpdater = function() { return _broken; };
-    return _broken;
-  };
-
-
-  AutoUpdater = function () {
-    var plugin;
-
-    var getControllerCurry = function getControllerFirstCurry (fn) {
-      return function() {
-        if (plugin) {
-          return fn(void 0, arguments);
-        }
-
-        PluginProxies.create({
-          mimeType: getInstallerMimeType(),
-          isVisible: 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 OTPlugin.
-    this.isOutOfDate = function () {
-      return versionGreaterThan(OTPlugin.meta.version, getInstalledVersion());
-    };
-
-    this.autoUpdate = getControllerCurry(function () {
-      var modal = OT.Dialogs.Plugin.updateInProgress(),
-          analytics = new OT.Analytics(),
-        payload = {
-          ieVersion: $.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)
-            });
-
-            plugin.destroy();
-
-            modal.close();
-            OT.Dialogs.Plugin.updateComplete().on({
-              reload: function() {
-                window.location.reload();
-              }
-            });
-          }),
-
-          error = curryCallAsync(function(errorCode, errorMessage, systemErrorCode) {
-            payload.errorCode = errorCode;
-            payload.systemErrorCode = systemErrorCode;
-
-            analytics.logEvent({
-              action: 'OTPluginAutoUpdate',
-              variation: 'Failure',
-              partnerId: OT.APIKEY,
-              payload: JSON.stringify(payload)
-            });
-
-            plugin.destroy();
-
-            modal.close();
-            var updateMessage = errorMessage + ' (' + errorCode +
-                                      '). Please restart your browser and try again.';
-
-            modal = OT.Dialogs.Plugin.updateComplete(updateMessage).on({
-              'reload': function() {
-                modal.close();
-              }
-            });
-
-            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(OTPlugin.pathToInstaller(), success, error, progress);
-    });
-
-    this.destroy = function() {
-      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) {
-      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('./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')
-
-/* global scope, shim, pluginIsReady:true, PluginProxies, AutoUpdater */
-/* export registerReadyListener, notifyReadyListeners, onDomReady */
-
-var readyCallbacks = [];
-
-var // jshint -W098
-    destroy = function destroy () {
-      PluginProxies.removeAll();
-    },
-
-    registerReadyListener = function registerReadyListener (callback) {
-      readyCallbacks.push(callback);
-    },
-
-    notifyReadyListeners = function notifyReadyListeners (err) {
-      var callback;
-
-      while ( (callback = readyCallbacks.pop()) && $.isFunction(callback) ) {
-        callback.call(OTPlugin, err);
-      }
-    },
-
-    onDomReady = function onDomReady () {
-      AutoUpdater.get(function(err, updater) {
-        if (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...
-        PluginProxies.createMediaCapturer(function(err) {
-          if (!err && (PluginProxies.mediaCapturer && !PluginProxies.mediaCapturer.isValid())) {
-            err = 'The TB Plugin failed to load properly';
-          }
-
-          pluginIsReady = true;
-          notifyReadyListeners(err);
-
-          $.onDOMUnload(destroy);
-        });
-      });
-    };
-
-// tb_require('./header.js')
-// tb_require('./shims.js')
 
 /* global scope:true */
 /* exported  createFrame */
 
-var createFrame = function createFrame (bodyContent, callbackId, callback) {
-  var Proto = function Frame () {},
+var createFrame = function createFrame(bodyContent, callbackId, callback) {
+  var Proto = function Frame() {},
       api = new Proto(),
       domElement = scope.document.createElement('iframe');
 
   domElement.id = 'OTPlugin_frame_' + $.uuid().replace(/\-+/g, '');
   domElement.style.border = '0';
 
   try {
     domElement.style.backgroundColor = 'rgba(0,0,0,0)';
@@ -7814,97 +7544,477 @@ var createFrame = function createFrame (
         domElement.contentWindow,
         doc
       );
     }
   };
 
   scope.document.body.appendChild(domElement);
 
-  if($.env.iframeNeedsLoad) {
+  if ($.env.iframeNeedsLoad) {
     if ($.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*/
     }
 
     $.on(domElement, 'load', wrappedCallback);
   } else {
     setTimeout(wrappedCallback, 0);
   }
 
-  api.reparent = function reparent (target) {
+  api.reparent = function reparent(target) {
     // document.adoptNode(domElement);
     target.appendChild(domElement);
   };
 
   api.element = domElement;
 
   return api;
 };
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
+// tb_require('./plugin_proxies.js')
+
+/* global OT:true, OTPlugin:true, ActiveXObject:true,
+          PluginProxies:true, curryCallAsync:true */
+
+/* exported AutoUpdater:true */
+var AutoUpdater;
+
+(function() {
+
+  var autoUpdaterController,
+      updaterMimeType,        // <- cached version, use getInstallerMimeType instead
+      installedVersion = -1;  // <- cached version, use getInstallerMimeType instead
+
+  var versionGreaterThan = function versionGreaterThan(version1, version2) {
+    if (version1 === version2) return false;
+    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('.'),
+        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.OpenTokPluginInstaller',
+        installPluginName = 'OpenTokPluginInstaller',
+        unversionedMimeType = 'application/x-opentokplugininstaller',
+        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-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) {
+
+          bits = extractVersion.exec(mimeType.type);
+
+          if (bits !== null && versionGreaterThan(bits[1], installedVersion)) {
+            installedVersion = bits[1];
+          }
+        }
+      }
+    } else if ($.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) {}
+    }
+
+    updaterMimeType = installedVersion !== -1 ?
+                              unversionedMimeType + ',version=' + installedVersion :
+                              null;
+  };
+
+  var getInstallerMimeType = function getInstallerMimeType() {
+    if (updaterMimeType === void 0) {
+      findMimeTypeAndVersion();
+    }
+
+    return updaterMimeType;
+  };
+
+  var getInstalledVersion = function getInstalledVersion() {
+    if (installedVersion === void 0) {
+      findMimeTypeAndVersion();
+    }
+
+    return installedVersion;
+  };
+
+  // Version 0.4.0.4 autoupdate was broken. We want to prompt
+  // for install on 0.4.0.4 or earlier. We're also including
+  // earlier versions just in case. Version 0.4.0.10 also
+  // had a broken updater, we'll treat that version the same
+  // way.
+  var hasBrokenUpdater = function() {
+    var _broken = getInstalledVersion() === '0.4.0.9' ||
+                  !versionGreaterThan(getInstalledVersion(), '0.4.0.4');
+
+    hasBrokenUpdater = function() { return _broken; };
+    return _broken;
+  };
+
+  AutoUpdater = function() {
+    var plugin;
+
+    var getControllerCurry = function getControllerFirstCurry(fn) {
+      return function() {
+        if (plugin) {
+          return fn(void 0, arguments);
+        }
+
+        PluginProxies.create({
+          mimeType: getInstallerMimeType(),
+          isVisible: 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 OTPlugin.
+    this.isOutOfDate = function() {
+      return versionGreaterThan(OTPlugin.meta.version, getInstalledVersion());
+    };
+
+    this.autoUpdate = getControllerCurry(function() {
+      var modal = OT.Dialogs.Plugin.updateInProgress(),
+          analytics = new OT.Analytics(),
+        payload = {
+          ieVersion: $.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)
+            });
+
+            plugin.destroy();
+
+            modal.close();
+            OT.Dialogs.Plugin.updateComplete().on({
+              reload: function() {
+                window.location.reload();
+              }
+            });
+          }),
+
+          error = curryCallAsync(function(errorCode, errorMessage, systemErrorCode) {
+            payload.errorCode = errorCode;
+            payload.systemErrorCode = systemErrorCode;
+
+            analytics.logEvent({
+              action: 'OTPluginAutoUpdate',
+              variation: 'Failure',
+              partnerId: OT.APIKEY,
+              payload: JSON.stringify(payload)
+            });
+
+            plugin.destroy();
+
+            modal.close();
+            var updateMessage = errorMessage + ' (' + errorCode +
+                                      '). Please restart your browser and try again.';
+
+            modal = OT.Dialogs.Plugin.updateComplete(updateMessage).on({
+              reload: function() {
+                modal.close();
+              }
+            });
+
+            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(OTPlugin.pathToInstaller(), success, error, progress);
+    });
+
+    this.destroy = function() {
+      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) {
+      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('./proxy.js')
+// tb_require('./auto_updater.js')
+
+/* global PluginProxies */
+/* exported MediaDevices  */
+
+// Exposes a enumerateDevices method and emits a devicechange event
+//
+// http://w3c.github.io/mediacapture-main/#idl-def-MediaDevices
+//
+var MediaDevices = function() {
+  var Proto = function MediaDevices() {},
+      api = new Proto();
+
+  api.enumerateDevices = function enumerateDevices(completion) {
+    OTPlugin.ready(function(error) {
+      if (error) {
+        completion(error);
+      } else {
+        PluginProxies.mediaCapturer.enumerateDevices(completion);
+      }
+    });
+  };
+
+  api.addListener = function addListener(fn, context) {
+    OTPlugin.ready(function(error) {
+      if (error) {
+        // No error message here, ready failing would have
+        // created a bunch elsewhere
+        return;
+      }
+
+      PluginProxies.mediaCapturer.on('devicesChanged', fn, context);
+    });
+  };
+
+  api.removeListener = function removeListener(fn, context) {
+    if (OTPlugin.isReady()) {
+      PluginProxies.mediaCapturer.off('devicesChanged', fn, context);
+    }
+  };
+
+  return api;
+};
+
+// 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')
+
+/* global scope, shim, pluginIsReady:true, PluginProxies, AutoUpdater */
+/* export registerReadyListener, notifyReadyListeners, onDomReady */
+
+var readyCallbacks = [],
+
+    // This stores the error from the load attempt. We use
+    // this if registerReadyListener gets called after a load
+    // attempt fails.
+    loadError;
+
+var // jshint -W098
+    destroy = function destroy() {
+      PluginProxies.removeAll();
+    },
+
+    registerReadyListener = function registerReadyListener(callback) {
+      if (!$.isFunction(callback)) {
+        OTPlugin.warn('registerReadyListener was called with something that was not a function.');
+        return;
+      }
+
+      if (OTPlugin.isReady()) {
+        callback.call(OTPlugin, loadError);
+      } else {
+        readyCallbacks.push(callback);
+      }
+    },
+
+    notifyReadyListeners = function notifyReadyListeners() {
+      var callback;
+
+      while ((callback = readyCallbacks.pop()) && $.isFunction(callback)) {
+        callback.call(OTPlugin, loadError);
+      }
+    },
+
+    onDomReady = function onDomReady() {
+      AutoUpdater.get(function(err, updater) {
+        if (err) {
+          loadError = 'Error while loading the AutoUpdater: ' + err;
+          notifyReadyListeners();
+          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...
+        PluginProxies.createMediaCapturer(function(err) {
+          loadError = err;
+
+          if (!loadError && (!PluginProxies.mediaCapturer ||
+                !PluginProxies.mediaCapturer.isValid())) {
+            loadError = 'The TB Plugin failed to load properly';
+          }
+
+          pluginIsReady = true;
+          notifyReadyListeners();
+
+          $.onDOMUnload(destroy);
+        });
+      });
+    };
+
+// 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('./media_devices.js')
+// tb_require('./video_container.js')
+// tb_require('./rumor.js')
 // tb_require('./lifecycle.js')
 
 /* global AutoUpdater,
           RumorSocket,
           MediaConstraints, PeerConnection, MediaStream,
           registerReadyListener,
-          PluginProxies */
-
-OTPlugin.isInstalled = function isInstalled () {
+          PluginProxies,
+          MediaDevices */
+
+OTPlugin.isInstalled = function isInstalled() {
   if (!this.isSupported()) return false;
   return AutoUpdater.isinstalled();
 };
 
-OTPlugin.version = function version () {
+OTPlugin.version = function version() {
   return OTPlugin.meta.version;
 };
 
-OTPlugin.installedVersion = function installedVersion () {
+OTPlugin.installedVersion = function installedVersion() {
   return AutoUpdater.installedVersion();
 };
 
 // Returns a URI to the OTPlugin installer that is paired with
 // this version of OTPlugin.js.
-OTPlugin.pathToInstaller = function pathToInstaller () {
+OTPlugin.pathToInstaller = function pathToInstaller() {
   return 'https://s3.amazonaws.com/otplugin.tokbox.com/v' +
-                    OTPlugin.meta.version + '/otiePluginMain.msi';
+                    OTPlugin.meta.version + '/OpenTokPluginMain.msi';
 };
 
 // Trigger +callback+ when the plugin is ready
 //
 // Most of the public API cannot be called until
 // the plugin is ready.
 //
-OTPlugin.ready = function ready (callback) {
-  if (OTPlugin.isReady()) {
-    var err;
-
-    if (!PluginProxies.mediaCapturer || !PluginProxies.mediaCapturer.isValid()) {
-      err = 'The TB Plugin failed to load properly';
-    }
-
-    callback.call(OTPlugin, err);
-  }
-  else {
-    registerReadyListener(callback);
-  }
+OTPlugin.ready = function ready(callback) {
+  registerReadyListener(callback);
 };
 
 // Helper function for OTPlugin.getUserMedia
 var _getUserMedia = function _getUserMedia(mediaConstraints, success, error) {
   PluginProxies.createMediaPeer(function(err, plugin) {
     if (err) {
       error.call(OTPlugin, err);
       return;
@@ -7914,23 +8024,22 @@ var _getUserMedia = function _getUserMed
       success.call(OTPlugin, MediaStream.fromJson(streamJson, plugin));
     }, error);
   });
 };
 
 // Equivalent to: window.getUserMedia(constraints, success, error);
 //
 // Except that the constraints won't be identical
-OTPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) {
+OTPlugin.getUserMedia = function getUserMedia(userConstraints, success, error) {
   var constraints = new MediaConstraints(userConstraints);
 
   if (constraints.screenSharing) {
     _getUserMedia(constraints, success, error);
-  }
-  else {
+  } else {
     var sources = [];
     if (constraints.hasVideo) sources.push('video');
     if (constraints.hasAudio) sources.push('audio');
 
     PluginProxies.mediaCapturer.selectSources(sources, function(captureDevices) {
       for (var key in captureDevices) {
         if (captureDevices.hasOwnProperty(key)) {
           OTPlugin.debug(key + ' Capture Device: ' + captureDevices[key]);
@@ -7941,34 +8050,43 @@ OTPlugin.getUserMedia = function getUser
       constraints.setVideoSource(captureDevices.video);
       constraints.setAudioSource(captureDevices.audio);
 
       _getUserMedia(constraints, success, error);
     }, error);
   }
 };
 
+OTPlugin.enumerateDevices = function(completion) {
+  OTPlugin.ready(function(error) {
+    if (error) {
+      completion(error);
+    } else {
+      PluginProxies.mediaCapturer.enumerateDevices(completion);
+    }
+  });
+};
+
 OTPlugin.initRumorSocket = function(messagingURL, completion) {
   OTPlugin.ready(function(error) {
-    if(error) {
+    if (error) {
       completion(error);
     } else {
       completion(null, new RumorSocket(PluginProxies.mediaCapturer, messagingURL));
     }
   });
 };
 
-
 // Equivalent to: var pc = new window.RTCPeerConnection(iceServers, options);
 //
 // Except that it is async and takes a completion handler
-OTPlugin.initPeerConnection = function initPeerConnection (iceServers,
-                                                           options,
-                                                           localStream,
-                                                           completion) {
+OTPlugin.initPeerConnection = function initPeerConnection(iceServers,
+                                                          options,
+                                                          localStream,
+                                                          completion) {
 
   var gotPeerObject = function(err, plugin) {
     if (err) {
       completion.call(OTPlugin, err);
       return;
     }
 
     OTPlugin.debug('Got PeerConnection for ' + plugin.id);
@@ -7986,49 +8104,117 @@ OTPlugin.initPeerConnection = function i
   // @fixme this is nasty and brittle. We need some way to use the same Object
   // for the PeerConnection that was used for the getUserMedia call (in the case
   // of publishers). We don't really have a way of implicitly associating them though.
   // Hence, publishers will have to pass through their localStream (if they have one)
   // and we will look up the original Object and use that. Otherwise we generate
   // a new one.
   if (localStream && localStream._.plugin) {
     gotPeerObject(null, localStream._.plugin);
-  }
-  else {
+  } else {
     PluginProxies.createMediaPeer(gotPeerObject);
   }
 };
 
 // A RTCSessionDescription like object exposed for native WebRTC compatability
-OTPlugin.RTCSessionDescription = function RTCSessionDescription (options) {
+OTPlugin.RTCSessionDescription = function RTCSessionDescription(options) {
   this.type = options.type;
   this.sdp = options.sdp;
 };
 
 // A RTCIceCandidate like object exposed for native WebRTC compatability
-OTPlugin.RTCIceCandidate = function RTCIceCandidate (options) {
+OTPlugin.RTCIceCandidate = function RTCIceCandidate(options) {
   this.sdpMid = options.sdpMid;
   this.sdpMLineIndex = parseInt(options.sdpMLineIndex, 10);
   this.candidate = options.candidate;
 };
 
+OTPlugin.mediaDevices = new MediaDevices();
+
+
 // tb_require('./api.js')
 
 /* global shim, onDomReady */
 
 shim();
 
 $.onDOMLoad(onDomReady);
 
 /* jshint ignore:start */
 })(this);
 /* jshint ignore:end */
 
 
 
+/* exported _setCertificates */
+
+var _setCertificates = function(pcConfig, completion) {
+  if (
+    OT.$.env.name === 'Firefox' &&
+    window.mozRTCPeerConnection &&
+    window.mozRTCPeerConnection.generateCertificate
+  ) {
+    window.mozRTCPeerConnection.generateCertificate({
+      name: 'RSASSA-PKCS1-v1_5',
+      hash: 'SHA-256',
+      modulusLength: 2048,
+      publicExponent: new Uint8Array([1, 0, 1])
+    }).catch(function(err) {
+      completion(err);
+    }).then(function(cert) {
+      pcConfig.certificates = [cert];
+      completion(undefined, pcConfig);
+    });
+  } else {
+    OT.$.callAsync(function() {
+      completion(undefined, pcConfig);
+    });
+  }
+};
+
+/* exported videoContentResizesMixin */
+
+var videoContentResizesMixin = function(self, domElement) {
+
+  var width = domElement.videoWidth,
+      height = domElement.videoHeight,
+      stopped = true;
+
+  function actor() {
+    if (stopped) {
+      return;
+    }
+    if (width !== domElement.videoWidth || height !== domElement.videoHeight) {
+      self.trigger('videoDimensionsChanged',
+        { width: width, height: height },
+        { width: domElement.videoWidth, height: domElement.videoHeight }
+      );
+      width = domElement.videoWidth;
+      height = domElement.videoHeight;
+    }
+    waiter();
+  }
+
+  function waiter() {
+    self.whenTimeIncrements(function() {
+      window.requestAnimationFrame(actor);
+    });
+  }
+
+  self.startObservingSize = function() {
+    stopped = false;
+    waiter();
+  };
+
+  self.stopObservingSize = function() {
+    stopped = true;
+  };
+
+};
+
 /* jshint ignore:start */
 !(function(window, OT) {
 /* jshint ignore:end */
 
 // tb_require('./header.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
@@ -8040,38 +8226,37 @@ if (location.protocol === 'file:') {
   /*global alert*/
   alert('You cannot test a page using WebRTC through the file system due to browser ' +
     'permissions. You must run it over a web server.');
 }
 
 var OT = window.OT || {};
 
 // Define the APIKEY this is a global parameter which should not change
-OT.APIKEY = (function(){
+OT.APIKEY = (function() {
   // Script embed
-  var scriptSrc = (function(){
+  var scriptSrc = (function() {
     var s = document.getElementsByTagName('script');
     s = s[s.length - 1];
     s = s.getAttribute('src') || s.src;
     return s;
   })();
 
   var m = scriptSrc.match(/[\?\&]apikey=([^&]+)/i);
   return m ? m[1] : '';
 })();
 
-
 if (!window.OT) window.OT = OT;
 if (!window.TB) window.TB = OT;
 
 // tb_require('../js/ot.js')
 
 OT.properties = {
-  version: 'v2.5.2',         // The current version (eg. v2.0.4) (This is replaced by gradle)
-  build: 'f4508e1',    // The current build hash (This is replaced by gradle)
+  version: 'v2.6.8',         // The current version (eg. v2.0.4) (This is replaced by gradle)
+  build: 'fae7901',    // 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',
@@ -8092,45 +8277,43 @@ OT.properties = {
   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')
-  }
-};
-
+    firefox: parseFloat('37'),
+    chrome: parseFloat('39')
+  }
+};
 
 // tb_require('../ot.js')
 // tb_require('../../conf/properties.js');
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT */
 
-
 // Mount OTHelpers on OT.$
 OT.$ = window.OTHelpers;
 
 // Allow events to be bound on OT
 OT.$.eventing(OT);
 
 // REMOVE THIS POST IE MERGE
 
 OT.$.defineGetters = function(self, getters, enumerable) {
   var propsDefinition = {};
 
   if (enumerable === void 0) enumerable = false;
 
   for (var key in getters) {
-    if(!getters.hasOwnProperty(key)) {
+    if (!getters.hasOwnProperty(key)) {
       continue;
     }
 
     propsDefinition[key] = {
       get: getters[key],
       enumerable: enumerable
     };
   }
@@ -8164,26 +8347,24 @@ OT.setLogLevel = function(level) {
   }
   OT.debug('OT.setLogLevel(' + retVal + ')');
   return retVal;
 };
 
 var debugTrue = OT.properties.debug === 'true' || OT.properties.debug === true;
 OT.setLogLevel(debugTrue ? OT.DEBUG : OT.ERROR);
 
-
 // Patch the userAgent to ref OTPlugin, if it's installed.
 if (OTPlugin && OTPlugin.isInstalled()) {
   OT.$.env.userAgent += '; OTPlugin ' + OTPlugin.version();
 }
 
 // @todo remove this
 OT.$.userAgent = function() { return OT.$.env.userAgent; };
 
-
 /**
   * 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
@@ -8248,16 +8429,132 @@ OT.$.userAgent = function() { return OT.
   * @name OT.log
   * @memberof OT
   * @function
   * @see <a href="#setLogLevel">OT.setLogLevel()</a>
   */
 
 // tb_require('../../../helpers/helpers.js')
 
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+// Rumor Messaging for JS
+//
+// https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork
+//
+// @todo Rumor {
+//     Add error codes for all the error cases
+//     Add Dependability commands
+// }
+
+OT.Rumor = {
+  MessageType: {
+    // This is used to subscribe to address/addresses. The address/addresses the
+    // client specifies here is registered on the server. Once any message is sent to
+    // that address/addresses, the client receives that message.
+    SUBSCRIBE: 0,
+
+    // This is used to unsubscribe to address / addresses. Once the client unsubscribe
+    // to an address, it will stop getting messages sent to that address.
+    UNSUBSCRIBE: 1,
+
+    // This is used to send messages to arbitrary address/ addresses. Messages can be
+    // anything and Rumor will not care about what is included.
+    MESSAGE: 2,
+
+    // This will be the first message that the client sends to the server. It includes
+    // the uniqueId for that client connection and a disconnect_notify address that will
+    // be notified once the client disconnects.
+    CONNECT: 3,
+
+    // This will be the message used by the server to notify an address that a
+    // client disconnected.
+    DISCONNECT: 4,
+
+    //Enhancements to support Keepalives
+    PING: 7,
+    PONG: 8,
+    STATUS: 9
+  }
+};
+
+// tb_require('../../../helpers/helpers.js')
+// tb_require('./rumor.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
+/* global OT, OTPlugin */
+
+!(function() {
+
+  OT.Rumor.PluginSocket = function(messagingURL, events) {
+
+    var webSocket,
+        state = 'initializing';
+
+    OTPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) {
+      if (err) {
+        state = 'closed';
+        events.onClose({ code: 4999 });
+      } else if (state === 'initializing') {
+        webSocket = rumorSocket;
+
+        webSocket.onOpen(function() {
+          state = 'open';
+          events.onOpen();
+        });
+        webSocket.onClose(function(error) {
+          state = 'closed'; /* CLOSED */
+          events.onClose({ code: error });
+        });
+        webSocket.onError(function(error) {
+          state = 'closed'; /* CLOSED */
+          events.onError(error);
+          /* native websockets seem to do this, so should we */
+          events.onClose({ code: error });
+        });
+
+        webSocket.onMessage(function(type, addresses, headers, payload) {
+          var msg = new OT.Rumor.Message(type, addresses, headers, payload);
+          events.onMessage(msg);
+        });
+
+        webSocket.open();
+      } else {
+        this.close();
+      }
+    }, this));
+
+    this.close = function() {
+      if (state === 'initializing' || state === 'closed') {
+        state = 'closed';
+        return;
+      }
+
+      webSocket.close(1000, '');
+    };
+
+    this.send = function(msg) {
+      if (state === 'open') {
+        webSocket.send(msg);
+      }
+    };
+
+    this.isClosed = function() {
+      return state === 'closed';
+    };
+
+  };
+
+}(this));
+
+// tb_require('../../../helpers/helpers.js')
+
 // https://code.google.com/p/stringencoding/
 // An implementation of http://encoding.spec.whatwg.org/#api
 // Modified by TokBox to remove all encoding support except for utf-8
 
 /**
  * @license  Copyright 2014 Joshua Bell
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -8274,21 +8571,21 @@ OT.$.userAgent = function() { return OT.
  *
  * Original source: https://github.com/inexorabletash/text-encoding
  ***/
 /*jshint unused:false*/
 
 (function(global) {
   'use strict';
 
-  if(OT.$.env && OT.$.env.name === 'IE' && OT.$.env.version < 10) {
+  if (OT.$.env && OT.$.env.name === 'IE' && OT.$.env.version < 10) {
     return; // IE 8 doesn't do websockets. No websockets, no encoding.
   }
 
-  if ( (global.TextEncoder !== void 0) && (global.TextDecoder !== void 0))  {
+  if ((global.TextEncoder !== void 0) && (global.TextDecoder !== void 0)) {
     // defer to the native ones
     return;
   }
 
   /* jshint ignore:start */
 
   //
   // Utilities
@@ -8308,17 +8605,16 @@ OT.$.userAgent = function() { return OT.
    * @param {number} n The numerator.
    * @param {number} d The denominator.
    * @return {number} The result of the integer division of n by d.
    */
   function div(n, d) {
     return Math.floor(n / d);
   }
 
-
   //
   // Implementation of Encoding specification
   // http://dvcs.w3.org/hg/encoding/raw-file/tip/Overview.html
   //
 
   //
   // 3. Terminology
   //
@@ -9304,157 +9600,41 @@ OT.$.userAgent = function() { return OT.
   global['TextEncoder'] = global['TextEncoder'] || TextEncoder;
   global['TextDecoder'] = global['TextDecoder'] || TextDecoder;
 
   /* jshint ignore:end */
 
 }(this));
 
 // tb_require('../../../helpers/helpers.js')
-
-/* jshint globalstrict: true, strict: false, undef: true, unused: true,
-          trailing: true, browser: true, smarttabs:true */
-/* global OT */
-
-// Rumor Messaging for JS
-//
-// https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork
-//
-// @todo Rumor {
-//     Add error codes for all the error cases
-//     Add Dependability commands
-// }
-
-OT.Rumor = {
-  MessageType: {
-    // This is used to subscribe to address/addresses. The address/addresses the
-    // client specifies here is registered on the server. Once any message is sent to
-    // that address/addresses, the client receives that message.
-    SUBSCRIBE: 0,
-
-    // This is used to unsubscribe to address / addresses. Once the client unsubscribe
-    // to an address, it will stop getting messages sent to that address.
-    UNSUBSCRIBE: 1,
-
-    // This is used to send messages to arbitrary address/ addresses. Messages can be
-    // anything and Rumor will not care about what is included.
-    MESSAGE: 2,
-
-    // This will be the first message that the client sends to the server. It includes
-    // the uniqueId for that client connection and a disconnect_notify address that will
-    // be notified once the client disconnects.
-    CONNECT: 3,
-
-    // This will be the message used by the server to notify an address that a
-    // client disconnected.
-    DISCONNECT: 4,
-
-    //Enhancements to support Keepalives
-    PING: 7,
-    PONG: 8,
-    STATUS: 9
-  }
-};
-
-// tb_require('../../../helpers/helpers.js')
-// tb_require('./rumor.js')
-
-/* jshint globalstrict: true, strict: false, undef: true, unused: true,
-          trailing: true, browser: true, smarttabs:true */
-/* global OT, OTPlugin */
-
-!(function() {
-
-  OT.Rumor.PluginSocket = function(messagingURL, events) {
-
-    var webSocket,
-        state = 'initializing';
-
-    OTPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) {
-      if(err) {
-        state = 'closed';
-        events.onClose({ code: 4999 });
-      } else if(state === 'initializing') {
-        webSocket = rumorSocket;
-
-        webSocket.onOpen(function() {
-          state = 'open';
-          events.onOpen();
-        });
-        webSocket.onClose(function(error) {
-          state = 'closed'; /* CLOSED */
-          events.onClose({ code: error });
-        });
-        webSocket.onError(function(error) {
-          state = 'closed'; /* CLOSED */
-          events.onError(error);
-          /* native websockets seem to do this, so should we */
-          events.onClose({ code: error });
-        });
-
-        webSocket.onMessage(function(type, addresses, headers, payload) {
-          var msg = new OT.Rumor.Message(type, addresses, headers, payload);
-          events.onMessage(msg);
-        });
-
-        webSocket.open();
-      } else {
-        this.close();
-      }
-    }, this));
-
-    this.close = function() {
-      if(state === 'initializing' || state === 'closed') {
-        state = 'closed';
-        return;
-      }
-
-      webSocket.close(1000, '');
-    };
-
-    this.send = function(msg) {
-      if(state === 'open') {
-        webSocket.send(msg);
-      }
-    };
-
-    this.isClosed = function() {
-      return state === 'closed';
-    };
-
-  };
-
-}(this));
-
-// tb_require('../../../helpers/helpers.js')
 // tb_require('./encoding.js')
 // tb_require('./rumor.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT, TextEncoder, TextDecoder */
 
 //
 //
 // @references
 // * https://tbwiki.tokbox.com/index.php/Rumor_Message_Packet
 // * https://tbwiki.tokbox.com/index.php/Rumor_Protocol
 //
-OT.Rumor.Message = function (type, toAddress, headers, data) {
+OT.Rumor.Message = function(type, toAddress, headers, data) {
   this.type = type;
   this.toAddress = toAddress;
   this.headers = headers;
   this.data = data;
 
   this.transactionId = this.headers['TRANSACTION-ID'];
   this.status = this.headers.STATUS;
   this.isError = !(this.status && this.status[0] === '2');
 };
 
-OT.Rumor.Message.prototype.serialize = function () {
+OT.Rumor.Message.prototype.serialize = function() {
   var offset = 8,
       cBuf = 7,
       address = [],
       headerKey = [],
       headerVal = [],
       strArray,
       dataView,
       i,
@@ -9473,17 +9653,17 @@ OT.Rumor.Message.prototype.serialize = f
 
   // The number of parameters
   cBuf++;
 
   // Write out the params
   i = 0;
 
   for (var key in this.headers) {
-    if(!this.headers.hasOwnProperty(key)) {
+    if (!this.headers.hasOwnProperty(key)) {
       continue;
     }
     headerKey.push(new TextEncoder('utf-8').encode(key));
     headerVal.push(new TextEncoder('utf-8').encode(this.headers[key]));
     cBuf += 4;
     cBuf += headerKey[i].length;
     cBuf += headerVal[i].length;
 
@@ -9555,19 +9735,19 @@ function toArrayBuffer(buffer) {
   var ab = new ArrayBuffer(buffer.length);
   var view = new Uint8Array(ab);
   for (var i = 0; i < buffer.length; ++i) {
     view[i] = buffer[i];
   }
   return ab;
 }
 
-OT.Rumor.Message.deserialize = function (buffer) {
-
-  if(typeof Buffer !== 'undefined' &&
+OT.Rumor.Message.deserialize = function(buffer) {
+
+  if (typeof Buffer !== 'undefined' &&
     Buffer.isBuffer(buffer)) {
     buffer = toArrayBuffer(buffer);
   }
   var cBuf = 0,
       type,
       offset = 8,
       uint8View = new Uint8Array(buffer),
       strView,
@@ -9615,40 +9795,39 @@ OT.Rumor.Message.deserialize = function 
   }
 
   var dataView = new Uint8Array(buffer, offset);
   var data = new TextDecoder('utf-8').decode(dataView);
 
   return new OT.Rumor.Message(type, address, headers, data);
 };
 
-
-OT.Rumor.Message.Connect = function (uniqueId, notifyDisconnectAddress) {
+OT.Rumor.Message.Connect = function(uniqueId, notifyDisconnectAddress) {
   var headers = {
     uniqueId: uniqueId,
     notifyDisconnectAddress: notifyDisconnectAddress
   };
 
   return new OT.Rumor.Message(OT.Rumor.MessageType.CONNECT, [], headers, '');
 };
 
-OT.Rumor.Message.Disconnect = function () {
+OT.Rumor.Message.Disconnect = function() {
   return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], {}, '');
 };
 
 OT.Rumor.Message.Subscribe = function(topics) {
   return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, {}, '');
 };
 
 OT.Rumor.Message.Unsubscribe = function(topics) {
   return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, {}, '');
 };
 
 OT.Rumor.Message.Publish = function(topics, message, headers) {
-  return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers||{}, message || '');
+  return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers || {}, message || '');
 };
 
 // This message is used to implement keepalives on the persistent
 // socket connection between the client and server. Every time the
 // client sends a PING to the server, the server will respond with
 // a PONG.
 OT.Rumor.Message.Ping = function() {
   return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], {}, '');
@@ -9692,26 +9871,26 @@ OT.Rumor.Message.Ping = function() {
       var msg = OT.Rumor.Message.deserialize(message.data);
       events.onMessage(msg);
     };
 
     // Ensure that the WebSocket send buffer is fully drained before disconnecting
     // the socket. If the buffer doesn't drain after a certain length of time
     // we give up and close it anyway.
     disconnectWhenSendBufferIsDrained =
-      function disconnectWhenSendBufferIsDrained (bufferDrainRetries) {
+      function disconnectWhenSendBufferIsDrained(bufferDrainRetries) {
       if (!webSocket) return;
 
       if (bufferDrainRetries === void 0) bufferDrainRetries = 0;
       if (bufferDrainTimeout) clearTimeout(bufferDrainTimeout);
 
       if (webSocket.bufferedAmount > 0 &&
         (bufferDrainRetries + 1) <= BUFFER_DRAIN_MAX_RETRIES) {
         bufferDrainTimeout = setTimeout(disconnectWhenSendBufferIsDrained,
-          BUFFER_DRAIN_INTERVAL, bufferDrainRetries+1);
+          BUFFER_DRAIN_INTERVAL, bufferDrainRetries + 1);
 
       } else {
         close();
       }
     };
 
     close = function close() {
       webSocket.close();
@@ -9730,36 +9909,36 @@ OT.Rumor.Message.Ping = function() {
     };
 
     this.isClosed = function() {
       return webSocket.readyState === 3;
     };
 
   };
 
-
 }(this));
 
 // tb_require('../../../helpers/helpers.js')
 // tb_require('./message.js')
 // tb_require('./native_socket.js')
 // tb_require('./plugin_socket.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT */
 
 var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000,
 
     // Magic Connectivity Timeout Constant: We wait 9*the keep alive interval,
     // on the third keep alive we trigger the timeout if we haven't received the
     // server pong.
-    WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5*WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100,
-
-    wsCloseErrorCodes;
+    WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5 * WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100,
+
+    wsCloseErrorCodes,
+    errorMap;
 
 // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes
 // http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html
 wsCloseErrorCodes = {
   1002:  'The endpoint is terminating the connection due to a protocol error. ' +
     '(CLOSE_PROTOCOL_ERROR)',
   1003:  'The connection is being terminated because the endpoint received data of ' +
     'a type it cannot accept (for example, a text-only endpoint received binary data). ' +
@@ -9778,116 +9957,134 @@ wsCloseErrorCodes = {
     'when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a ' +
     'need to hide specific details about the policy',
   1009: 'Indicates that an endpoint is terminating the connection because it has received a ' +
     'message that is too big for it to process',
   1011: 'Indicates that a server is terminating the connection because it encountered an ' +
     'unexpected condition that prevented it from fulfilling the request',
 
   // .... codes in the 4000-4999 range are available for use by applications.
-  4001:   'Connectivity loss was detected as it was too long since the socket received the ' +
-    'last PONG message'
+  4001: 'Connectivity loss was detected as it was too long since the socket received the ' +
+    'last PONG message',
+  4010: 'Timed out while waiting for the Rumor socket to connect.',
+  4020: 'Error code unavailable.',
+  4030: 'Exception was thrown during Rumor connection, possibly because of a blocked port.'
+};
+
+errorMap = {
+  CLOSE_PROTOCOL_ERROR: 1002,
+  CLOSE_UNSUPPORTED: 1003,
+  CLOSE_TOO_LARGE: 1004,
+  CLOSE_NO_STATUS: 1005,
+  CLOSE_ABNORMAL: 1006,
+  CLOSE_TIMEOUT: 4010,
+  CLOSE_FALLBACK_CODE: 4020,
+  CLOSE_CONNECT_EXCEPTION: 4030
 };
 
 OT.Rumor.SocketError = function(code, message) {
   this.code = code;
   this.message = message;
 };
 
 // The NativeSocket bit is purely to make testing simpler, it defaults to WebSocket
 // so in normal operation you would omit it.
 OT.Rumor.Socket = function(messagingURL, notifyDisconnectAddress, NativeSocket) {
 
+  var _this = this;
+
   var states = ['disconnected',  'error', 'connected', 'connecting', 'disconnecting'],
       webSocket,
       id,
       onOpen,
       onError,
       onClose,
       onMessage,
       connectCallback,
       connectTimeout,
       lastMessageTimestamp,         // The timestamp of the last message received
       keepAliveTimer;               // Timer for the connectivity checks
 
-
   //// Private API
   var stateChanged = function(newState) {
         switch (newState) {
           case 'disconnected':
           case 'error':
             webSocket = null;
             if (onClose) {
               var error;
-              if(hasLostConnectivity()) {
+              if (hasLostConnectivity()) {
                 error = new Error(wsCloseErrorCodes[4001]);
                 error.code = 4001;
               }
               onClose(error);
             }
             break;
         }
       },
 
       setState = OT.$.statable(this, states, 'disconnected', stateChanged),
 
-      validateCallback = function validateCallback (name, callback) {
-        if (callback === null || !OT.$.isFunction(callback) ) {
+      validateCallback = function validateCallback(name, callback) {
+        if (callback === null || !OT.$.isFunction(callback)) {
           throw new Error('The Rumor.Socket ' + name +
             ' callback must be a valid function or null');
         }
       },
 
-      error = OT.$.bind(function error (errorMessage) {
-        OT.error('Rumor.Socket: ' + errorMessage);
-
-        var socketError = new OT.Rumor.SocketError(null, errorMessage || 'Unknown Socket Error');
+      raiseError = function raiseError(code, extraDetail) {
+        code = code || errorMap.CLOSE_FALLBACK_CODE;
+
+        var messageFromCode = wsCloseErrorCodes[code] || 'No message available from code.';
+        var message = messageFromCode + (extraDetail ? ' ' + extraDetail : '');
+
+        OT.error('Rumor.Socket: ' + message);
+
+        var socketError = new OT.Rumor.SocketError(code, message);
 
         if (connectTimeout) clearTimeout(connectTimeout);
 
         setState('error');
 
-        if (this.previousState === 'connecting' && connectCallback) {
+        if (_this.previousState === 'connecting' && connectCallback) {
           connectCallback(socketError, void 0);
           connectCallback = null;
         }
 
         if (onError) onError(socketError);
-      }, this),
-
-      hasLostConnectivity = function hasLostConnectivity () {
+      },
+
+      hasLostConnectivity = function hasLostConnectivity() {
         if (!lastMessageTimestamp) return false;
 
         return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT;
       },
 
-      sendKeepAlive = OT.$.bind(function() {
-        if (!this.is('connected')) return;
-
-        if ( hasLostConnectivity() ) {
+      sendKeepAlive = function() {
+        if (!_this.is('connected')) return;
+
+        if (hasLostConnectivity()) {
           webSocketDisconnected({code: 4001});
-        }
-        else  {
+        } else {
           webSocket.send(OT.Rumor.Message.Ping());
           keepAliveTimer = setTimeout(sendKeepAlive, WEB_SOCKET_KEEP_ALIVE_INTERVAL);
         }
-      }, this),
+      },
 
       // Returns true if we think the DOM has been unloaded
       // It detects this by looking for the OT global, which
       // should always exist until the DOM is cleaned up.
-      isDOMUnloaded = function isDOMUnloaded () {
+      isDOMUnloaded = function isDOMUnloaded() {
         return !window.OT;
       };
 
-
   //// Private Event Handlers
-  var webSocketConnected = OT.$.bind(function webSocketConnected () {
+  var webSocketConnected = function webSocketConnected() {
         if (connectTimeout) clearTimeout(connectTimeout);
-        if (this.isNot('connecting')) {
+        if (_this.isNot('connecting')) {
           OT.debug('webSocketConnected reached in state other than connecting');
           return;
         }
 
         // Connect to Rumor by registering our connection id and the
         // app server address to notify if we disconnect.
         //
         // We don't need to wait for a reply to this message.
@@ -9900,91 +10097,92 @@ OT.Rumor.Socket = function(messagingURL,
         }
 
         if (onOpen) onOpen(id);
 
         keepAliveTimer = setTimeout(function() {
           lastMessageTimestamp = OT.$.now();
           sendKeepAlive();
         }, WEB_SOCKET_KEEP_ALIVE_INTERVAL);
-      }, this),
-
-      webSocketConnectTimedOut = function webSocketConnectTimedOut () {
+      },
+
+      webSocketConnectTimedOut = function webSocketConnectTimedOut() {
         var webSocketWas = webSocket;
-        error('Timed out while waiting for the Rumor socket to connect.');
+        raiseError(errorMap.CLOSE_TIMEOUT);
         // This will prevent a socket eventually connecting
         // But call it _after_ the error just in case any of
         // the callbacks fire synchronously, breaking the error
         // handling code.
         try {
           webSocketWas.close();
-        } catch(x) {}
-      },
-
-      webSocketError = function webSocketError () {},
+        } catch (x) {}
+      },
+
+      webSocketError = function webSocketError() {},
         // var errorMessage = 'Unknown Socket Error';
         // @fixme We MUST be able to do better than this!
 
         // All errors seem to result in disconnecting the socket, the close event
         // has a close reason and code which gives some error context. This,
         // combined with the fact that the errorEvent argument contains no
         // error info at all, means we'll delay triggering the error handlers
         // until the socket is closed.
         // error(errorMessage);
 
-      webSocketDisconnected = OT.$.bind(function webSocketDisconnected (closeEvent) {
+      webSocketDisconnected = function webSocketDisconnected(closeEvent) {
         if (connectTimeout) clearTimeout(connectTimeout);
         if (keepAliveTimer) clearTimeout(keepAliveTimer);
 
         if (isDOMUnloaded()) {
           // Sometimes we receive the web socket close event after
           // the DOM has already been partially or fully unloaded
           // if that's the case here then it's not really safe, or
           // desirable, to continue.
           return;
         }
 
         if (closeEvent.code !== 1000 && closeEvent.code !== 1001) {
-          var reason = closeEvent.reason || closeEvent.message;
-          if (!reason && wsCloseErrorCodes.hasOwnProperty(closeEvent.code)) {
-            reason = wsCloseErrorCodes[closeEvent.code];
-          }
-
-          error('Rumor Socket Disconnected: ' + reason);
-        }
-
-        if (this.isNot('error')) setState('disconnected');
-      }, this),
-
-      webSocketReceivedMessage = function webSocketReceivedMessage (msg) {
+          if (closeEvent.code) {
+            raiseError(closeEvent.code);
+          } else {
+            raiseError(
+              errorMap.CLOSE_FALLBACK_CODE,
+              closeEvent.reason || closeEvent.message
+            );
+          }
+        }
+
+        if (_this.isNot('error')) setState('disconnected');
+      },
+
+      webSocketReceivedMessage = function webSocketReceivedMessage(msg) {
         lastMessageTimestamp = OT.$.now();
 
         if (onMessage) {
           if (msg.type !== OT.Rumor.MessageType.PONG) {
             onMessage(msg);
           }
         }
       };
 
-
   //// Public API
 
-  this.publish = function (topics, message, headers) {
+  this.publish = function(topics, message, headers) {
     webSocket.send(OT.Rumor.Message.Publish(topics, message, headers));
   };
 
   this.subscribe = function(topics) {
     webSocket.send(OT.Rumor.Message.Subscribe(topics));
   };
 
   this.unsubscribe = function(topics) {
     webSocket.send(OT.Rumor.Message.Unsubscribe(topics));
   };
 
-  this.connect = function (connectionId, complete) {
+  this.connect = function(connectionId, complete) {
     if (this.is('connecting', 'connected')) {
       complete(new OT.Rumor.SocketError(null,
           'Rumor.Socket cannot connect when it is already connecting or connected.'));
       return;
     }
 
     id = connectionId;
     connectCallback = complete;
@@ -9996,57 +10194,52 @@ OT.Rumor.Socket = function(messagingURL,
     var events = {
       onOpen:    webSocketConnected,
       onClose:   webSocketDisconnected,
       onError:   webSocketError,
       onMessage: webSocketReceivedMessage
     };
 
     try {
-      if(typeof TheWebSocket !== 'undefined') {
+      if (typeof TheWebSocket !== 'undefined') {
         webSocket = new OT.Rumor.NativeSocket(TheWebSocket, messagingURL, events);
       } else {
         webSocket = new OT.Rumor.PluginSocket(messagingURL, events);
       }
 
       connectTimeout = setTimeout(webSocketConnectTimedOut, OT.Rumor.Socket.CONNECT_TIMEOUT);
-    }
-    catch(e) {
+    } catch (e) {
       OT.error(e);
 
-      // @todo add an actual error message
-      error('Could not connect to the Rumor socket, possibly because of a blocked port.');
+      raiseError(errorMap.CLOSE_CONNECT_EXCEPTION);
     }
   };
 
   this.disconnect = function(drainSocketBuffer) {
     if (connectTimeout) clearTimeout(connectTimeout);
     if (keepAliveTimer) clearTimeout(keepAliveTimer);
 
     if (!webSocket) {
       if (this.isNot('error')) setState('disconnected');
       return;
     }
 
     if (webSocket.isClosed()) {
       if (this.isNot('error')) setState('disconnected');
-    }
-    else {
+    } else {
       if (this.is('connected')) {
         // Look! We are nice to the rumor server ;-)
         webSocket.send(OT.Rumor.Message.Disconnect());
       }
 
       // Wait until the socket is ready to close
       webSocket.close(drainSocketBuffer);
     }
   };
 
-
-
   OT.$.defineProperties(this, {
     id: {
       get: function() { return id; }
     },
 
     onOpen: {
       set: function(callback) {
         validateCallback('onOpen', callback);
@@ -10083,17 +10276,16 @@ OT.Rumor.Socket = function(messagingURL,
       get: function() { return onMessage; }
     }
   });
 };
 
 // The number of ms to wait for the websocket to connect
 OT.Rumor.Socket.CONNECT_TIMEOUT = 15000;
 
-
 // tb_require('../../../helpers/helpers.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT */
 
 // Rumor Messaging for JS
 //
@@ -10192,132 +10384,126 @@ OT.Raptor = {
 
 // tb_require('../../../helpers/helpers.js')
 // tb_require('./raptor.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global OT */
 
-OT.Raptor.serializeMessage = function (message) {
+OT.Raptor.serializeMessage = function(message) {
   return JSON.stringify(message);
 };
 
-
 // Deserialising a Raptor message mainly means doing a JSON.parse on it.
 // We do decorate the final message with a few extra helper properies though.
 //
 // These include:
 // * typeName: A human readable version of the Raptor type. E.g. STREAM instead of 102
 // * actionName: A human readable version of the Raptor action. E.g. CREATE instead of 101
 // * signature: typeName and actionName combined. This is mainly for debugging. E.g. A type
 //    of 102 and an action of 101 would result in a signature of "STREAM:CREATE"
 //
-OT.Raptor.deserializeMessage = function (msg) {
+OT.Raptor.deserializeMessage = function(msg) {
   if (msg.length === 0) return {};
 
   var message = JSON.parse(msg),
       bits = message.uri.substr(1).split('/');
 
   // Remove the Raptor protocol version
   bits.shift();
-  if (bits[bits.length-1] === '') bits.pop();
+  if (bits[bits.length - 1] === '') bits.pop();
 
   message.params = {};
-  for (var i=0, numBits=bits.length ; i<numBits-1; i+=2) {
-    message.params[bits[i]] = bits[i+1];
+  for (var i = 0, numBits = bits.length ; i < numBits - 1; i += 2) {
+    message.params[bits[i]] = bits[i + 1];
   }
 
   // extract the resource name. We special case 'channel' slightly, as
   // 'subscriber_channel' or 'stream_channel' is more useful for us
   // than 'channel' alone.
   if (bits.length % 2 === 0) {
-    if (bits[bits.length-2] === 'channel' && bits.length > 6) {
-      message.resource = bits[bits.length-4] + '_' + bits[bits.length-2];
-    } else {
-      message.resource = bits[bits.length-2];
-    }
-  }
-  else {
-    if (bits[bits.length-1] === 'channel' && bits.length > 5) {
-      message.resource = bits[bits.length-3] + '_' + bits[bits.length-1];
-    } else {
-      message.resource = bits[bits.length-1];
+    if (bits[bits.length - 2] === 'channel' && bits.length > 6) {
+      message.resource = bits[bits.length - 4] + '_' + bits[bits.length - 2];
+    } else {
+      message.resource = bits[bits.length - 2];
+    }
+  } else {
+    if (bits[bits.length - 1] === 'channel' && bits.length > 5) {
+      message.resource = bits[bits.length - 3] + '_' + bits[bits.length - 1];
+    } else {
+      message.resource = bits[bits.length - 1];
     }
   }
 
   message.signature = message.resource + '#' + message.method;
   return message;
 };
 
-OT.Raptor.unboxFromRumorMessage = function (rumorMessage) {
+OT.Raptor.unboxFromRumorMessage = function(rumorMessage) {
   var message = OT.Raptor.deserializeMessage(rumorMessage.data);
   message.transactionId = rumorMessage.transactionId;
   message.fromAddress = rumorMessage.headers['X-TB-FROM-ADDRESS'];
 
   return message;
 };
 
-OT.Raptor.parseIceServers = function (message) {
+OT.Raptor.parseIceServers = function(message) {
   try {
     return JSON.parse(message.data).content.iceServers;
   } catch (e) {
     return [];
   }
 };
 
 OT.Raptor.Message = {};
 
-
-OT.Raptor.Message.offer = function (uri, offerSdp) {
+OT.Raptor.Message.offer = function(uri, offerSdp) {
   return OT.Raptor.serializeMessage({
     method: 'offer',
     uri: uri,
     content: {
       sdp: offerSdp
     }
   });
 };
 
-
 OT.Raptor.Message.connections = {};
 
-OT.Raptor.Message.connections.create = function (apiKey, sessionId, connectionId) {
+OT.Raptor.Message.connections.create = function(apiKey, sessionId, connectionId) {
   return OT.Raptor.serializeMessage({
     method: 'create',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
     content: {
       userAgent: OT.$.env.userAgent
     }
   });
 };
 
-OT.Raptor.Message.connections.destroy = function (apiKey, sessionId, connectionId) {
+OT.Raptor.Message.connections.destroy = function(apiKey, sessionId, connectionId) {
   return OT.Raptor.serializeMessage({
     method: 'delete',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId,
     content: {}
   });
 };
 
-
 OT.Raptor.Message.sessions = {};
 
-OT.Raptor.Message.sessions.get = function (apiKey, sessionId) {
+OT.Raptor.Message.sessions.get = function(apiKey, sessionId) {
   return OT.Raptor.serializeMessage({
     method: 'read',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId,
     content: {}
   });
 };
 
-
 OT.Raptor.Message.streams = {};
 
-OT.Raptor.Message.streams.get = function (apiKey, sessionId, streamId) {
+OT.Raptor.Message.streams.get = function(apiKey, sessionId, streamId) {
   return OT.Raptor.serializeMessage({
     method: 'read',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
     content: {}
   });
 };
 
 OT.Raptor.Message.streams.channelFromOTChannel = function(channel) {
@@ -10336,17 +10522,17 @@ OT.Raptor.Message.streams.channelFromOTC
       raptorChannel.source = channel.source;
     }
     raptorChannel.fitMode = channel.fitMode;
   }
 
   return raptorChannel;
 };
 
-OT.Raptor.Message.streams.create = function (apiKey, sessionId, streamId, name,
+OT.Raptor.Message.streams.create = function(apiKey, sessionId, streamId, name,
   audioFallbackEnabled, channels, minBitrate, maxBitrate) {
   var messageContent = {
     id: streamId,
     name: name,
     audioFallbackEnabled: audioFallbackEnabled,
     channel: OT.$.map(channels, function(channel) {
       return OT.Raptor.Message.streams.channelFromOTChannel(channel);
     })
@@ -10357,59 +10543,57 @@ OT.Raptor.Message.streams.create = funct
 
   return OT.Raptor.serializeMessage({
     method: 'create',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
     content: messageContent
   });
 };
 
-OT.Raptor.Message.streams.destroy = function (apiKey, sessionId, streamId) {
+OT.Raptor.Message.streams.destroy = function(apiKey, sessionId, streamId) {
   return OT.Raptor.serializeMessage({
     method: 'delete',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
     content: {}
   });
 };
 
-
-OT.Raptor.Message.streams.answer = function (apiKey, sessionId, streamId, answerSdp) {
+OT.Raptor.Message.streams.answer = function(apiKey, sessionId, streamId, answerSdp) {
   return OT.Raptor.serializeMessage({
     method: 'answer',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
     content: {
       sdp: answerSdp
     }
   });
 };
 
-OT.Raptor.Message.streams.candidate = function (apiKey, sessionId, streamId, candidate) {
+OT.Raptor.Message.streams.candidate = function(apiKey, sessionId, streamId, candidate) {
   return OT.Raptor.serializeMessage({
     method: 'candidate',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId,
     content: candidate
   });
 };
 
 OT.Raptor.Message.streamChannels = {};
 OT.Raptor.Message.streamChannels.update =
-  function (apiKey, sessionId, streamId, channelId, attributes) {
+  function(apiKey, sessionId, streamId, channelId, attributes) {
   return OT.Raptor.serializeMessage({
     method: 'update',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' +
       streamId + '/channel/' + channelId,
     content: attributes
   });
 };
 
-
 OT.Raptor.Message.subscribers = {};
 
 OT.Raptor.Message.subscribers.create =
-  function (apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) {
+  function(apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) {
   var content = {
     id: subscriberId,
     connection: connectionId,
     keyManagementMethod: OT.$.supportedCryptoScheme(),
     bundleSupport: OT.$.hasCapabilities('bundle'),
     rtcpMuxSupport: OT.$.hasCapabilities('RTCPMux')
   };
   if (channelsToSubscribeTo) content.channel = channelsToSubscribeTo;
@@ -10417,117 +10601,111 @@ OT.Raptor.Message.subscribers.create =
   return OT.Raptor.serializeMessage({
     method: 'create',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
       '/stream/' + streamId + '/subscriber/' + subscriberId,
     content: content
   });
 };
 
-OT.Raptor.Message.subscribers.destroy = function (apiKey, sessionId, streamId, subscriberId) {
+OT.Raptor.Message.subscribers.destroy = function(apiKey, sessionId, streamId, subscriberId) {
   return OT.Raptor.serializeMessage({
     method: 'delete',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
       '/stream/' + streamId + '/subscriber/' + subscriberId,
     content: {}
   });
 };
 
 OT.Raptor.Message.subscribers.update =
-  function (apiKey, sessionId, streamId, subscriberId, attributes) {
+  function(apiKey, sessionId, streamId, subscriberId, attributes) {
   return OT.Raptor.serializeMessage({
     method: 'update',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
     '/stream/' + streamId + '/subscriber/' + subscriberId,
     content: attributes
   });
 };
 
-
 OT.Raptor.Message.subscribers.candidate =
-  function (apiKey, sessionId, streamId, subscriberId, candidate) {
+  function(apiKey, sessionId, streamId, subscriberId, candidate) {
   return OT.Raptor.serializeMessage({
     method: 'candidate',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
       '/stream/' + streamId + '/subscriber/' + subscriberId,
     content: candidate
   });
 };
 
-
 OT.Raptor.Message.subscribers.answer =
-  function (apiKey, sessionId, streamId, subscriberId, answerSdp) {
+  function(apiKey, sessionId, streamId, subscriberId, answerSdp) {
   return OT.Raptor.serializeMessage({
     method: 'answer',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
     '/stream/' + streamId + '/subscriber/' + subscriberId,
     content: {
       sdp: answerSdp
     }
   });
 };
 
-
 OT.Raptor.Message.subscriberChannels = {};
 
 OT.Raptor.Message.subscriberChannels.update =
-  function (apiKey, sessionId, streamId, subscriberId, channelId, attributes) {
+  function(apiKey, sessionId, streamId, subscriberId, channelId, attributes) {
   return OT.Raptor.serializeMessage({
     method: 'update',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
     '/stream/' + streamId + '/subscriber/' + subscriberId + '/channel/' + channelId,
     content: attributes
   });
 };
 
-
 OT.Raptor.Message.signals = {};
 
-OT.Raptor.Message.signals.create = function (apiKey, sessionId, toAddress, type, data) {
+OT.Raptor.Message.signals.create = function(apiKey, sessionId, toAddress, type, data) {
   var content = {};
   if (type !== void 0) content.type = type;
   if (data !== void 0) content.data = data;
 
   return OT.Raptor.serializeMessage({
     method: 'signal',
     uri: '/v2/partner/' + apiKey + '/session/' + sessionId +
       (toAddress !== void 0 ? '/connection/' + toAddress : '') + '/signal/' + OT.$.uuid(),
     content: content
   });
 };
 
 // tb_require('../../../helpers/helpers.js')
 // tb_require('./message.js')
 
-
 !(function() {
   /* jshint globalstrict: true, strict: false, undef: true, unused: true,
             trailing: true, browser: true, smarttabs:true */
   /* global OT */
 
   // Connect error codes and reasons that Raptor can return.
   var connectErrorReasons;
 
   connectErrorReasons = {
     409: 'This P2P session already has 2 participants.',
     410: 'The session already has four participants.',
     1004: 'The token passed is invalid.'
   };
 
-
-  OT.Raptor.Dispatcher = function () {
+  OT.Raptor.Dispatcher = function() {
     OT.$.eventing(this, true);
     this.callbacks = {};
   };
 
-  OT.Raptor.Dispatcher.prototype.registerCallback = function (transactionId, completion) {
+  OT.Raptor.Dispatcher.prototype.registerCallback = function(transactionId, completion) {
     this.callbacks[transactionId] = completion;
   };
 
-  OT.Raptor.Dispatcher.prototype.triggerCallback = function (transactionId) {
+  OT.Raptor.Dispatcher.prototype.triggerCallback = function(transactionId) {
     /*, arg1, arg2, argN-1, argN*/
     if (!transactionId) return;
 
     var completion = this.callbacks[transactionId];
 
     if (completion && OT.$.isFunction(completion)) {
       var args = Array.prototype.slice.call(arguments);
       args.shift();
@@ -10537,17 +10715,16 @@ OT.Raptor.Message.signals.create = funct
 
     delete this.callbacks[transactionId];
   };
 
   OT.Raptor.Dispatcher.prototype.onClose = function(reason) {
     this.emit('close', reason);
   };
 
-
   OT.Raptor.Dispatcher.prototype.dispatch = function(rumorMessage) {
     // The special casing of STATUS messages is ugly. Need to think about
     // how to better integrate this.
 
     if (rumorMessage.type === OT.Rumor.MessageType.STATUS) {
       OT.debug('OT.Raptor.dispatch: STATUS');
       OT.debug(rumorMessage);
 
@@ -10561,17 +10738,17 @@ OT.Raptor.Message.signals.create = funct
 
       return;
     }
 
     var message = OT.Raptor.unboxFromRumorMessage(rumorMessage);
     OT.debug('OT.Raptor.dispatch ' + message.signature);
     OT.debug(rumorMessage.data);
 
-    switch(message.resource) {
+    switch (message.resource) {
       case 'session':
         this.dispatchSession(message);
         break;
 
       case 'connection':
         this.dispatchConnection(message);
         break;
 
@@ -10599,79 +10776,75 @@ OT.Raptor.Message.signals.create = funct
         this.dispatchArchive(message);
         break;
 
       default:
         OT.warn('OT.Raptor.dispatch: Type ' + message.resource + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchSession = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchSession = function(message) {
     switch (message.method) {
       case 'read':
         this.emit('session#read', message.content, message.transactionId);
         break;
 
-
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchConnection = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchConnection = function(message) {
 
     switch (message.method) {
       case 'created':
         this.emit('connection#created', message.content);
         break;
 
-
       case 'deleted':
         this.emit('connection#deleted', message.params.connection, message.reason);
         break;
 
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchStream = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchStream = function(message) {
 
     switch (message.method) {
       case 'created':
         this.emit('stream#created', message.content, message.transactionId);
         break;
 
       case 'deleted':
         this.emit('stream#deleted', message.params.stream,
           message.reason);
         break;
 
-
       case 'updated':
         this.emit('stream#updated', message.params.stream,
           message.content);
         break;
 
-
       // The JSEP process
       case 'generateoffer':
       case 'answer':
       case 'pranswer':
       case 'offer':
       case 'candidate':
         this.dispatchJsep(message.method, message);
         break;
 
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchStreamChannel = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchStreamChannel = function(message) {
     switch (message.method) {
       case 'updated':
         this.emit('streamChannel#updated', message.params.stream,
           message.params.channel, message.content);
         break;
 
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
@@ -10689,80 +10862,74 @@ OT.Raptor.Message.signals.create = funct
   // and requirements.
   //
   // pranswer:
   // a provisional answer, i.e. not the final one.
   //
   // candidate
   //
   //
-  OT.Raptor.Dispatcher.prototype.dispatchJsep = function (method, message) {
+  OT.Raptor.Dispatcher.prototype.dispatchJsep = function(method, message) {
     this.emit('jsep#' + method, message.params.stream, message.fromAddress, message);
   };
 
-
-  OT.Raptor.Dispatcher.prototype.dispatchSubscriberChannel = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchSubscriberChannel = function(message) {
     switch (message.method) {
       case 'updated':
         this.emit('subscriberChannel#updated', message.params.stream,
           message.params.channel, message.content);
         break;
 
-
       case 'update': // subscriberId, streamId, content
         this.emit('subscriberChannel#update', message.params.subscriber,
           message.params.stream, message.content);
         break;
 
-
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchSubscriber = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchSubscriber = function(message) {
     switch (message.method) {
       case 'created':
         this.emit('subscriber#created', message.params.stream, message.fromAddress,
           message.content.id);
         break;
 
-
       case 'deleted':
         this.dispatchJsep('unsubscribe', message);
         this.emit('subscriber#deleted', message.params.stream,
           message.fromAddress);
         break;
 
-
       // The JSEP process
       case 'generateoffer':
       case 'answer':
       case 'pranswer':
       case 'offer':
       case 'candidate':
         this.dispatchJsep(message.method, message);
         break;
 
-
       default:
         OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
     }
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchSignal = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchSignal = function(message) {
     if (message.method !== 'signal') {
       OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented');
       return;
     }
     this.emit('signal', message.fromAddress, message.content.type,
       message.content.data);
   };
 
-  OT.Raptor.Dispatcher.prototype.dispatchArchive = function (message) {
+  OT.Raptor.Dispatcher.prototype.dispatchArchive = function(message) {
     switch (message.method) {
       case 'created':
         this.emit('archive#created', message.content);
         break;
 
       case 'updated':
         this.emit('archive#updated', message.params.archive, message.content);
         break;
@@ -10788,56 +10955,56 @@ OT.Raptor.Message.signals.create = funct
 
   function parseStream(dict, session) {
     var channel = dict.channel.map(function(channel) {
       return new OT.StreamChannel(channel);
     });
 
     var connectionId = dict.connectionId ? dict.connectionId : dict.connection.id;
 
-    return  new OT.Stream(  dict.id,
-                            dict.name,
-                            dict.creationTime,
-                            session.connections.get(connectionId),
-                            session,
-                            channel );
+    return new OT.Stream(dict.id,
+                         dict.name,
+                         dict.creationTime,
+                         session.connections.get(connectionId),
+                         session,
+                         channel);
   }
 
   function parseAndAddStreamToSession(dict, session) {
     if (session.streams.has(dict.id)) return;
 
     var stream = parseStream(dict, session);
-    session.streams.add( stream );
+    session.streams.add(stream);
 
     return stream;
   }
 
   function parseArchive(dict) {
-    return new OT.Archive( dict.id,
-                           dict.name,
-                           dict.status );
+    return new OT.Archive(dict.id,
+                          dict.name,
+                          dict.status);
   }
 
   function parseAndAddArchiveToSession(dict, session) {
     if (session.archives.has(dict.id)) return;
 
     var archive = parseArchive(dict);
     session.archives.add(archive);
 
     return archive;
   }
 
-  var DelayedEventQueue = function DelayedEventQueue (eventDispatcher) {
+  var DelayedEventQueue = function DelayedEventQueue(eventDispatcher) {
     var queue = [];
 
-    this.enqueue = function enqueue (/* arg1, arg2, ..., argN */) {
-      queue.push( Array.prototype.slice.call(arguments) );
-    };
-
-    this.triggerAll = function triggerAll () {
+    this.enqueue = function enqueue(/* arg1, arg2, ..., argN */) {
+      queue.push(Array.prototype.slice.call(arguments));
+    };
+
+    this.triggerAll = function triggerAll() {
       var event;
 
       // Array.prototype.shift is actually pretty inefficient for longer Arrays,
       // this is because after the first element is removed it reshuffles every
       // remaining element up one (1). This involves way too many allocations and
       // deallocations as the queue size increases.
       //
       // A more efficient version could be written by keeping an index to the current
@@ -10850,41 +11017,41 @@ OT.Raptor.Message.signals.create = funct
       //
       // TLDR: Array.prototype.shift is O(n), where n is the array length,
       // instead of the expected O(1). You can implement your own shift that runs
       // in amortised constant time.
       //
       // @todo benchmark and see if we should actually care about shift's performance
       // for our common queue sizes.
       //
-      while( (event = queue.shift()) ) {
+      while ((event = queue.shift())) {
         eventDispatcher.trigger.apply(eventDispatcher, event);
       }
     };
   };
 
   var DelayedSessionEvents = function(dispatcher) {
     var eventQueues = {};
 
-    this.enqueue = function enqueue (/* key, arg1, arg2, ..., argN */) {
+    this.enqueue = function enqueue(/* key, arg1, arg2, ..., argN */) {
       var key = arguments[0];
       var eventArgs = Array.prototype.slice.call(arguments, 1);
       if (!eventQueues[key]) {
         eventQueues[key] = new DelayedEventQueue(dispatcher);
       }
       eventQueues[key].enqueue.apply(eventQueues[key], eventArgs);
     };
 
-    this.triggerConnectionCreated = function triggerConnectionCreated (connection) {
+    this.triggerConnectionCreated = function triggerConnectionCreated(connection) {
       if (eventQueues['connectionCreated' + connection.id]) {
         eventQueues['connectionCreated' + connection.id].triggerAll();
       }
     };
 
-    this.triggerSessionConnected = function triggerSessionConnected (connections) {
+    this.triggerSessionConnected = function triggerSessionConnected(connections) {
       if (eventQueues.sessionConnected) {
         eventQueues.sessionConnected.triggerAll();
       }
 
       OT.$.forEach(connections, function(connection) {
         this.triggerConnectionCreated(connection);
       });
     };
@@ -10907,41 +11074,41 @@ OT.Raptor.Message.signals.create = funct
       }
 
       if (connection.destroyedReason()) {
         OT.debug('OT.Raptor.Socket: Socket was closed but the connection had already ' +
           'been destroyed. Reason: ' + connection.destroyedReason());
         return;
       }
 
-      connection.destroy( reason );
+      connection.destroy(reason);
     });
     
     // This method adds connections to the session both on a connection#created and
     // on a session#read. In the case of session#read sessionRead is set to true and
     // we include our own connection.
-    var addConnection = function (connection, sessionRead) {
+    var addConnection = function(connection, sessionRead) {
       connection = OT.Connection.fromHash(connection);
       if (sessionRead || session.connection && connection.id !== session.connection.id) {
-        session.connections.add( connection );
+        session.connections.add(connection);
         delayedSessionEvents.triggerConnectionCreated(connection);
       }
 
       OT.$.forEach(OT.$.keys(unconnectedStreams), function(streamId) {
         var stream = unconnectedStreams[streamId];
         if (stream && connection.id === stream.connection.id) {
           // dispatch streamCreated event now that the connectionCreated has been dispatched
           parseAndAddStreamToSession(stream, session);
           delete unconnectedStreams[stream.id];
           
           var payload = {
             debug: sessionRead ? 'connection came in session#read' :
               'connection came in connection#created',
-            streamId : stream.id,
-            connectionId : connection.id
+            streamId: stream.id,
+            connectionId: connection.id
           };
           session.logEvent('streamCreated', 'warning', payload);
         }
       });
       
       return connection;
     };
 
@@ -10955,21 +11122,21 @@ OT.Raptor.Message.signals.create = funct
       state.archives = [];
 
       OT.$.forEach(content.connection, function(connectionParams) {
         connection = addConnection(connectionParams, true);
         state.connections.push(connection);
       });
 
       OT.$.forEach(content.stream, function(streamParams) {
-        state.streams.push( parseAndAddStreamToSession(streamParams, session) );
+        state.streams.push(parseAndAddStreamToSession(streamParams, session));
       });
 
       OT.$.forEach(content.archive || content.archives, function(archiveParams) {
-        state.archives.push( parseAndAddArchiveToSession(archiveParams, session) );
+        state.archives.push(parseAndAddArchiveToSession(archiveParams, session));
       });
 
       session._.subscriberMap = {};
 
       dispatcher.triggerCallback(transactionId, null, state);
 
       sessionStateReceived = true;
       delayedSessionEvents.triggerSessionConnected(session.connections);
@@ -10987,18 +11154,18 @@ OT.Raptor.Message.signals.create = funct
     dispatcher.on('stream#created', function(stream, transactionId) {
       var connectionId = stream.connectionId ? stream.connectionId : stream.connection.id;
       if (session.connections.has(connectionId)) {
         stream = parseAndAddStreamToSession(stream, session);
       } else {
         unconnectedStreams[stream.id] = stream;
 
         var payload = {
-          debug : 'eventOrderError -- streamCreated event before connectionCreated',
-          streamId : stream.id,
+          debug: 'eventOrderError -- streamCreated event before connectionCreated',
+          streamId: stream.id
         };
         session.logEvent('streamCreated', 'warning', payload);
       }
 
       if (stream.publisher) {
         stream.publisher.setStream(stream);
       }
 
@@ -11067,55 +11234,52 @@ OT.Raptor.Message.signals.create = funct
       switch (method) {
         // Messages for Subscribers
         case 'offer':
           actors = [];
           var subscriber = OT.subscribers.find({streamId: streamId});
           if (subscriber) actors.push(subscriber);
           break;
 
-
         // Messages for Publishers
         case 'answer':
         case 'pranswer':
         case 'generateoffer':
         case 'unsubscribe':
           actors = OT.publishers.where({streamId: streamId});
           break;
 
-
         // Messages for Publishers and Subscribers
         case 'candidate':
           // send to whichever of your publisher or subscribers are
           // subscribing/publishing that stream
           actors = OT.publishers.where({streamId: streamId})
             .concat(OT.subscribers.where({streamId: streamId}));
           break;
 
-
         default:
           OT.warn('OT.Raptor.dispatch: jsep#' + method +
             ' is not currently implemented');
           return;
       }
 
       if (actors.length === 0) return;
 
       // This is a bit hacky. We don't have the session in the message so we iterate
       // until we find the actor that the message relates to this stream, and then
       // we grab the session from it.
       fromConnection = actors[0].session.connections.get(fromAddress);
-      if(!fromConnection && fromAddress.match(/^symphony\./)) {
+      if (!fromConnection && fromAddress.match(/^symphony\./)) {
         fromConnection = OT.Connection.fromHash({
           id: fromAddress,
           creationTime: Math.floor(OT.$.now())
         });
 
         actors[0].session.connections.add(fromConnection);
-      } else if(!fromConnection) {
+      } else if (!fromConnection) {
         OT.warn('OT.Raptor.dispatch: Messsage comes from a connection (' +
           fromAddress + ') that we do not know about. The message was ignored.');
         return;
       }
 
       OT.$.forEach(actors, function(actor) {
         actor.processMessage(method, fromConnection, message);
       });
@@ -11369,128 +11533,352 @@ function httpTest(config) {
 }
 
 OT.httpTest = httpTest;
 
 // tb_require('../../helpers/helpers.js')
 
 /* exported SDPHelpers */
 
+var START_MEDIA_SSRC = 10000,
+    START_RTX_SSRC = 20000;
+
 // Here are the structure of the rtpmap attribute and the media line, most of the
 // complex Regular Expressions in this code are matching against one of these two
 // formats:
 // * a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
 // * m=<media> <port>/<number of ports> <proto> <fmts>
 //
 // References:
 // * https://tools.ietf.org/html/rfc4566
 // * http://en.wikipedia.org/wiki/Session_Description_Protocol
 //
-var SDPHelpers = {
-  // Search through sdpLines to find the Media Line of type +mediaType+.
-  getMLineIndex: function getMLineIndex(sdpLines, mediaType) {
-    var targetMLine = 'm=' + mediaType;
-
-    // Find the index of the media line for +type+
-    return OT.$.findIndex(sdpLines, function(line) {
-      if (line.indexOf(targetMLine) !== -1) {
-        return true;
-      }
-
-      return false;
-    });
-  },
-
-  // Extract the payload types for a give Media Line.
-  //
-  getMLinePayloadTypes: function getMLinePayloadTypes (mediaLine, mediaType) {
-    var mLineSelector = new RegExp('^m=' + mediaType +
-                          ' \\d+(/\\d+)? [a-zA-Z0-9/]+(( [a-zA-Z0-9/]+)+)$', 'i');
-