Bug 1131568 - Update the OpenTok SDK to version 2.5.0. r=Standard8, a=lsblakk
authorMike de Boer <mdeboer@mozilla.com>
Fri, 06 Mar 2015 08:28:16 +0000
changeset 248119 4037672fcc2adb6af5e821560bdb8defd62afe60
parent 248118 f8089ba15c56aab0536d154efeb2dfdfd3e8e031
child 248120 6c5849a66b1ac07ce819846a578b44d1e8ba8b87
push id7762
push usermdeboer@mozilla.com
push dateMon, 16 Mar 2015 15:07:09 +0000
treeherdermozilla-aurora@9b59f3a2743d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8, lsblakk
bugs1131568
milestone38.0a2
Bug 1131568 - Update the OpenTok SDK to version 2.5.0. r=Standard8, a=lsblakk
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,31 @@
 /**
- * @license  OpenTok JavaScript Library v2.4.0 54ae164 HEAD
+ * @license  OpenTok JavaScript Library v2.5.0 17447b9 HEAD
  * http://www.tokbox.com/
  *
  * Copyright (c) 2014 TokBox, Inc.
  * Released under the MIT license
  * http://opensource.org/licenses/MIT
  *
- * Date: January 08 08:54:40 2015
+ * Date: March 02 09:16:29 2015
  */
 
 
 !(function(window) {
 
 !(function(window, OTHelpers, undefined) {
 
 /**
- * @license  Common JS Helpers on OpenTok 0.2.0 ef06638 2014Q4-2.2
+ * @license  Common JS Helpers on OpenTok 0.3.0 f4918b4 2014Q4-2.2.patch.1
  * http://www.tokbox.com/
  *
  * Copyright (c) 2015 TokBox, Inc.
  *
- * Date: January 08 08:54:29 2015
+ * Date: March 02 09:16:17 2015
  *
  */
 
 
 // OT Helper Methods
 //
 // helpers.js                           <- the root file
 // helpers/lib/{helper topic}.js        <- specialised helpers for specific tasks/topics
@@ -1904,116 +1904,16 @@ OTHelpers.Collection = function(idField)
     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;
-});
-/*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.
@@ -3200,16 +3100,40 @@ var observeNodeOrChildNodeRemoval = func
     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.
 //
@@ -3269,27 +3193,397 @@ ElementCollection.prototype.observeNodeO
     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];
 };
 
+// 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);
+  };
+
+})();
+
+/*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 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() {
+
+  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('./dom.js')
 // tb_require('./capabilities.js')
 
 // Returns true if the client supports element.classList
 OTHelpers.registerCapability('classList', function() {
@@ -3591,229 +3885,16 @@ OTHelpers.centerElement = function(eleme
   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
@@ -3906,45 +3987,16 @@ OTHelpers.centerElement = function(eleme
 })();
 
 /*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',
@@ -4150,22 +4202,22 @@ OTHelpers.post = function(url, options, 
   }
 };
 
 
 })(window, window.OTHelpers);
 
 
 /**
- * @license  TB Plugin 0.4.0.9 88af499 2014Q4-2.2
+ * @license  TB Plugin 0.4.0.9 2c62633 2014Q4-2.2.patch.1
  * http://www.tokbox.com/
  *
  * Copyright (c) 2015 TokBox, Inc.
  *
- * Date: January 08 08:54:38 2015
+ * Date: March 02 09:16:25 2015
  *
  */
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: false,
           trailing: true, browser: true, smarttabs:true */
 /* global scope:true, OT:true, OTHelpers:true */
 /* exported OTPlugin */
 
@@ -4669,129 +4721,16 @@ var VideoContainer = function VideoConta
 var RTCStatsReport = function (reports) {
   this.forEach = function (callback, context) {
     for (var id in reports) {
       callback.call(context, reports[id]);
     }
   };
 };
 
-// tb_require('./header.js')
-// tb_require('./shims.js')
-// tb_require('./proxy.js')
-// tb_require('./video_container.js')
-
-/* jshint globalstrict: true, strict: false, undef: true, unused: true,
-          trailing: true, browser: true, smarttabs:true */
-/* global VideoContainer:true */
-/* exported MediaStream */
-
-var MediaStreamTrack = function MediaStreamTrack (mediaStreamId, options, plugin) {
-  this.id = options.id;
-  this.kind = options.kind;
-  this.label = options.label;
-  this.enabled = OTHelpers.castToBoolean(options.enabled);
-  this.streamId = mediaStreamId;
-
-  this.setEnabled = function (enabled) {
-    this.enabled = OTHelpers.castToBoolean(enabled);
-
-    if (this.enabled) {
-      plugin._.enableMediaStreamTrack(mediaStreamId, this.id);
-    }
-    else {
-      plugin._.disableMediaStreamTrack(mediaStreamId, this.id);
-    }
-  };
-};
-
-var MediaStream = function MediaStream (options, plugin) {
-  var audioTracks = [],
-      videoTracks = [];
-
-  this.id = options.id;
-  plugin.addRef(this);
-
-  // TODO
-  // this.ended =
-  // this.onended =
-
-  if (options.videoTracks) {
-    options.videoTracks.map(function(track) {
-      videoTracks.push( new MediaStreamTrack(options.id, track, plugin) );
-    });
-  }
-
-  if (options.audioTracks) {
-    options.audioTracks.map(function(track) {
-      audioTracks.push( new MediaStreamTrack(options.id, track, plugin) );
-    });
-  }
-
-  var hasTracksOfType = function (type) {
-    var tracks = type === 'video' ? videoTracks : audioTracks;
-
-    return OTHelpers.some(tracks, function(track) {
-      return track.enabled;
-    });
-  };
-
-  this.getVideoTracks = function () { return videoTracks; };
-  this.getAudioTracks = function () { return audioTracks; };
-
-  this.getTrackById = function (id) {
-    videoTracks.concat(audioTracks).forEach(function(track) {
-      if (track.id === id) return track;
-    });
-
-    return null;
-  };
-
-  this.hasVideo = function () {
-    return hasTracksOfType('video');
-  };
-
-  this.hasAudio = function () {
-    return hasTracksOfType('audio');
-  };
-
-  this.addTrack = function (/* MediaStreamTrack */) {
-    // TODO
-  };
-
-  this.removeTrack = function (/* MediaStreamTrack */) {
-    // TODO
-  };
-
-  this.stop = function() {
-    plugin._.stopMediaStream(this.id);
-    plugin.removeRef(this);
-  };
-
-  this.destroy = function() {
-    this.stop();
-  };
-
-  // Private MediaStream API
-  this._ = {
-    plugin: plugin,
-
-    // Get a VideoContainer to render the stream in.
-    render: OTHelpers.bind(function() {
-      return new VideoContainer(plugin, this);
-    }, this)
-  };
-};
-
-
-MediaStream.fromJson = function (json, plugin) {
-  if (!json) return null;
-  return new MediaStream( JSON.parse(json), plugin );
-};
-
   // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 // tb_require('./stats.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
 /* global MediaStream:true, RTCStatsReport:true */
@@ -5087,16 +5026,129 @@ PeerConnection.create = function (iceSer
 
 // tb_require('./header.js')
 // tb_require('./shims.js')
 // tb_require('./proxy.js')
 // tb_require('./video_container.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
+/* global VideoContainer:true */
+/* exported MediaStream */
+
+var MediaStreamTrack = function MediaStreamTrack (mediaStreamId, options, plugin) {
+  this.id = options.id;
+  this.kind = options.kind;
+  this.label = options.label;
+  this.enabled = OTHelpers.castToBoolean(options.enabled);
+  this.streamId = mediaStreamId;
+
+  this.setEnabled = function (enabled) {
+    this.enabled = OTHelpers.castToBoolean(enabled);
+
+    if (this.enabled) {
+      plugin._.enableMediaStreamTrack(mediaStreamId, this.id);
+    }
+    else {
+      plugin._.disableMediaStreamTrack(mediaStreamId, this.id);
+    }
+  };
+};
+
+var MediaStream = function MediaStream (options, plugin) {
+  var audioTracks = [],
+      videoTracks = [];
+
+  this.id = options.id;
+  plugin.addRef(this);
+
+  // TODO
+  // this.ended =
+  // this.onended =
+
+  if (options.videoTracks) {
+    options.videoTracks.map(function(track) {
+      videoTracks.push( new MediaStreamTrack(options.id, track, plugin) );
+    });
+  }
+
+  if (options.audioTracks) {
+    options.audioTracks.map(function(track) {
+      audioTracks.push( new MediaStreamTrack(options.id, track, plugin) );
+    });
+  }
+
+  var hasTracksOfType = function (type) {
+    var tracks = type === 'video' ? videoTracks : audioTracks;
+
+    return OTHelpers.some(tracks, function(track) {
+      return track.enabled;
+    });
+  };
+
+  this.getVideoTracks = function () { return videoTracks; };
+  this.getAudioTracks = function () { return audioTracks; };
+
+  this.getTrackById = function (id) {
+    videoTracks.concat(audioTracks).forEach(function(track) {
+      if (track.id === id) return track;
+    });
+
+    return null;
+  };
+
+  this.hasVideo = function () {
+    return hasTracksOfType('video');
+  };
+
+  this.hasAudio = function () {
+    return hasTracksOfType('audio');
+  };
+
+  this.addTrack = function (/* MediaStreamTrack */) {
+    // TODO
+  };
+
+  this.removeTrack = function (/* MediaStreamTrack */) {
+    // TODO
+  };
+
+  this.stop = function() {
+    plugin._.stopMediaStream(this.id);
+    plugin.removeRef(this);
+  };
+
+  this.destroy = function() {
+    this.stop();
+  };
+
+  // Private MediaStream API
+  this._ = {
+    plugin: plugin,
+
+    // Get a VideoContainer to render the stream in.
+    render: OTHelpers.bind(function() {
+      return new VideoContainer(plugin, this);
+    }, this)
+  };
+};
+
+
+MediaStream.fromJson = function (json, plugin) {
+  if (!json) return null;
+  return new MediaStream( JSON.parse(json), plugin );
+};
+
+// tb_require('./header.js')
+// tb_require('./shims.js')
+// tb_require('./proxy.js')
+// tb_require('./video_container.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+          trailing: true, browser: true, smarttabs:true */
 /* exported MediaConstraints */
 
 var MediaConstraints = function(userConstraints) {
   var constraints = OTHelpers.clone(userConstraints);
 
   this.hasVideo = constraints.video !== void 0 && constraints.video !== false;
   this.hasAudio = constraints.audio !== void 0 && constraints.audio !== false;
 
@@ -5869,18 +5921,18 @@ OT.APIKEY = (function(){
 
 
 if (!window.OT) window.OT = OT;
 if (!window.TB) window.TB = OT;
 
 // tb_require('../js/ot.js')
 
 OT.properties = {
-  version: 'v2.4.0',         // The current version (eg. v2.0.4) (This is replaced by gradle)
-  build: '54ae164',    // The current build hash (This is replaced by gradle)
+  version: 'v2.5.0',         // The current version (eg. v2.0.4) (This is replaced by gradle)
+  build: '17447b9',    // 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',
@@ -6199,17 +6251,16 @@ OT.Rumor = {
  *
  * 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) {
     return; // IE 8 doesn't do websockets. No websockets, no encoding.
   }
 
   if ( (global.TextEncoder !== void 0) && (global.TextDecoder !== void 0))  {
     // defer to the native ones
     return;
   }
@@ -8752,16 +8803,17 @@ OT.Raptor.Message.signals.create = funct
           // 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
           };
           session.logEvent('streamCreated', 'warning', payload);
         }
       });
       
       return connection;
     };
 
@@ -8807,17 +8859,17 @@ 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 = {
-          type : 'eventOrderError -- streamCreated event before connectionCreated',
+          debug : 'eventOrderError -- streamCreated event before connectionCreated',
           streamId : stream.id,
         };
         session.logEvent('streamCreated', 'warning', payload);
       }
 
       if (stream.publisher) {
         stream.publisher.setStream(stream);
       }
@@ -11118,16 +11170,20 @@ OT.PeerConnection = function(config) {
       }, this);
 
   this.addLocalStream = function(webRTCStream) {
     createPeerConnection(function() {
       _peerConnection.addStream(webRTCStream);
     }, webRTCStream);
   };
 
