Bug 1210865 - Update OpenTok library to version 2.6.8. r=dmose
authorMark Banner <standard8@mozilla.com>
Tue, 10 Nov 2015 08:58:10 +0000
changeset 271926 aa429ba5cf28756f3ef2dcd55a711446dbebee19
parent 271925 21f52f8344413fc44a1d5815272662179b6c09f4
child 271927 0823cc0681929bc74c4d248c7b894a2d0a55c055
push id29658
push usercbook@mozilla.com
push dateWed, 11 Nov 2015 11:08:38 +0000
treeherdermozilla-central@754119dfb99f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose
bugs1210865
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1210865 - Update OpenTok library to version 2.6.8. r=dmose
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) {
 }