+  this.getSenders = function() {
+    return _peerConnection.getSenders();
+  };
+
   this.disconnect = function() {
     _iceProcessor = null;
 
     if (_peerConnection &&
         _peerConnection.signalingState &&
         _peerConnection.signalingState.toLowerCase() !== 'closed') {
 
       _peerConnection.close();
@@ -11931,16 +11987,20 @@ OT.PublisherPeerConnection = function(re
       return remoteConnection;
     };
 
     this.hasRelayCandidates = function() {
       return _hasRelayCandidates;
     };
 
   };
+
+  this.getSenders = function() {
+    return _peerConnection.getSenders();
+  };
 };
 
 
 // tb_require('../helpers.js')
 // tb_require('./web_rtc.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
@@ -12903,17 +12963,17 @@ var videoContentResizesMixin = function(
   // their video element and other chrome.
   OT.WidgetView = function(targetElement, properties) {
 
     var widgetView = {};
 
     var container = getOrCreateContainer(targetElement, properties && properties.insertMode),
         widgetContainer = document.createElement('div'),
         oldContainerStyles = {},
-        dimensionsObserver,
+        sizeObserver,
         videoElement,
         videoObserver,
         posterContainer,
         loadingContainer,
         width,
         height,
         loading = true,
         audioOnly = false,
@@ -12973,28 +13033,28 @@ var videoContentResizesMixin = function(
     OT.$.addClass(posterContainer, 'OT_video-poster');
     widgetContainer.appendChild(posterContainer);
 
     oldContainerStyles.width = container.offsetWidth;
     oldContainerStyles.height = container.offsetHeight;
 
     if (!OTPlugin.isInstalled()) {
       // Observe changes to the width and height and update the aspect ratio
-      dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'],
-        function(changeSet) {
-          var width = changeSet.width ? changeSet.width[1] : container.offsetWidth,
-              height = changeSet.height ? changeSet.height[1] : container.offsetHeight;
+      sizeObserver = OT.$(container).observeSize(
+        function(size) {
+          var width = size.width,
+              height = size.height;
 
           fixMini(container, width, height);
 
           if (videoElement) {
             fixFitMode(widgetContainer, width, height, videoElement.aspectRatio(),
               videoElement.isRotated());
           }
-        });
+        })[0];
 
 
       // @todo observe if the video container or the video element get removed
       // if they do we should do some cleanup
       videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) {
         if (!videoElement) return;
 
         // This assumes a video element being removed is the main video element. This may
@@ -13008,32 +13068,32 @@ var videoContentResizesMixin = function(
           videoElement = null;
         }
 
         if (widgetContainer) {
           OT.$.removeElement(widgetContainer);
           widgetContainer = null;
         }
 
-        if (dimensionsObserver) {
-          dimensionsObserver.disconnect();
-          dimensionsObserver = null;
+        if (sizeObserver) {
+          sizeObserver.disconnect();
+          sizeObserver = null;
         }
 
         if (videoObserver) {
           videoObserver.disconnect();
           videoObserver = null;
         }
       });
     }
 
     widgetView.destroy = function() {
-      if (dimensionsObserver) {
-        dimensionsObserver.disconnect();
-        dimensionsObserver = null;
+      if (sizeObserver) {
+        sizeObserver.disconnect();
+        sizeObserver = null;
       }
 
       if (videoObserver) {
         videoObserver.disconnect();
         videoObserver = null;
       }
 
       if (videoElement) {
@@ -16405,17 +16465,18 @@ OT.checkScreenSharingCapability = functi
   }
 
   response.supported = true;
   response.extensionRequired = helper.proto.extensionRequired ? helper.name : void 0;
 
   response.supportedSources = {
     screen: helper.proto.sources.screen,
     application: helper.proto.sources.application,
-    window: helper.proto.sources.window
+    window: helper.proto.sources.window,
+    browser: helper.proto.sources.browser
   };
 
   if (!helper.instance) {
     response.extensionRegistered = false;
     if (response.extensionRequired) {
       response.extensionInstalled = false;
     }
     setTimeout(callback.bind(null, response));
@@ -16435,42 +16496,55 @@ OT.checkScreenSharingCapability = functi
 OT.registerScreenSharingExtensionHelper('firefox', {
   isSupportedInThisBrowser: OT.$.env.name === 'Firefox',
   autoRegisters: true,
   extensionRequired: false,
   getConstraintsShowsPermissionUI: false,
   sources: {
     screen: true,
     application: OT.$.env.name === 'Firefox' && OT.$.env.version >= 34,
-    window: OT.$.env.name === 'Firefox' && OT.$.env.version >= 34
+    window: OT.$.env.name === 'Firefox' && OT.$.env.version >= 34,
+    browser: OT.$.env.name === 'Firefox' && OT.$.env.version >= 38
   },
   register: function() {
     return {
       isInstalled: function(callback) {
         callback(true);
       },
       getConstraints: function(source, constraints, callback) {
         constraints.video = {
           mediaSource: source
         };
+
+        // copy constraints under the video object and removed them from the root constraint object
+        if (constraints.browserWindow) {
+          constraints.video.browserWindow = constraints.browserWindow;
+          delete constraints.browserWindow;
+        }
+        if (typeof constraints.scrollWithPage !== 'undefined') {
+          constraints.video.scrollWithPage = constraints.scrollWithPage;
+          delete constraints.scrollWithPage;
+        }
+
         callback(void 0, constraints);
       }
     };
   }
 });
 
 OT.registerScreenSharingExtensionHelper('chrome', {
   isSupportedInThisBrowser: !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined',
   autoRegisters: false,
   extensionRequired: true,
   getConstraintsShowsPermissionUI: true,
   sources: {
     screen: true,
     application: false,
-    window: false
+    window: false,
+    browser: false
   },
   register: function (extensionID) {
     if(!extensionID) {
       throw new Error('initChromeScreenSharingExtensionHelper: extensionID is required.');
     }
 
     var isChrome = !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined',
         callbackRegistry = {},
@@ -16750,18 +16824,17 @@ OT.StreamChannel = function(options) {
  *
  * @property {String} streamId The unique ID of the stream.
  *
  * @property {Object} videoDimensions This object has two properties: <code>width</code> and
  * <code>height</code>. Both are numbers. The <code>width</code> property is the width of the
  * encoded stream; the <code>height</code> property is the height of the encoded stream. (These
  * are independent of the actual width of Publisher and Subscriber objects corresponding to the
  * stream.) This property can change if a stream published from a mobile device resizes, based on
- * a change in the device orientation. It can also occur if the video source is a screen-sharing
- * window and the user publishing the stream resizes the window. When the video dimensions change,
+ * a change in the device orientation. When the video dimensions change,
  * the {@link Session} object dispatches a <code>streamPropertyChanged</code> event
  * (see {@link StreamPropertyChangedEvent}).
  *
  * @property {String} videoType The type of video &mdash; either <code>"camera"</code> or
  * <code>"screen"</code>. A <code>"screen"</code> video uses screen sharing on the publisher
  * as the video source; for other videos, this property is set to <code>"camera"</code>.
  * This property can change if a stream published from a mobile device changes from a
  * camera to a screen-sharing video type. When the video type changes, the {@link Session} object
@@ -17069,18 +17142,17 @@ OT.StreamChannel = function(options) {
     else {
       options = {
         headers: {
           'X-TB-TOKEN-AUTH': session.token,
           'X-TB-VERSION': 1
         }
       };
     }
-    session.logEvent('SessionInfo', 'Attempt', {messagingServer: OT.properties.apiURL});
-
+    session.logEvent('SessionInfo', 'Attempt');
     OT.$.getJSON(sessionInfoURL, options, function(error, sessionInfo) {
       if(error) {
         var responseText = sessionInfo;
         onGetErrorCallback(session, onFailure,
           new OT.Error(error.target && error.target.status || error.code, error.message ||
             'Could not connect to the OpenTok API Server.'), responseText);
       } else {
         validateRawSessionInfo(sessionInfo);
@@ -17120,21 +17192,24 @@ OT.StreamChannel = function(options) {
     } else {
       return {
         code: null,
         message: 'Unknown error: getSessionInfo JSON response was badly formed'
       };
     }
   };
 
+  /* jshint camelcase:false */
   onGetResponseCallback = function(session, onSuccess, rawSessionInfo) {
-    session.logEvent('SessionInfo', 'Success', {messagingServer: OT.properties.apiURL});
+    session.logEvent('SessionInfo', 'Success',
+      {messagingServer: rawSessionInfo[0].messaging_server_url});
 
     onSuccess( new OT.SessionInfo(rawSessionInfo) );
   };
+  /* jshint camelcase:true */
 
   onGetErrorCallback = function(session, onFailure, error, responseText) {
     var payload = {
       reason:'GetSessionInfo',
       code: (error.code || 'No code'),
       message: error.message + ':' +
         (responseText || 'Empty responseText from API server')
     };
@@ -18249,20 +18324,19 @@ OT.Raptor.Socket = function(connectionId
       'TRANSACTION-ID': transactionId,
       'X-TB-FROM-ADDRESS': _rumor.id()
     }));
 
     return transactionId;
   };
 
   // Register a new stream against _sessionId
-  this.streamCreate = function(name, audioFallbackEnabled, channels, minBitrate, maxBitrate,
-    completion) {
-    var streamId = OT.$.uuid(),
-        message = OT.Raptor.Message.streams.create( OT.APIKEY,
+  this.streamCreate = function(name, streamId, audioFallbackEnabled, channels, minBitrate,
+    maxBitrate, completion) {
+    var message = OT.Raptor.Message.streams.create( OT.APIKEY,
                                                     _sessionId,
                                                     streamId,
                                                     name,
                                                     audioFallbackEnabled,
                                                     channels,
                                                     minBitrate,
                                                     maxBitrate);
 
@@ -18438,34 +18512,41 @@ OT.GetStatsAudioLevelSampler = function(
  *
  * @constructor
  * @param {AudioContext} audioContext an audio context instance to get an analyser node
  */
 OT.AnalyserAudioLevelSampler = function(audioContext) {
 
   var _sampler = this,
       _analyser = null,
-      _timeDomainData = null;
-
-  var _getAnalyser = function(stream) {
-    var sourceNode = audioContext.createMediaStreamSource(stream);
-    var analyser = audioContext.createAnalyser();
-    sourceNode.connect(analyser);
-    return analyser;
-  };
-
-  this.webRTCStream = null;
+      _timeDomainData = null,
+      _webRTCStream = null;
+
+  var buildAnalyzer = function(stream) {
+        var sourceNode = audioContext.createMediaStreamSource(stream);
+        var analyser = audioContext.createAnalyser();
+        sourceNode.connect(analyser);
+        return analyser;
+      };
+
+  OT.$.defineProperties(_sampler, {
+    webRTCStream: {
+      get: function() {
+        return _webRTCStream;
+      },
+      set: function(webRTCStream) {
+        // when the stream is updated we need to create a new analyzer
+        _webRTCStream = webRTCStream;
+        _analyser = buildAnalyzer(_webRTCStream);
+        _timeDomainData = new Uint8Array(_analyser.frequencyBinCount);
+      }
+    }
+  });
 
   this.sample = function(done) {
-
-    if (!_analyser && _sampler.webRTCStream) {
-      _analyser = _getAnalyser(_sampler.webRTCStream);
-      _timeDomainData = new Uint8Array(_analyser.frequencyBinCount);
-    }
-
     if (_analyser) {
       _analyser.getByteTimeDomainData(_timeDomainData);
 
       // varies from 0 to 255
       var max = 0;
       for (var idx = 0; idx < _timeDomainData.length; idx++) {
         max = Math.max(max, Math.abs(_timeDomainData[idx] - 128));
       }
@@ -18881,17 +18962,17 @@ OT.Subscriber = function(targetElement, 
             videoOrientation: _stream.videoDimensions.orientation
           });
 
           onLoaded.call(this, null);
         }, this));
 
         if (OT.$.hasCapabilities('webAudioCapableRemoteStream') && _audioLevelSampler &&
           webRTCStream.getAudioTracks().length > 0) {
-          _audioLevelSampler.webRTCStream = webRTCStream;
+          _audioLevelSampler.webRTCStream(webRTCStream);
         }
 
         logAnalyticsEvent('createPeerConnection', 'StreamAdded');
         this.trigger('streamAdded', this);
       },
 
       onRemoteStreamRemoved = function(webRTCStream) {
         OT.debug('OT.Subscriber.onStreamRemoved');
@@ -20321,26 +20402,26 @@ OT.Session = function(apiKey, sessionId)
    * @param {function(?OT.$.Error, Stats=)} callback
    */
   this.testNetwork = function(token, publisher, callback) {
 
     // intercept call to callback to log the result
     var origCallback = callback;
     callback = function loggingCallback(error, stats) {
       if (error) {
-        _session.logEvent('testNetwork', 'Failure', {
+        _session.logEvent('TestNetwork', 'Failure', {
           failureCode: error.name || error.message || 'unknown'
         });
       } else {
-        _session.logEvent('testNetwork', 'Success', stats);
+        _session.logEvent('TestNetwork', 'Success', stats);
       }
       origCallback(error, stats);
     };
 
-    _session.logEvent('testNetwork', 'Attempt', {});
+    _session.logEvent('TestNetwork', 'Attempt', {});
 
     if(this.isConnected()) {
       callback(new OT.$.Error('Session connected, cannot test network', 1015));
       return;
     }
 
     var webRtcStreamPromise = new Promise(
       function(resolve, reject) {
@@ -20796,16 +20877,17 @@ OT.Session = function(apiKey, sessionId)
         action: 'Publish',
         variation: 'Failure',
         payload: {
           reason:'unconnected',
           code: OT.ExceptionCodes.NOT_CONNECTED,
           message: 'We need to be connected before you can publish'
         },
         sessionId: _sessionId,
+        streamId: (publisher && publisher.stream) ? publisher.stream.id : null,
         partnerId: _apiKey,
       });
 
       if (completionHandler && OT.$.isFunction(completionHandler)) {
         dispatchError(OT.ExceptionCodes.NOT_CONNECTED,
           'We need to be connected before you can publish', completionHandler);
       }
 
@@ -21387,19 +21469,20 @@ OT.Session = function(apiKey, sessionId)
       return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes);
     },
 
     subscriberChannelUpdate: function(stream, subscriber, channel, attributes) {
       return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id,
         attributes);
     },
 
-    streamCreate: function(name, audioFallbackEnabled, channels, completion) {
+    streamCreate: function(name, streamId, audioFallbackEnabled, channels, completion) {
       _socket.streamCreate(
         name,
+        streamId,
         audioFallbackEnabled,
         channels,
         OT.Config.get('bitrates', 'min', OT.APIKEY),
         OT.Config.get('bitrates', 'max', OT.APIKEY),
         completion
       );
     },
 
@@ -21987,17 +22070,17 @@ OT.Session = function(apiKey, sessionId)
 // tb_require('./chrome/mute_button.js')
 // tb_require('./chrome/archiving.js')
 // tb_require('./chrome/audio_level_meter.js')
 // tb_require('./peer_connection/publisher_peer_connection.js')
 // tb_require('./screensharing/register.js')
 
 /* jshint globalstrict: true, strict: false, undef: true, unused: true,
           trailing: true, browser: true, smarttabs:true */
-/* global OT */
+/* global OT, Promise */
 
 // The default constraints
 var defaultConstraints = {
   audio: true,
   video: true
 };
 
 /**
@@ -22075,16 +22158,17 @@ OT.Publisher = function(options) {
       _state,
       _iceServers,
       _audioLevelCapable = OT.$.hasCapabilities('webAudio'),
       _audioLevelSampler,
       _isScreenSharing = options && (
         options.videoSource === 'screen' ||
         options.videoSource === 'window' ||
         options.videoSource === 'tab' ||
+        options.videoSource === 'browser' ||
         options.videoSource === 'application'
       ),
       _connectivityAttemptPinger,
       _publisher = this;
 
   _properties = OT.$.defaults(options || {}, {
     publishAudio: _isScreenSharing ? false : true,
     publishVideo: true,
@@ -22146,29 +22230,29 @@ OT.Publisher = function(options) {
         OT.analytics.logEvent({
           action: action,
           variation: variation,
           payload: payload,
           'sessionId': _session ? _session.sessionId : null,
           'connectionId': _session &&
             _session.isConnected() ? _session.connection.connectionId : null,
           'partnerId': _session ? _session.apiKey : OT.APIKEY,
-          streamId: _stream ? _stream.id : null
+          streamId: _streamId
         }, throttle);
       },
 
       logConnectivityEvent = function(variation, payload) {
         if (variation === 'Attempt' || !_connectivityAttemptPinger) {
           _connectivityAttemptPinger = new OT.ConnectivityAttemptPinger({
             action: 'Publish',
             'sessionId': _session ? _session.sessionId : null,
             'connectionId': _session &&
               _session.isConnected() ? _session.connection.connectionId : null,
             'partnerId': _session ? _session.apiKey : OT.APIKEY,
-            streamId: _stream ? _stream.id : null
+            streamId: _streamId
           });
         }
         if (variation === 'Failure' && payload.reason !== 'Non-fatal') {
           // We don't want to log an invalid sequence in this case because it was a 
           // non-fatal failure
           _connectivityAttemptPinger.setVariation(variation);
         }
         logAnalyticsEvent('Publish', variation, payload);
@@ -22176,17 +22260,17 @@ OT.Publisher = function(options) {
 
       recordQOS = OT.$.bind(function(connection, parsedStats) {
         var QoSBlob = {
           streamType: 'WebRTC',
           sessionId: _session ? _session.sessionId : null,
           connectionId: _session && _session.isConnected() ?
             _session.connection.connectionId : null,
           partnerId: _session ? _session.apiKey : OT.APIKEY,
-          streamId: _stream ? _stream.id : null,
+          streamId: _streamId,
           width: _widgetView ? Number(OT.$.width(_widgetView.domElement).replace('px', ''))
             : undefined,
           height: _widgetView ? Number(OT.$.height(_widgetView.domElement).replace('px', ''))
             : undefined,
           version: OT.properties.version,
           mediaServerName: _session ? _session.sessionInfo.messagingServer : null,
           p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
           duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0,
@@ -22273,17 +22357,17 @@ OT.Publisher = function(options) {
             onLoadFailure(err);
             return;
           }
 
           onLoaded();
         });
 
         if(_audioLevelSampler && _webRTCStream.getAudioTracks().length > 0) {
-          _audioLevelSampler.webRTCStream = _webRTCStream;
+          _audioLevelSampler.webRTCStream(_webRTCStream);
         }
 
       }, this),
 
       onStreamAvailableError = OT.$.bind(function(error) {
         OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
 
         _state.set('Failed');
@@ -22980,17 +23064,16 @@ OT.Publisher = function(options) {
       }
     }
 
     setAudioOnly(!value);
 
     return this;
   };
 
-
   /**
   * Deletes the Publisher object and removes it from the HTML DOM.
   * <p>
   * The Publisher object dispatches a <code>destroyed</code> event when the DOM
   * element is removed.
   * </p>
   * @method #destroy
   * @memberOf Publisher
@@ -23088,16 +23171,17 @@ OT.Publisher = function(options) {
 
 
   // API Compatibility layer for Flash Publisher, this could do with some tidyup.
   this._ = {
     publishToSession: OT.$.bind(function(session) {
       // Add session property to Publisher
       this.session = _session = session;
 
+      _streamId = OT.$.uuid();
       var createStream = function() {
 
         var streamWidth,
             streamHeight;
 
         // Bail if this.session is gone, it means we were unpublished
         // before createStream could finish.
         if (!_session) return;
@@ -23156,18 +23240,18 @@ OT.Publisher = function(options) {
         if (!(_properties.audioSource === null || _properties.audioSource === false)) {
           streamChannels.push(new OT.StreamChannel({
             id: 'audio1',
             type: 'audio',
             active: _properties.publishAudio
           }));
         }
 
-        session._.streamCreate(_properties.name || '', _properties.audioFallbackEnabled,
-          streamChannels, onStreamRegistered);
+        session._.streamCreate(_properties.name || '', _streamId,
+          _properties.audioFallbackEnabled, streamChannels, onStreamRegistered);
 
       };
 
       if (_loaded) createStream.call(this);
       else this.on('initSuccess', createStream, this);
 
       logConnectivityEvent('Attempt', {streamType: 'WebRTC'});
 
@@ -23222,16 +23306,85 @@ OT.Publisher = function(options) {
         _chrome.archive.setArchiving(status);
       }
 
       return status;
     }, this),
 
     webRtcStream: function() {
       return _webRTCStream;
+    },
+
+    /**
+     * @param {string=} windowId
+     */
+    switchAcquiredWindow: function(windowId) {
+
+      if (OT.$.env.name !== 'Firefox' || OT.$.env.version < 38) {
+        throw new Error('switchAcquiredWindow is an experimental method and is not supported by' +
+        'the current platform');
+      }
+
+      if (typeof windowId !== 'undefined') {
+        _properties.constraints.video.browserWindow = windowId;
+      }
+
+      return new Promise(function(resolve, reject) {
+        OT.$.getUserMedia(
+          _properties.constraints,
+          function(newStream) {
+
+            cleanupLocalStream();
+            _webRTCStream = newStream;
+
+            _microphone = new OT.Microphone(_webRTCStream, !_properties.publishAudio);
+
+            var videoContainerOptions = {
+              muted: true,
+              error: onVideoError
+            };
+
+            _targetElement = _widgetView.bindVideo(_webRTCStream, videoContainerOptions,
+              function(err) {
+                if (err) {
+                  onLoadFailure(err);
+                  reject(err);
+                }
+              });
+
+            if (_audioLevelSampler && _webRTCStream.getAudioTracks().length > 0) {
+              _audioLevelSampler.webRTCStream(_webRTCStream);
+            }
+
+            var replacePromises = [];
+
+            Object.keys(_peerConnections).forEach(function(connectionId) {
+              var peerConnection = _peerConnections[connectionId];
+              peerConnection.getSenders().forEach(function(sender) {
+                if (sender.track.kind === 'audio' && newStream.getAudioTracks().length) {
+                  replacePromises.push(sender.replaceTrack(newStream.getAudioTracks()[0]));
+                } else if (sender.track.kind === 'video' && newStream.getVideoTracks().length) {
+                  replacePromises.push(sender.replaceTrack(newStream.getVideoTracks()[0]));
+                }
+              });
+            });
+
+            Promise.all(replacePromises).then(resolve, reject);
+          },
+          function(error) {
+            onStreamAvailableError(error);
+            reject(error);
+          },
+          onAccessDialogOpened,
+          onAccessDialogClosed,
+          function(error) {
+            onAccessDenied(error);
+            reject(error);
+          });
+      });
     }
   };
 
   this.detectDevices = function() {
     OT.warn('Fixme: Haven\'t implemented detectDevices');
   };
 
   this.detectMicActivity = function() {
@@ -23701,17 +23854,18 @@ OT.initSession = function(apiKey, sessio
 *    match an existing video input device, the call to <code>OT.initPublisher()</code> fails with an
 *    error (error code 1500, "Unable to Publish") passed to the completion handler function.
 *    <p>
 *    If you set this property to <code>null</code> or <code>false</code>, the browser does not
 *    request access to the camera, and no video is published. In a voice-only call, set this
 *    property to <code>null</code> or <code>false</code> for each Publisher.
 *    </p>
 *   <p>
-*    Set this property to <code>"screen"</code> to publish a screen-sharing stream. Call
+*     To publish a screen-sharing streamet this property to <code>"application"</code>,
+*    <code>"screen"</code>, or <code>"window"</code>. Call
 *    <a href="OT.html#checkScreenSharingCapability">OT.checkScreenSharingCapability()</a> to check
 *    if screen sharing is supported. When you set the <code>videoSource</code> property to
 *    <code>"screen"</code>, the following are default values for other properties:
 *    <code>audioFallbackEnabled == false</code>,
 *    <code>maxResolution == {width: 1920, height: 1920}</code>, <code>mirror == false</code>,
 *    <code>scaleMode == "fit"</code>. Also, the default <code>scaleMode</code> setting for
 *    subscribers to the stream is <code>"fit"</code>.
 * </li>