diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala
index ad5e4c5f64cf5520b61f66a409315d4a791565d9..ef46d2b8dc88b6a46d754b9767db312307175827 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala
@@ -16,7 +16,7 @@ trait UserBroadcastCamStartMsgHdlr {
       val envelope = BbbCoreEnvelope(UserBroadcastCamStartedEvtMsg.NAME, routing)
       val header = BbbClientMsgHeader(UserBroadcastCamStartedEvtMsg.NAME, props.meetingProp.intId, msg.header.userId)
 
-      val body = UserBroadcastCamStartedEvtMsgBody(msg.header.userId, msg.body.stream)
+      val body = UserBroadcastCamStartedEvtMsgBody(msg.header.userId, msg.body.stream, msg.body.isHtml5Client)
       val event = UserBroadcastCamStartedEvtMsg(header, body)
       val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
       outGW.send(msgEvent)
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala
index 5ae7d4d228c9d5151b778f4ede4633c40c91d228..c1d5fcff1be79e96dcd5f3182ff1487239155618 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala
@@ -3,15 +3,15 @@ package org.bigbluebutton.common2.msgs
 object UserBroadcastCamStartedEvtMsg { val NAME = "UserBroadcastCamStartedEvtMsg" }
 case class UserBroadcastCamStartedEvtMsg(header: BbbClientMsgHeader,
                                          body: UserBroadcastCamStartedEvtMsgBody) extends BbbCoreMsg
-case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String)
+case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String, isHtml5Client: Boolean = false)
 
 object UserBroadcastCamStartMsg { val NAME = "UserBroadcastCamStartMsg" }
 case class UserBroadcastCamStartMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStartMsgBody) extends StandardMsg
-case class UserBroadcastCamStartMsgBody(stream: String)
+case class UserBroadcastCamStartMsgBody(stream: String, isHtml5Client: Boolean = false)
 
 object UserBroadcastCamStopMsg { val NAME = "UserBroadcastCamStopMsg" }
 case class UserBroadcastCamStopMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStopMsgBody) extends StandardMsg
-case class UserBroadcastCamStopMsgBody(stream: String)
+case class UserBroadcastCamStopMsgBody(stream: String, isHtml5Client: Boolean = false)
 
 object UserBroadcastCamStoppedEvtMsg { val NAME = "UserBroadcastCamStoppedEvtMsg" }
 case class UserBroadcastCamStoppedEvtMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStoppedEvtMsgBody) extends BbbCoreMsg
diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template
index cc0b56fd9c4d63dc83160cbf101713d2e93f0e76..4d43ee876264fd7a29ae485b80518446b4ddf012 100755
--- a/bigbluebutton-client/resources/config.xml.template
+++ b/bigbluebutton-client/resources/config.xml.template
@@ -48,6 +48,7 @@
 			uri="rtmp://HOST/screenshare"
 			showButton="true"
 			enablePause="true"
+			tryKurentoWebRTC="false"
 			tryWebRTCFirst="false"
 			chromeExtensionLink=""
 			chromeExtensionKey=""
diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html
index 3084a19be3d5a7046dcc2b85c99e92861ba8f699..ecd8710c8d5107671da8ae4674687292d4098fca 100755
--- a/bigbluebutton-client/resources/prod/BigBlueButton.html
+++ b/bigbluebutton-client/resources/prod/BigBlueButton.html
@@ -142,8 +142,8 @@
     <script src="lib/verto-min.js" language="javascript"></script>
     <script src="lib/verto_extension.js" language="javascript"></script>
 
-    <script src="lib/kurento-utils.min.js" language="javascript"></script>
     <script src="lib/kurento-extension.js" language="javascript"></script>
+    <script src="lib/kurento-utils.js" language="javascript"></script>
 
     <script src="lib/bbb_api_bridge.js?v=VERSION" language="javascript"></script>
     <script src="lib/sip.js?v=VERSION" language="javascript"></script>
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
index cd9c5fab9026142ecae3133278762eabdf33fd9b..3aa4e9243994311f35964cb79379d1a53cc74f74 100644
--- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js
+++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
@@ -1,6 +1,7 @@
 var isFirefox = typeof window.InstallTrigger !== 'undefined';
 var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
 var isChrome = !!window.chrome && !isOpera;
+var isSafari = navigator.userAgent.indexOf("Safari") >= 0 && !isChrome;
 var kurentoHandler = null;
 
 Kurento = function (
@@ -20,7 +21,7 @@ Kurento = function (
   this.screenConstraints = {};
   this.mediaCallback = null;
 
-  this.voiceBridge = voiceBridge;
+  this.voiceBridge = voiceBridge + '-SCREENSHARE';
   this.internalMeetingId = internalMeetingId;
 
   this.vid_width = window.screen.width;
@@ -33,9 +34,8 @@ Kurento = function (
 
   this.caller_id_name = conferenceUsername;
   this.caller_id_number = conferenceUsername;
-  this.pingInterval;
 
-  this.kurentoPort = "kurento-screenshare";
+  this.kurentoPort = "bbb-webrtc-sfu";
   this.hostName = window.location.hostname;
   this.socketUrl = 'wss://' + this.hostName + '/' + this.kurentoPort;
 
@@ -43,6 +43,7 @@ Kurento = function (
 
   if (chromeExtension != null) {
     this.chromeExtension = chromeExtension;
+    window.chromeExtension = chromeExtension;
   }
 
   if (onFail != null) {
@@ -57,21 +58,44 @@ Kurento = function (
 
 this.KurentoManager= function () {
   this.kurentoVideo = null;
-  this.kurentoScreenShare = null;
+  this.kurentoScreenshare = null;
 };
 
 KurentoManager.prototype.exitScreenShare = function () {
-  if (this.kurentoScreenShare != null) {
-    if(kurentoHandler.pingInterval) {
-      clearInterval(kurentoHandler.pingInterval);
+  console.log("  [exitScreenShare] Exiting screensharing");
+  if(typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) {
+    if(this.kurentoScreenshare.ws !== null) {
+      this.kurentoScreenshare.ws.onclose = function(){};
+      this.kurentoScreenshare.ws.close();
     }
-    if(kurentoHandler.ws !== null) {
-      kurentoHandler.ws.onclose = function(){};
-      kurentoHandler.ws.close();
+
+    this.kurentoScreenshare.disposeScreenShare();
+    this.kurentoScreenshare = null;
+  }
+
+  if (this.kurentoScreenshare) {
+    this.kurentoScreenshare = null;
+  }
+
+  if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
+    this.exitVideo();
+  }
+};
+
+KurentoManager.prototype.exitVideo = function () {
+  console.log("  [exitScreenShare] Exiting screensharing viewing");
+  if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
+    if(this.kurentoVideo.ws !== null) {
+      this.kurentoVideo.ws.onclose = function(){};
+      this.kurentoVideo.ws.close();
     }
-    kurentoHandler.disposeScreenShare();
-    this.kurentoScreenShare = null;
-    kurentoHandler = null;
+
+    this.kurentoVideo.disposeScreenShare();
+    this.kurentoVideo = null;
+  }
+
+  if (this.kurentoVideo) {
+    this.kurentoVideo = null;
   }
 };
 
@@ -79,24 +103,21 @@ KurentoManager.prototype.shareScreen = function (tag) {
   this.exitScreenShare();
   var obj = Object.create(Kurento.prototype);
   Kurento.apply(obj, arguments);
-  this.kurentoScreenShare = obj;
-  kurentoHandler = obj;
-  this.kurentoScreenShare.setScreenShare(tag);
+  this.kurentoScreenshare = obj;
+  this.kurentoScreenshare.setScreenShare(tag);
 };
 
-// Still unused, part of the HTML5 implementation
 KurentoManager.prototype.joinWatchVideo = function (tag) {
   this.exitVideo();
   var obj = Object.create(Kurento.prototype);
   Kurento.apply(obj, arguments);
   this.kurentoVideo = obj;
-  kurentoHandler = obj;
   this.kurentoVideo.setWatchVideo(tag);
 };
 
 
 Kurento.prototype.setScreenShare = function (tag) {
-  this.mediaCallback = this.makeShare;
+  this.mediaCallback = this.makeShare.bind(this);
   this.create(tag);
 };
 
@@ -112,19 +133,18 @@ Kurento.prototype.init = function () {
     console.log("this browser supports websockets");
     this.ws = new WebSocket(this.socketUrl);
 
-    this.ws.onmessage = this.onWSMessage;
-    this.ws.onclose = function (close) {
+    this.ws.onmessage = this.onWSMessage.bind(this);
+    this.ws.onclose = (close) => {
       kurentoManager.exitScreenShare();
       self.onFail("Websocket connection closed");
     };
-    this.ws.onerror = function (error) {
+    this.ws.onerror = (error) => {
       kurentoManager.exitScreenShare();
       self.onFail("Websocket connection error");
     };
-    this.ws.onopen = function() {
-      self.pingInterval = setInterval(self.ping, 3000);
+    this.ws.onopen = function () {
       self.mediaCallback();
-    };
+    }.bind(self);
   }
   else
     console.log("this browser does not support websockets");
@@ -135,15 +155,16 @@ Kurento.prototype.onWSMessage = function (message) {
   switch (parsedMessage.id) {
 
     case 'presenterResponse':
-      kurentoHandler.presenterResponse(parsedMessage);
+      this.presenterResponse(parsedMessage);
+      break;
+    case 'viewerResponse':
+      this.viewerResponse(parsedMessage);
       break;
     case 'stopSharing':
       kurentoManager.exitScreenShare();
       break;
     case 'iceCandidate':
-      kurentoHandler.webRtcPeer.addIceCandidate(parsedMessage.candidate);
-      break;
-    case 'pong':
+      this.webRtcPeer.addIceCandidate(parsedMessage.candidate);
       break;
     default:
       console.error('Unrecognized message', parsedMessage);
@@ -156,21 +177,33 @@ Kurento.prototype.setRenderTag = function (tag) {
 
 Kurento.prototype.presenterResponse = function (message) {
   if (message.response != 'accepted') {
-    var errorMsg = message.message ? message.message : 'Unknow error';
-    console.warn('Call not accepted for the following reason: ' + errorMsg);
+    var errorMsg = message.message ? message.message : 'Unknown error';
+    console.warn('Call not accepted for the following reason: ' + JSON.stringify(errorMsg, null, 2));
     kurentoManager.exitScreenShare();
-    kurentoHandler.onFail(errorMessage);
+    this.onFail(errorMessage);
   } else {
     console.log("Presenter call was accepted with SDP => " + message.sdpAnswer);
     this.webRtcPeer.processAnswer(message.sdpAnswer);
   }
 }
 
+Kurento.prototype.viewerResponse = function (message) {
+  if (message.response != 'accepted') {
+    var errorMsg = message.message ? message.message : 'Unknown error';
+    console.warn('Call not accepted for the following reason: ' + errorMsg);
+    kurentoManager.exitScreenShare();
+    this.onFail(errorMessage);
+  } else {
+    console.log("Viewer call was accepted with SDP => " + message.sdpAnswer);
+    this.webRtcPeer.processAnswer(message.sdpAnswer);
+  }
+}
+
 Kurento.prototype.serverResponse = function (message) {
   if (message.response != 'accepted') {
     var errorMsg = message.message ? message.message : 'Unknow error';
     console.warn('Call not accepted for the following reason: ' + errorMsg);
-    kurentoHandler.dispose();
+    kurentoManager.exitScreenShare();
   } else {
     this.webRtcPeer.processAnswer(message.sdpAnswer);
   }
@@ -178,89 +211,105 @@ Kurento.prototype.serverResponse = function (message) {
 
 Kurento.prototype.makeShare = function() {
   var self = this;
-  console.log("Kurento.prototype.makeShare " + JSON.stringify(this.webRtcPeer, null, 2));
   if (!this.webRtcPeer) {
-
     var options = {
-      onicecandidate : this.onIceCandidate
+      onicecandidate : self.onIceCandidate.bind(self)
     }
 
-    console.log("Peer options " + JSON.stringify(options, null, 2));
-
-    kurentoHandler.startScreenStreamFrom();
-
+    this.startScreenStreamFrom();
   }
 }
 
 Kurento.prototype.onOfferPresenter = function (error, offerSdp) {
+  let self = this;
   if(error)  {
     console.log("Kurento.prototype.onOfferPresenter Error " + error);
-    kurentoHandler.onFail(error);
+    this.onFail(error);
     return;
   }
 
   var message = {
     id : 'presenter',
     type: 'screenshare',
-    internalMeetingId: kurentoHandler.internalMeetingId,
-    voiceBridge: kurentoHandler.voiceBridge,
-    callerName : kurentoHandler.caller_id_name,
+    role: 'presenter',
+    internalMeetingId: self.internalMeetingId,
+    voiceBridge: self.voiceBridge,
+    callerName : self.caller_id_name,
     sdpOffer : offerSdp,
-    vh: kurentoHandler.vid_height,
-    vw: kurentoHandler.vid_width
+    vh: self.vid_height,
+    vw: self.vid_width
   };
   console.log("onOfferPresenter sending to screenshare server => " + JSON.stringify(message, null, 2));
-  kurentoHandler.sendMessage(message);
+  this.sendMessage(message);
 }
 
 Kurento.prototype.startScreenStreamFrom = function () {
-  var screenInfo = null;
-  var _this = this;
+  var self = this;
   if (!!window.chrome) {
-    if (!_this.chromeExtension) {
-      _this.logError({
+    if (!self.chromeExtension) {
+      self.logError({
         status:  'failed',
         message: 'Missing Chrome Extension key',
       });
-      _this.onFail();
+      self.onFail();
       return;
     }
   }
   // TODO it would be nice to check those constraints
-  _this.screenConstraints.video = {};
+  if (typeof screenConstraints !== undefined) {
+    self.screenConstraints = {};
+  }
+  self.screenConstraints.video = {};
 
+  console.log(self);
   var options = {
-    //localVideo: this.renderTag,
-    onicecandidate : _this.onIceCandidate,
-    mediaConstraints : _this.screenConstraints,
+    localVideo: document.getElementById(this.renderTag),
+    onicecandidate : self.onIceCandidate.bind(self),
+    mediaConstraints : self.screenConstraints,
     sendSource : 'desktop'
   };
 
   console.log(" Peer options => " + JSON.stringify(options, null, 2));
 
-  _this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) {
+  self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) {
     if(error)  {
       console.log("WebRtcPeerSendonly constructor error " + JSON.stringify(error, null, 2));
-      kurentoHandler.onFail(error);
+      self.onFail(error);
       return kurentoManager.exitScreenShare();
     }
 
-    _this.webRtcPeer.generateOffer(_this.onOfferPresenter);
+    self.webRtcPeer.generateOffer(self.onOfferPresenter.bind(self));
     console.log("Generated peer offer w/ options "  + JSON.stringify(options));
   });
 }
 
-Kurento.prototype.onIceCandidate = function(candidate) {
+Kurento.prototype.onIceCandidate = function (candidate) {
+  let self = this;
   console.log('Local candidate' + JSON.stringify(candidate));
 
   var message = {
     id : 'onIceCandidate',
+    role: 'presenter',
     type: 'screenshare',
-    voiceBridge: kurentoHandler.voiceBridge,
+    voiceBridge: self.voiceBridge,
     candidate : candidate
   }
-  console.log("this object " + JSON.stringify(this, null, 2));
-  kurentoHandler.sendMessage(message);
+  this.sendMessage(message);
+}
+
+Kurento.prototype.onViewerIceCandidate = function (candidate) {
+  let self = this;
+  console.log('Viewer local candidate' + JSON.stringify(candidate));
+
+  var message = {
+    id : 'viewerIceCandidate',
+    role: 'viewer',
+    type: 'screenshare',
+    voiceBridge: self.voiceBridge,
+    candidate : candidate,
+    callerName: self.caller_id_name
+  }
+  this.sendMessage(message);
 }
 
 Kurento.prototype.setWatchVideo = function (tag) {
@@ -276,60 +325,50 @@ Kurento.prototype.viewer = function () {
   if (!this.webRtcPeer) {
 
     var options = {
-      remoteVideo: this.renderTag,
-      onicecandidate : onIceCandidate
+      remoteVideo: document.getElementById(this.renderTag),
+      onicecandidate : this.onViewerIceCandidate.bind(this)
     }
 
-    webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) {
+    self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) {
       if(error) {
-        return kurentoHandler.onFail(error);
+        return self.onFail(error);
       }
 
-      this.generateOffer(onOfferViewer);
+      this.generateOffer(self.onOfferViewer.bind(self));
     });
   }
 };
 
 Kurento.prototype.onOfferViewer = function (error, offerSdp) {
+  let self = this;
   if(error)  {
     console.log("Kurento.prototype.onOfferViewer Error " + error);
-    return kurentoHandler.onFail();
+    return this.onFail();
   }
   var message = {
     id : 'viewer',
     type: 'screenshare',
-    internalMeetingId: kurentoHandler.internalMeetingId,
-    voiceBridge: kurentoHandler.voiceBridge,
-    callerName : kurentoHandler.caller_id_name,
+    role: 'viewer',
+    internalMeetingId: self.internalMeetingId,
+    voiceBridge: self.voiceBridge,
+    callerName : self.caller_id_name,
     sdpOffer : offerSdp
   };
 
   console.log("onOfferViewer sending to screenshare server => " + JSON.stringify(message, null, 2));
-  kurentoHandler.sendMessage(message);
+  this.sendMessage(message);
 };
 
-Kurento.prototype.ping = function() {
-   var message = {
-    id : 'ping',
-    type: 'screenshare',
-    internalMeetingId: kurentoHandler.internalMeetingId,
-    voiceBridge: kurentoHandler.voiceBridge,
-    callerName : kurentoHandler.caller_id_name,
-  };
-
-  kurentoHandler.sendMessage(message);
-}
-
 Kurento.prototype.stop = function() {
-  if (this.webRtcPeer) {
-    var message = {
-      id : 'stop',
-      type : 'screenshare',
-      voiceBridge: kurentoHandler.voiceBridge
-    }
-    kurentoHandler.sendMessage(message);
-    kurentoHandler.disposeScreenShare();
-  }
+  //if (this.webRtcPeer) {
+  //  var message = {
+  //    id : 'stop',
+  //    type : 'screenshare',
+  //    voiceBridge: kurentoHandler.voiceBridge
+  //  }
+  //  kurentoHandler.sendMessage(message);
+  //  kurentoHandler.disposeScreenShare();
+  //}
 }
 
 Kurento.prototype.dispose = function() {
@@ -360,19 +399,6 @@ Kurento.prototype.logError = function (obj) {
   console.error(obj);
 };
 
-Kurento.prototype.getChromeScreenConstraints = function(callback, extensionId) {
-  chrome.runtime.sendMessage(extensionId, {
-    getStream: true,
-    sources: [
-      "window",
-      "screen",
-      "tab"
-    ]},
-    function(response) {
-      console.log(response);
-      callback(response);
-    });
-};
 
 Kurento.normalizeCallback = function (callback) {
   if (typeof callback == 'function') {
@@ -389,30 +415,42 @@ Kurento.normalizeCallback = function (callback) {
 
 // this function explains how to use above methods/objects
 window.getScreenConstraints = function(sendSource, callback) {
-  var _this = this;
-  var chromeMediaSourceId = sendSource;
-  if(isChrome) {
-    kurentoHandler.getChromeScreenConstraints (function (constraints) {
+  let chromeMediaSourceId = sendSource;
+  let screenConstraints = {video: {}};
 
-      var sourceId = constraints.streamId;
+  if(isChrome) {
+    getChromeScreenConstraints ((constraints) => {
+      let sourceId = constraints.streamId;
 
       // this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
-      kurentoHandler.screenConstraints.video.chromeMediaSource = { exact: [sendSource]};
-      kurentoHandler.screenConstraints.video.chromeMediaSourceId= sourceId;
-      console.log("getScreenConstraints for Chrome returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2));
+      screenConstraints.video.chromeMediaSource = { exact: [sendSource]};
+      screenConstraints.video.chromeMediaSourceId = sourceId;
+      console.log("getScreenConstraints for Chrome returns => ");
+      console.log(screenConstraints);
       // now invoking native getUserMedia API
-      callback(null, kurentoHandler.screenConstraints);
+      callback(null, screenConstraints);
 
-    }, kurentoHandler.chromeExtension);
+    }, chromeExtension);
   }
   else if (isFirefox) {
-    kurentoHandler.screenConstraints.video.mediaSource= "screen";
-    kurentoHandler.screenConstraints.video.width= {max: kurentoHandler.vid_width};
-    kurentoHandler.screenConstraints.video.height = {max:  kurentoHandler.vid_height};
+    screenConstraints.video.mediaSource= "window";
+    screenConstraints.video.width= {max: "1280"};
+    screenConstraints.video.height = {max:  "720"};
 
-    console.log("getScreenConstraints for Firefox returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2));
+    console.log("getScreenConstraints for Firefox returns => ");
+    console.log(screenConstraints);
     // now invoking native getUserMedia API
-    callback(null, kurentoHandler.screenConstraints);
+    callback(null, screenConstraints);
+  }
+  else if(isSafari) {
+    screenConstraints.video.mediaSource= "screen";
+    screenConstraints.video.width= {max: window.screen.width};
+    screenConstraints.video.height = {max:  window.screen.vid_height};
+
+    console.log("getScreenConstraints for Safari returns => ");
+    console.log(screenConstraints);
+    // now invoking native getUserMedia API
+    callback(null, screenConstraints);
   }
 }
 
@@ -437,3 +475,22 @@ window.kurentoWatchVideo = function () {
   window.kurentoInitialize();
   window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments);
 };
+
+window.kurentoExitVideo = function () {
+  window.kurentoInitialize();
+  window.kurentoManager.exitVideo();
+}
+
+window.getChromeScreenConstraints = function(callback, extensionId) {
+  chrome.runtime.sendMessage(extensionId, {
+    getStream: true,
+    sources: [
+      "window",
+      "screen",
+      "tab"
+    ]},
+    function(response) {
+      console.log(response);
+      callback(response);
+    });
+};;
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..d171093e8784054a9f42264a0dccb960c5c25d10
--- /dev/null
+++ b/bigbluebutton-client/resources/prod/lib/kurento-utils.js
@@ -0,0 +1,4362 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kurentoUtils = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var freeice = require('freeice');
+var inherits = require('inherits');
+var UAParser = require('ua-parser-js');
+var uuid = require('uuid');
+var hark = require('hark');
+var EventEmitter = require('events').EventEmitter;
+var recursive = require('merge').recursive.bind(undefined, true);
+var sdpTranslator = require('sdp-translator');
+var logger = window.Logger || console;
+try {
+    require('kurento-browser-extensions');
+} catch (error) {
+    if (typeof getScreenConstraints === 'undefined') {
+        logger.warn('screen sharing is not available');
+        getScreenConstraints = function getScreenConstraints(sendSource, callback) {
+            callback(new Error('This library is not enabled for screen sharing'));
+        };
+    }
+}
+var MEDIA_CONSTRAINTS = {
+        audio: true,
+        video: {
+            width: 640,
+            framerate: 15
+        }
+    };
+var ua = window && window.navigator ? window.navigator.userAgent : '';
+var parser = new UAParser(ua);
+var browser = parser.getBrowser();
+var usePlanB = false;
+if (browser.name === 'Chrome' || browser.name === 'Chromium') {
+    logger.debug(browser.name + ': using SDP PlanB');
+    usePlanB = true;
+}
+function noop(error) {
+    if (error)
+        logger.error(error);
+}
+function trackStop(track) {
+    track.stop && track.stop();
+}
+function streamStop(stream) {
+    stream.getTracks().forEach(trackStop);
+}
+var dumpSDP = function (description) {
+    if (typeof description === 'undefined' || description === null) {
+        return '';
+    }
+    return 'type: ' + description.type + '\r\n' + description.sdp;
+};
+function bufferizeCandidates(pc, onerror) {
+    var candidatesQueue = [];
+    pc.addEventListener('signalingstatechange', function () {
+        if (this.signalingState === 'stable') {
+            while (candidatesQueue.length) {
+                var entry = candidatesQueue.shift();
+                this.addIceCandidate(entry.candidate, entry.callback, entry.callback);
+            }
+        }
+    });
+    return function (candidate, callback) {
+        callback = callback || onerror;
+        switch (pc.signalingState) {
+        case 'closed':
+            callback(new Error('PeerConnection object is closed'));
+            break;
+        case 'stable':
+            if (pc.remoteDescription) {
+                pc.addIceCandidate(candidate, callback, callback);
+                break;
+            }
+        default:
+            candidatesQueue.push({
+                candidate: candidate,
+                callback: callback
+            });
+        }
+    };
+}
+function removeFIDFromOffer(sdp) {
+    var n = sdp.indexOf('a=ssrc-group:FID');
+    if (n > 0) {
+        return sdp.slice(0, n);
+    } else {
+        return sdp;
+    }
+}
+function getSimulcastInfo(videoStream) {
+    var videoTracks = videoStream.getVideoTracks();
+    if (!videoTracks.length) {
+        logger.warn('No video tracks available in the video stream');
+        return '';
+    }
+    var lines = [
+            'a=x-google-flag:conference',
+            'a=ssrc-group:SIM 1 2 3',
+            'a=ssrc:1 cname:localVideo',
+            'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:1 mslabel:' + videoStream.id,
+            'a=ssrc:1 label:' + videoTracks[0].id,
+            'a=ssrc:2 cname:localVideo',
+            'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:2 mslabel:' + videoStream.id,
+            'a=ssrc:2 label:' + videoTracks[0].id,
+            'a=ssrc:3 cname:localVideo',
+            'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
+            'a=ssrc:3 mslabel:' + videoStream.id,
+            'a=ssrc:3 label:' + videoTracks[0].id
+        ];
+    lines.push('');
+    return lines.join('\n');
+}
+function WebRtcPeer(mode, options, callback) {
+    if (!(this instanceof WebRtcPeer)) {
+        return new WebRtcPeer(mode, options, callback);
+    }
+    WebRtcPeer.super_.call(this);
+    if (options instanceof Function) {
+        callback = options;
+        options = undefined;
+    }
+    options = options || {};
+    callback = (callback || noop).bind(this);
+    var self = this;
+    var localVideo = options.localVideo;
+    var remoteVideo = options.remoteVideo;
+    var videoStream = options.videoStream;
+    var audioStream = options.audioStream;
+    var mediaConstraints = options.mediaConstraints;
+    var connectionConstraints = options.connectionConstraints;
+    var pc = options.peerConnection;
+    var sendSource = options.sendSource || 'webcam';
+    var dataChannelConfig = options.dataChannelConfig;
+    var useDataChannels = options.dataChannels || false;
+    var dataChannel;
+    var guid = uuid.v4();
+    var configuration = recursive({ iceServers: freeice() }, options.configuration);
+    var onicecandidate = options.onicecandidate;
+    if (onicecandidate)
+        this.on('icecandidate', onicecandidate);
+    var oncandidategatheringdone = options.oncandidategatheringdone;
+    if (oncandidategatheringdone) {
+        this.on('candidategatheringdone', oncandidategatheringdone);
+    }
+    var simulcast = options.simulcast;
+    var multistream = options.multistream;
+    var interop = new sdpTranslator.Interop();
+    var candidatesQueueOut = [];
+    var candidategatheringdone = false;
+    Object.defineProperties(this, {
+        'peerConnection': {
+            get: function () {
+                return pc;
+            }
+        },
+        'id': {
+            value: options.id || guid,
+            writable: false
+        },
+        'remoteVideo': {
+            get: function () {
+                return remoteVideo;
+            }
+        },
+        'localVideo': {
+            get: function () {
+                return localVideo;
+            }
+        },
+        'dataChannel': {
+            get: function () {
+                return dataChannel;
+            }
+        },
+        'currentFrame': {
+            get: function () {
+                if (!remoteVideo)
+                    return;
+                if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
+                    throw new Error('No video stream data available');
+                var canvas = document.createElement('canvas');
+                canvas.width = remoteVideo.videoWidth;
+                canvas.height = remoteVideo.videoHeight;
+                canvas.getContext('2d').drawImage(remoteVideo, 0, 0);
+                return canvas;
+            }
+        }
+    });
+    if (!pc) {
+        pc = new RTCPeerConnection(configuration);
+        if (useDataChannels && !dataChannel) {
+            var dcId = 'WebRtcPeer-' + self.id;
+            var dcOptions = undefined;
+            if (dataChannelConfig) {
+                dcId = dataChannelConfig.id || dcId;
+                dcOptions = dataChannelConfig.options;
+            }
+            dataChannel = pc.createDataChannel(dcId, dcOptions);
+            if (dataChannelConfig) {
+                dataChannel.onopen = dataChannelConfig.onopen;
+                dataChannel.onclose = dataChannelConfig.onclose;
+                dataChannel.onmessage = dataChannelConfig.onmessage;
+                dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow;
+                dataChannel.onerror = dataChannelConfig.onerror || noop;
+            }
+        }
+    }
+    pc.addEventListener('icecandidate', function (event) {
+        var candidate = event.candidate;
+        if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) {
+            if (candidate) {
+                var cand;
+                if (multistream && usePlanB) {
+                    cand = interop.candidateToUnifiedPlan(candidate);
+                } else {
+                    cand = candidate;
+                }
+                self.emit('icecandidate', cand);
+                candidategatheringdone = false;
+            } else if (!candidategatheringdone) {
+                self.emit('candidategatheringdone');
+                candidategatheringdone = true;
+            }
+        } else if (!candidategatheringdone) {
+            candidatesQueueOut.push(candidate);
+            if (!candidate)
+                candidategatheringdone = true;
+        }
+    });
+    pc.ontrack = options.onaddstream;
+    pc.onnegotiationneeded = options.onnegotiationneeded;
+    this.on('newListener', function (event, listener) {
+        if (event === 'icecandidate' || event === 'candidategatheringdone') {
+            while (candidatesQueueOut.length) {
+                var candidate = candidatesQueueOut.shift();
+                if (!candidate === (event === 'candidategatheringdone')) {
+                    listener(candidate);
+                }
+            }
+        }
+    });
+    var addIceCandidate = bufferizeCandidates(pc);
+    this.addIceCandidate = function (iceCandidate, callback) {
+        var candidate;
+        if (multistream && usePlanB) {
+            candidate = interop.candidateToPlanB(iceCandidate);
+        } else {
+            candidate = new RTCIceCandidate(iceCandidate);
+        }
+        logger.debug('Remote ICE candidate received', iceCandidate);
+        callback = (callback || noop).bind(this);
+        addIceCandidate(candidate, callback);
+    };
+    this.generateOffer = function (callback) {
+        callback = callback.bind(this);
+        var offerAudio = true;
+        var offerVideo = true;
+        if (mediaConstraints) {
+            offerAudio = typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true;
+            offerVideo = typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true;
+        }
+        var browserDependantConstraints = {
+                offerToReceiveAudio: mode !== 'sendonly' && offerAudio,
+                offerToReceiveVideo: mode !== 'sendonly' && offerVideo
+            };
+        var constraints = browserDependantConstraints;
+        logger.debug('constraints: ' + JSON.stringify(constraints));
+        pc.createOffer(constraints).then(function (offer) {
+            logger.debug('Created SDP offer');
+            offer = mangleSdpToAddSimulcast(offer);
+            return pc.setLocalDescription(offer);
+        }).then(function () {
+            var localDescription = pc.localDescription;
+            logger.debug('Local description set', localDescription.sdp);
+            if (multistream && usePlanB) {
+                localDescription = interop.toUnifiedPlan(localDescription);
+                logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
+            }
+            callback(null, localDescription.sdp, self.processAnswer.bind(self));
+        }).catch(callback);
+    };
+    this.getLocalSessionDescriptor = function () {
+        return pc.localDescription;
+    };
+    this.getRemoteSessionDescriptor = function () {
+        return pc.remoteDescription;
+    };
+    function setRemoteVideo() {
+        if (remoteVideo) {
+            var stream = pc.getRemoteStreams()[0];
+            remoteVideo.pause();
+            remoteVideo.srcObject = stream;
+            remoteVideo.load();
+            logger.info('Remote URL:', remoteVideo.srcObject);
+        }
+    }
+    this.showLocalVideo = function () {
+        localVideo.srcObject = videoStream;
+        localVideo.muted = true;
+    };
+    this.send = function (data) {
+        if (dataChannel && dataChannel.readyState === 'open') {
+            dataChannel.send(data);
+        } else {
+            logger.warn('Trying to send data over a non-existing or closed data channel');
+        }
+    };
+    this.processAnswer = function (sdpAnswer, callback) {
+        callback = (callback || noop).bind(this);
+        var answer = new RTCSessionDescription({
+                type: 'answer',
+                sdp: sdpAnswer
+            });
+        if (multistream && usePlanB) {
+            var planBAnswer = interop.toPlanB(answer);
+            logger.debug('asnwer::planB', dumpSDP(planBAnswer));
+            answer = planBAnswer;
+        }
+        logger.debug('SDP answer received, setting remote description');
+        if (pc.signalingState === 'closed') {
+            return callback('PeerConnection is closed');
+        }
+        pc.setRemoteDescription(answer, function () {
+            setRemoteVideo();
+            callback();
+        }, callback);
+    };
+    this.processOffer = function (sdpOffer, callback) {
+        callback = callback.bind(this);
+        var offer = new RTCSessionDescription({
+                type: 'offer',
+                sdp: sdpOffer
+            });
+        if (multistream && usePlanB) {
+            var planBOffer = interop.toPlanB(offer);
+            logger.debug('offer::planB', dumpSDP(planBOffer));
+            offer = planBOffer;
+        }
+        logger.debug('SDP offer received, setting remote description');
+        if (pc.signalingState === 'closed') {
+            return callback('PeerConnection is closed');
+        }
+        pc.setRemoteDescription(offer).then(function () {
+            return setRemoteVideo();
+        }).then(function () {
+            return pc.createAnswer();
+        }).then(function (answer) {
+            answer = mangleSdpToAddSimulcast(answer);
+            logger.debug('Created SDP answer');
+            return pc.setLocalDescription(answer);
+        }).then(function () {
+            var localDescription = pc.localDescription;
+            if (multistream && usePlanB) {
+                localDescription = interop.toUnifiedPlan(localDescription);
+                logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
+            }
+            logger.debug('Local description set', localDescription.sdp);
+            callback(null, localDescription.sdp);
+        }).catch(callback);
+    };
+    function mangleSdpToAddSimulcast(answer) {
+        if (simulcast) {
+            if (browser.name === 'Chrome' || browser.name === 'Chromium') {
+                logger.debug('Adding multicast info');
+                answer = new RTCSessionDescription({
+                    'type': answer.type,
+                    'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream)
+                });
+            } else {
+                logger.warn('Simulcast is only available in Chrome browser.');
+            }
+        }
+        return answer;
+    }
+    function start() {
+        if (pc.signalingState === 'closed') {
+            callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
+        }
+        if (videoStream && localVideo) {
+            self.showLocalVideo();
+        }
+        if (videoStream) {
+            pc.addStream(videoStream);
+        }
+        if (audioStream) {
+            pc.addStream(audioStream);
+        }
+        var browser = parser.getBrowser();
+        if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) {
+            mode = 'sendrecv';
+        }
+        callback();
+    }
+    if (mode !== 'recvonly' && !videoStream && !audioStream) {
+        function getMedia(constraints) {
+            if (constraints === undefined) {
+                constraints = MEDIA_CONSTRAINTS;
+            }
+            navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
+                videoStream = stream;
+                start();
+            }).catch(callback);
+        }
+        if (sendSource === 'webcam') {
+            getMedia(mediaConstraints);
+        } else {
+            getScreenConstraints(sendSource, function (error, constraints_) {
+                if (error)
+                    return callback(error);
+                constraints = [mediaConstraints];
+                constraints.unshift(constraints_);
+                getMedia(recursive.apply(undefined, constraints));
+            }, guid);
+        }
+    } else {
+        setTimeout(start, 0);
+    }
+    this.on('_dispose', function () {
+        if (localVideo) {
+            localVideo.pause();
+            localVideo.src = '';
+            localVideo.load();
+            localVideo.muted = false;
+        }
+        if (remoteVideo) {
+            remoteVideo.pause();
+            remoteVideo.src = '';
+            remoteVideo.load();
+        }
+        self.removeAllListeners();
+        if (window.cancelChooseDesktopMedia !== undefined) {
+            window.cancelChooseDesktopMedia(guid);
+        }
+    });
+}
+inherits(WebRtcPeer, EventEmitter);
+function createEnableDescriptor(type) {
+    var method = 'get' + type + 'Tracks';
+    return {
+        enumerable: true,
+        get: function () {
+            if (!this.peerConnection)
+                return;
+            var streams = this.peerConnection.getLocalStreams();
+            if (!streams.length)
+                return;
+            for (var i = 0, stream; stream = streams[i]; i++) {
+                var tracks = stream[method]();
+                for (var j = 0, track; track = tracks[j]; j++)
+                    if (!track.enabled)
+                        return false;
+            }
+            return true;
+        },
+        set: function (value) {
+            function trackSetEnable(track) {
+                track.enabled = value;
+            }
+            this.peerConnection.getLocalStreams().forEach(function (stream) {
+                stream[method]().forEach(trackSetEnable);
+            });
+        }
+    };
+}
+Object.defineProperties(WebRtcPeer.prototype, {
+    'enabled': {
+        enumerable: true,
+        get: function () {
+            return this.audioEnabled && this.videoEnabled;
+        },
+        set: function (value) {
+            this.audioEnabled = this.videoEnabled = value;
+        }
+    },
+    'audioEnabled': createEnableDescriptor('Audio'),
+    'videoEnabled': createEnableDescriptor('Video')
+});
+WebRtcPeer.prototype.getLocalStream = function (index) {
+    if (this.peerConnection) {
+        return this.peerConnection.getLocalStreams()[index || 0];
+    }
+};
+WebRtcPeer.prototype.getRemoteStream = function (index) {
+    if (this.peerConnection) {
+        return this.peerConnection.getRemoteStreams()[index || 0];
+    }
+};
+WebRtcPeer.prototype.dispose = function () {
+    logger.debug('Disposing WebRtcPeer');
+    var pc = this.peerConnection;
+    var dc = this.dataChannel;
+    try {
+        if (dc) {
+            if (dc.signalingState === 'closed')
+                return;
+            dc.close();
+        }
+        if (pc) {
+            if (pc.signalingState === 'closed')
+                return;
+            pc.getLocalStreams().forEach(streamStop);
+            pc.close();
+        }
+    } catch (err) {
+        logger.warn('Exception disposing webrtc peer ' + err);
+    }
+    this.emit('_dispose');
+};
+function WebRtcPeerRecvonly(options, callback) {
+    if (!(this instanceof WebRtcPeerRecvonly)) {
+        return new WebRtcPeerRecvonly(options, callback);
+    }
+    WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback);
+}
+inherits(WebRtcPeerRecvonly, WebRtcPeer);
+function WebRtcPeerSendonly(options, callback) {
+    if (!(this instanceof WebRtcPeerSendonly)) {
+        return new WebRtcPeerSendonly(options, callback);
+    }
+    WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback);
+}
+inherits(WebRtcPeerSendonly, WebRtcPeer);
+function WebRtcPeerSendrecv(options, callback) {
+    if (!(this instanceof WebRtcPeerSendrecv)) {
+        return new WebRtcPeerSendrecv(options, callback);
+    }
+    WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback);
+}
+inherits(WebRtcPeerSendrecv, WebRtcPeer);
+function harkUtils(stream, options) {
+    return hark(stream, options);
+}
+exports.bufferizeCandidates = bufferizeCandidates;
+exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly;
+exports.WebRtcPeerSendonly = WebRtcPeerSendonly;
+exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv;
+exports.hark = harkUtils;
+},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){
+if (window.addEventListener)
+    module.exports = require('./index');
+},{"./index":3}],3:[function(require,module,exports){
+var WebRtcPeer = require('./WebRtcPeer');
+exports.WebRtcPeer = WebRtcPeer;
+},{"./WebRtcPeer":1}],4:[function(require,module,exports){
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// 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 EventEmitter() {
+  this._events = this._events || {};
+  this._maxListeners = this._maxListeners || undefined;
+}
+module.exports = EventEmitter;
+
+// Backwards-compat with node 0.10.x
+EventEmitter.EventEmitter = EventEmitter;
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._maxListeners = undefined;
+
+// By default EventEmitters will print a warning if more than 10 listeners are
+// added to it. This is a useful default which helps finding memory leaks.
+EventEmitter.defaultMaxListeners = 10;
+
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+EventEmitter.prototype.setMaxListeners = function(n) {
+  if (!isNumber(n) || n < 0 || isNaN(n))
+    throw TypeError('n must be a positive number');
+  this._maxListeners = n;
+  return this;
+};
+
+EventEmitter.prototype.emit = function(type) {
+  var er, handler, len, args, i, listeners;
+
+  if (!this._events)
+    this._events = {};
+
+  // If there is no 'error' event listener then throw.
+  if (type === 'error') {
+    if (!this._events.error ||
+        (isObject(this._events.error) && !this._events.error.length)) {
+      er = arguments[1];
+      if (er instanceof Error) {
+        throw er; // Unhandled 'error' event
+      } else {
+        // At least give some kind of context to the user
+        var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
+        err.context = er;
+        throw err;
+      }
+    }
+  }
+
+  handler = this._events[type];
+
+  if (isUndefined(handler))
+    return false;
+
+  if (isFunction(handler)) {
+    switch (arguments.length) {
+      // fast cases
+      case 1:
+        handler.call(this);
+        break;
+      case 2:
+        handler.call(this, arguments[1]);
+        break;
+      case 3:
+        handler.call(this, arguments[1], arguments[2]);
+        break;
+      // slower
+      default:
+        args = Array.prototype.slice.call(arguments, 1);
+        handler.apply(this, args);
+    }
+  } else if (isObject(handler)) {
+    args = Array.prototype.slice.call(arguments, 1);
+    listeners = handler.slice();
+    len = listeners.length;
+    for (i = 0; i < len; i++)
+      listeners[i].apply(this, args);
+  }
+
+  return true;
+};
+
+EventEmitter.prototype.addListener = function(type, listener) {
+  var m;
+
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  if (!this._events)
+    this._events = {};
+
+  // To avoid recursion in the case that type === "newListener"! Before
+  // adding it to the listeners, first emit "newListener".
+  if (this._events.newListener)
+    this.emit('newListener', type,
+              isFunction(listener.listener) ?
+              listener.listener : listener);
+
+  if (!this._events[type])
+    // Optimize the case of one listener. Don't need the extra array object.
+    this._events[type] = listener;
+  else if (isObject(this._events[type]))
+    // If we've already got an array, just append.
+    this._events[type].push(listener);
+  else
+    // Adding the second element, need to change to array.
+    this._events[type] = [this._events[type], listener];
+
+  // Check for listener leak
+  if (isObject(this._events[type]) && !this._events[type].warned) {
+    if (!isUndefined(this._maxListeners)) {
+      m = this._maxListeners;
+    } else {
+      m = EventEmitter.defaultMaxListeners;
+    }
+
+    if (m && m > 0 && this._events[type].length > m) {
+      this._events[type].warned = true;
+      console.error('(node) warning: possible EventEmitter memory ' +
+                    'leak detected. %d listeners added. ' +
+                    'Use emitter.setMaxListeners() to increase limit.',
+                    this._events[type].length);
+      if (typeof console.trace === 'function') {
+        // not supported in IE 10
+        console.trace();
+      }
+    }
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  var fired = false;
+
+  function g() {
+    this.removeListener(type, g);
+
+    if (!fired) {
+      fired = true;
+      listener.apply(this, arguments);
+    }
+  }
+
+  g.listener = listener;
+  this.on(type, g);
+
+  return this;
+};
+
+// emits a 'removeListener' event iff the listener was removed
+EventEmitter.prototype.removeListener = function(type, listener) {
+  var list, position, length, i;
+
+  if (!isFunction(listener))
+    throw TypeError('listener must be a function');
+
+  if (!this._events || !this._events[type])
+    return this;
+
+  list = this._events[type];
+  length = list.length;
+  position = -1;
+
+  if (list === listener ||
+      (isFunction(list.listener) && list.listener === listener)) {
+    delete this._events[type];
+    if (this._events.removeListener)
+      this.emit('removeListener', type, listener);
+
+  } else if (isObject(list)) {
+    for (i = length; i-- > 0;) {
+      if (list[i] === listener ||
+          (list[i].listener && list[i].listener === listener)) {
+        position = i;
+        break;
+      }
+    }
+
+    if (position < 0)
+      return this;
+
+    if (list.length === 1) {
+      list.length = 0;
+      delete this._events[type];
+    } else {
+      list.splice(position, 1);
+    }
+
+    if (this._events.removeListener)
+      this.emit('removeListener', type, listener);
+  }
+
+  return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+  var key, listeners;
+
+  if (!this._events)
+    return this;
+
+  // not listening for removeListener, no need to emit
+  if (!this._events.removeListener) {
+    if (arguments.length === 0)
+      this._events = {};
+    else if (this._events[type])
+      delete this._events[type];
+    return this;
+  }
+
+  // emit removeListener for all listeners on all events
+  if (arguments.length === 0) {
+    for (key in this._events) {
+      if (key === 'removeListener') continue;
+      this.removeAllListeners(key);
+    }
+    this.removeAllListeners('removeListener');
+    this._events = {};
+    return this;
+  }
+
+  listeners = this._events[type];
+
+  if (isFunction(listeners)) {
+    this.removeListener(type, listeners);
+  } else if (listeners) {
+    // LIFO order
+    while (listeners.length)
+      this.removeListener(type, listeners[listeners.length - 1]);
+  }
+  delete this._events[type];
+
+  return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+  var ret;
+  if (!this._events || !this._events[type])
+    ret = [];
+  else if (isFunction(this._events[type]))
+    ret = [this._events[type]];
+  else
+    ret = this._events[type].slice();
+  return ret;
+};
+
+EventEmitter.prototype.listenerCount = function(type) {
+  if (this._events) {
+    var evlistener = this._events[type];
+
+    if (isFunction(evlistener))
+      return 1;
+    else if (evlistener)
+      return evlistener.length;
+  }
+  return 0;
+};
+
+EventEmitter.listenerCount = function(emitter, type) {
+  return emitter.listenerCount(type);
+};
+
+function isFunction(arg) {
+  return typeof arg === 'function';
+}
+
+function isNumber(arg) {
+  return typeof arg === 'number';
+}
+
+function isObject(arg) {
+  return typeof arg === 'object' && arg !== null;
+}
+
+function isUndefined(arg) {
+  return arg === void 0;
+}
+
+},{}],5:[function(require,module,exports){
+/* jshint node: true */
+'use strict';
+
+var normalice = require('normalice');
+
+/**
+  # freeice
+
+  The `freeice` module is a simple way of getting random STUN or TURN server
+  for your WebRTC application.  The list of servers (just STUN at this stage)
+  were sourced from this [gist](https://gist.github.com/zziuni/3741933).
+
+  ## Example Use
+
+  The following demonstrates how you can use `freeice` with
+  [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect):
+
+  <<< examples/quickconnect.js
+
+  As the `freeice` module generates ice servers in a list compliant with the
+  WebRTC spec you will be able to use it with raw `RTCPeerConnection`
+  constructors and other WebRTC libraries.
+
+  ## Hey, don't use my STUN/TURN server!
+
+  If for some reason your free STUN or TURN server ends up in the
+  list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or
+  [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json))
+  that is used in this module, you can feel
+  free to open an issue on this repository and those servers will be removed
+  within 24 hours (or sooner).  This is the quickest and probably the most
+  polite way to have something removed (and provides us some visibility
+  if someone opens a pull request requesting that a server is added).
+
+  ## Please add my server!
+
+  If you have a server that you wish to add to the list, that's awesome! I'm
+  sure I speak on behalf of a whole pile of WebRTC developers who say thanks.
+  To get it into the list, feel free to either open a pull request or if you
+  find that process a bit daunting then just create an issue requesting
+  the addition of the server (make sure you provide all the details, and if
+  you have a Terms of Service then including that in the PR/issue would be
+  awesome).
+
+  ## I know of a free server, can I add it?
+
+  Sure, if you do your homework and make sure it is ok to use (I'm currently
+  in the process of reviewing the terms of those STUN servers included from
+  the original list).  If it's ok to go, then please see the previous entry
+  for how to add it.
+
+  ## Current List of Servers
+
+  * current as at the time of last `README.md` file generation
+
+  ### STUN
+
+  <<< stun.json
+
+  ### TURN
+
+  <<< turn.json
+
+**/
+
+var freeice = module.exports = function(opts) {
+  // if a list of servers has been provided, then use it instead of defaults
+  var servers = {
+    stun: (opts || {}).stun || require('./stun.json'),
+    turn: (opts || {}).turn || require('./turn.json')
+  };
+
+  var stunCount = (opts || {}).stunCount || 2;
+  var turnCount = (opts || {}).turnCount || 0;
+  var selected;
+
+  function getServers(type, count) {
+    var out = [];
+    var input = [].concat(servers[type]);
+    var idx;
+
+    while (input.length && out.length < count) {
+      idx = (Math.random() * input.length) | 0;
+      out = out.concat(input.splice(idx, 1));
+    }
+
+    return out.map(function(url) {
+        //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up
+        if ((typeof url !== 'string') && (! (url instanceof String))) {
+            return url;
+        } else {
+            return normalice(type + ':' + url);
+        }
+    });
+  }
+
+  // add stun servers
+  selected = [].concat(getServers('stun', stunCount));
+
+  if (turnCount) {
+    selected = selected.concat(getServers('turn', turnCount));
+  }
+
+  return selected;
+};
+
+},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){
+module.exports=[
+  "stun.l.google.com:19302",
+  "stun1.l.google.com:19302",
+  "stun2.l.google.com:19302",
+  "stun3.l.google.com:19302",
+  "stun4.l.google.com:19302",
+  "stun.ekiga.net",
+  "stun.ideasip.com",
+  "stun.schlund.de",
+  "stun.stunprotocol.org:3478",
+  "stun.voiparound.com",
+  "stun.voipbuster.com",
+  "stun.voipstunt.com",
+  "stun.voxgratia.org",
+  "stun.services.mozilla.com"
+]
+
+},{}],7:[function(require,module,exports){
+module.exports=[]
+
+},{}],8:[function(require,module,exports){
+var WildEmitter = require('wildemitter');
+
+function getMaxVolume (analyser, fftBins) {
+  var maxVolume = -Infinity;
+  analyser.getFloatFrequencyData(fftBins);
+
+  for(var i=4, ii=fftBins.length; i < ii; i++) {
+    if (fftBins[i] > maxVolume && fftBins[i] < 0) {
+      maxVolume = fftBins[i];
+    }
+  };
+
+  return maxVolume;
+}
+
+
+var audioContextType = window.AudioContext || window.webkitAudioContext;
+// use a single audio context due to hardware limits
+var audioContext = null;
+module.exports = function(stream, options) {
+  var harker = new WildEmitter();
+
+
+  // make it not break in non-supported browsers
+  if (!audioContextType) return harker;
+
+  //Config
+  var options = options || {},
+      smoothing = (options.smoothing || 0.1),
+      interval = (options.interval || 50),
+      threshold = options.threshold,
+      play = options.play,
+      history = options.history || 10,
+      running = true;
+
+  //Setup Audio Context
+  if (!audioContext) {
+    audioContext = new audioContextType();
+  }
+  var sourceNode, fftBins, analyser;
+
+  analyser = audioContext.createAnalyser();
+  analyser.fftSize = 512;
+  analyser.smoothingTimeConstant = smoothing;
+  fftBins = new Float32Array(analyser.fftSize);
+
+  if (stream.jquery) stream = stream[0];
+  if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
+    //Audio Tag
+    sourceNode = audioContext.createMediaElementSource(stream);
+    if (typeof play === 'undefined') play = true;
+    threshold = threshold || -50;
+  } else {
+    //WebRTC Stream
+    sourceNode = audioContext.createMediaStreamSource(stream);
+    threshold = threshold || -50;
+  }
+
+  sourceNode.connect(analyser);
+  if (play) analyser.connect(audioContext.destination);
+
+  harker.speaking = false;
+
+  harker.setThreshold = function(t) {
+    threshold = t;
+  };
+
+  harker.setInterval = function(i) {
+    interval = i;
+  };
+  
+  harker.stop = function() {
+    running = false;
+    harker.emit('volume_change', -100, threshold);
+    if (harker.speaking) {
+      harker.speaking = false;
+      harker.emit('stopped_speaking');
+    }
+  };
+  harker.speakingHistory = [];
+  for (var i = 0; i < history; i++) {
+      harker.speakingHistory.push(0);
+  }
+
+  // Poll the analyser node to determine if speaking
+  // and emit events if changed
+  var looper = function() {
+    setTimeout(function() {
+    
+      //check if stop has been called
+      if(!running) {
+        return;
+      }
+      
+      var currentVolume = getMaxVolume(analyser, fftBins);
+
+      harker.emit('volume_change', currentVolume, threshold);
+
+      var history = 0;
+      if (currentVolume > threshold && !harker.speaking) {
+        // trigger quickly, short history
+        for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
+          history += harker.speakingHistory[i];
+        }
+        if (history >= 2) {
+          harker.speaking = true;
+          harker.emit('speaking');
+        }
+      } else if (currentVolume < threshold && harker.speaking) {
+        for (var i = 0; i < harker.speakingHistory.length; i++) {
+          history += harker.speakingHistory[i];
+        }
+        if (history == 0) {
+          harker.speaking = false;
+          harker.emit('stopped_speaking');
+        }
+      }
+      harker.speakingHistory.shift();
+      harker.speakingHistory.push(0 + (currentVolume > threshold));
+
+      looper();
+    }, interval);
+  };
+  looper();
+
+
+  return harker;
+}
+
+},{"wildemitter":24}],9:[function(require,module,exports){
+if (typeof Object.create === 'function') {
+  // implementation from standard node.js 'util' module
+  module.exports = function inherits(ctor, superCtor) {
+    ctor.super_ = superCtor
+    ctor.prototype = Object.create(superCtor.prototype, {
+      constructor: {
+        value: ctor,
+        enumerable: false,
+        writable: true,
+        configurable: true
+      }
+    });
+  };
+} else {
+  // old school shim for old browsers
+  module.exports = function inherits(ctor, superCtor) {
+    ctor.super_ = superCtor
+    var TempCtor = function () {}
+    TempCtor.prototype = superCtor.prototype
+    ctor.prototype = new TempCtor()
+    ctor.prototype.constructor = ctor
+  }
+}
+
+},{}],10:[function(require,module,exports){
+// Does nothing at all.
+
+},{}],11:[function(require,module,exports){
+/*!
+ * @name JavaScript/NodeJS Merge v1.2.0
+ * @author yeikos
+ * @repository https://github.com/yeikos/js.merge
+
+ * Copyright 2014 yeikos - MIT license
+ * https://raw.github.com/yeikos/js.merge/master/LICENSE
+ */
+
+;(function(isNode) {
+
+	/**
+	 * Merge one or more objects 
+	 * @param bool? clone
+	 * @param mixed,... arguments
+	 * @return object
+	 */
+
+	var Public = function(clone) {
+
+		return merge(clone === true, false, arguments);
+
+	}, publicName = 'merge';
+
+	/**
+	 * Merge two or more objects recursively 
+	 * @param bool? clone
+	 * @param mixed,... arguments
+	 * @return object
+	 */
+
+	Public.recursive = function(clone) {
+
+		return merge(clone === true, true, arguments);
+
+	};
+
+	/**
+	 * Clone the input removing any reference
+	 * @param mixed input
+	 * @return mixed
+	 */
+
+	Public.clone = function(input) {
+
+		var output = input,
+			type = typeOf(input),
+			index, size;
+
+		if (type === 'array') {
+
+			output = [];
+			size = input.length;
+
+			for (index=0;index<size;++index)
+
+				output[index] = Public.clone(input[index]);
+
+		} else if (type === 'object') {
+
+			output = {};
+
+			for (index in input)
+
+				output[index] = Public.clone(input[index]);
+
+		}
+
+		return output;
+
+	};
+
+	/**
+	 * Merge two objects recursively
+	 * @param mixed input
+	 * @param mixed extend
+	 * @return mixed
+	 */
+
+	function merge_recursive(base, extend) {
+
+		if (typeOf(base) !== 'object')
+
+			return extend;
+
+		for (var key in extend) {
+
+			if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') {
+
+				base[key] = merge_recursive(base[key], extend[key]);
+
+			} else {
+
+				base[key] = extend[key];
+
+			}
+
+		}
+
+		return base;
+
+	}
+
+	/**
+	 * Merge two or more objects
+	 * @param bool clone
+	 * @param bool recursive
+	 * @param array argv
+	 * @return object
+	 */
+
+	function merge(clone, recursive, argv) {
+
+		var result = argv[0],
+			size = argv.length;
+
+		if (clone || typeOf(result) !== 'object')
+
+			result = {};
+
+		for (var index=0;index<size;++index) {
+
+			var item = argv[index],
+
+				type = typeOf(item);
+
+			if (type !== 'object') continue;
+
+			for (var key in item) {
+
+				var sitem = clone ? Public.clone(item[key]) : item[key];
+
+				if (recursive) {
+
+					result[key] = merge_recursive(result[key], sitem);
+
+				} else {
+
+					result[key] = sitem;
+
+				}
+
+			}
+
+		}
+
+		return result;
+
+	}
+
+	/**
+	 * Get type of variable
+	 * @param mixed input
+	 * @return string
+	 *
+	 * @see http://jsperf.com/typeofvar
+	 */
+
+	function typeOf(input) {
+
+		return ({}).toString.call(input).slice(8, -1).toLowerCase();
+
+	}
+
+	if (isNode) {
+
+		module.exports = Public;
+
+	} else {
+
+		window[publicName] = Public;
+
+	}
+
+})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports);
+},{}],12:[function(require,module,exports){
+/**
+  # normalice
+
+  Normalize an ice server configuration object (or plain old string) into a format
+  that is usable in all browsers supporting WebRTC.  Primarily this module is designed
+  to help with the transition of the `url` attribute of the configuration object to
+  the `urls` attribute.
+
+  ## Example Usage
+
+  <<< examples/simple.js
+
+**/
+
+var protocols = [
+  'stun:',
+  'turn:'
+];
+
+module.exports = function(input) {
+  var url = (input || {}).url || input;
+  var protocol;
+  var parts;
+  var output = {};
+
+  // if we don't have a string url, then allow the input to passthrough
+  if (typeof url != 'string' && (! (url instanceof String))) {
+    return input;
+  }
+
+  // trim the url string, and convert to an array
+  url = url.trim();
+
+  // if the protocol is not known, then passthrough
+  protocol = protocols[protocols.indexOf(url.slice(0, 5))];
+  if (! protocol) {
+    return input;
+  }
+
+  // now let's attack the remaining url parts
+  url = url.slice(5);
+  parts = url.split('@');
+
+  output.username = input.username;
+  output.credential = input.credential;
+  // if we have an authentication part, then set the credentials
+  if (parts.length > 1) {
+    url = parts[1];
+    parts = parts[0].split(':');
+
+    // add the output credential and username
+    output.username = parts[0];
+    output.credential = (input || {}).credential || parts[1] || '';
+  }
+
+  output.url = protocol + url;
+  output.urls = [ output.url ];
+
+  return output;
+};
+
+},{}],13:[function(require,module,exports){
+var grammar = module.exports = {
+  v: [{
+      name: 'version',
+      reg: /^(\d*)$/
+  }],
+  o: [{ //o=- 20518 0 IN IP4 203.0.113.1
+    // NB: sessionId will be a String in most cases because it is huge
+    name: 'origin',
+    reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
+    names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
+    format: "%s %s %d %s IP%d %s"
+  }],
+  // default parsing of these only (though some of these feel outdated)
+  s: [{ name: 'name' }],
+  i: [{ name: 'description' }],
+  u: [{ name: 'uri' }],
+  e: [{ name: 'email' }],
+  p: [{ name: 'phone' }],
+  z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
+  r: [{ name: 'repeats' }],   // TODO: this one can also be parsed properly
+  //k: [{}], // outdated thing ignored
+  t: [{ //t=0 0
+    name: 'timing',
+    reg: /^(\d*) (\d*)/,
+    names: ['start', 'stop'],
+    format: "%d %d"
+  }],
+  c: [{ //c=IN IP4 10.47.197.26
+      name: 'connection',
+      reg: /^IN IP(\d) (\S*)/,
+      names: ['version', 'ip'],
+      format: "IN IP%d %s"
+  }],
+  b: [{ //b=AS:4000
+      push: 'bandwidth',
+      reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
+      names: ['type', 'limit'],
+      format: "%s:%s"
+  }],
+  m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
+      // NB: special - pushes to session
+      // TODO: rtp/fmtp should be filtered by the payloads found here?
+      reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
+      names: ['type', 'port', 'protocol', 'payloads'],
+      format: "%s %d %s %s"
+  }],
+  a: [
+    { //a=rtpmap:110 opus/48000/2
+      push: 'rtp',
+      reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
+      names: ['payload', 'codec', 'rate', 'encoding'],
+      format: function (o) {
+        return (o.encoding) ?
+          "rtpmap:%d %s/%s/%s":
+          o.rate ?
+          "rtpmap:%d %s/%s":
+          "rtpmap:%d %s";
+      }
+    },
+    {
+      //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
+      //a=fmtp:111 minptime=10; useinbandfec=1
+      push: 'fmtp',
+      reg: /^fmtp:(\d*) ([\S| ]*)/,
+      names: ['payload', 'config'],
+      format: "fmtp:%d %s"
+    },
+    { //a=control:streamid=0
+        name: 'control',
+        reg: /^control:(.*)/,
+        format: "control:%s"
+    },
+    { //a=rtcp:65179 IN IP4 193.84.77.194
+      name: 'rtcp',
+      reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
+      names: ['port', 'netType', 'ipVer', 'address'],
+      format: function (o) {
+        return (o.address != null) ?
+          "rtcp:%d %s IP%d %s":
+          "rtcp:%d";
+      }
+    },
+    { //a=rtcp-fb:98 trr-int 100
+      push: 'rtcpFbTrrInt',
+      reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
+      names: ['payload', 'value'],
+      format: "rtcp-fb:%d trr-int %d"
+    },
+    { //a=rtcp-fb:98 nack rpsi
+      push: 'rtcpFb',
+      reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
+      names: ['payload', 'type', 'subtype'],
+      format: function (o) {
+        return (o.subtype != null) ?
+          "rtcp-fb:%s %s %s":
+          "rtcp-fb:%s %s";
+      }
+    },
+    { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+      //a=extmap:1/recvonly URI-gps-string
+      push: 'ext',
+      reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
+      names: ['value', 'uri', 'config'], // value may include "/direction" suffix
+      format: function (o) {
+        return (o.config != null) ?
+          "extmap:%s %s %s":
+          "extmap:%s %s";
+      }
+    },
+    {
+      //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
+      push: 'crypto',
+      reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
+      names: ['id', 'suite', 'config', 'sessionConfig'],
+      format: function (o) {
+        return (o.sessionConfig != null) ?
+          "crypto:%d %s %s %s":
+          "crypto:%d %s %s";
+      }
+    },
+    { //a=setup:actpass
+      name: 'setup',
+      reg: /^setup:(\w*)/,
+      format: "setup:%s"
+    },
+    { //a=mid:1
+      name: 'mid',
+      reg: /^mid:([^\s]*)/,
+      format: "mid:%s"
+    },
+    { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
+      name: 'msid',
+      reg: /^msid:(.*)/,
+      format: "msid:%s"
+    },
+    { //a=ptime:20
+      name: 'ptime',
+      reg: /^ptime:(\d*)/,
+      format: "ptime:%d"
+    },
+    { //a=maxptime:60
+      name: 'maxptime',
+      reg: /^maxptime:(\d*)/,
+      format: "maxptime:%d"
+    },
+    { //a=sendrecv
+      name: 'direction',
+      reg: /^(sendrecv|recvonly|sendonly|inactive)/
+    },
+    { //a=ice-lite
+      name: 'icelite',
+      reg: /^(ice-lite)/
+    },
+    { //a=ice-ufrag:F7gI
+      name: 'iceUfrag',
+      reg: /^ice-ufrag:(\S*)/,
+      format: "ice-ufrag:%s"
+    },
+    { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
+      name: 'icePwd',
+      reg: /^ice-pwd:(\S*)/,
+      format: "ice-pwd:%s"
+    },
+    { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
+      name: 'fingerprint',
+      reg: /^fingerprint:(\S*) (\S*)/,
+      names: ['type', 'hash'],
+      format: "fingerprint:%s %s"
+    },
+    {
+      //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
+      //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
+      //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
+      //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
+      //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
+      push:'candidates',
+      reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
+      names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
+      format: function (o) {
+        var str = "candidate:%s %d %s %d %s %d typ %s";
+
+        str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
+
+        // NB: candidate has three optional chunks, so %void middles one if it's missing
+        str += (o.tcptype != null) ? " tcptype %s" : "%v";
+
+        if (o.generation != null) {
+          str += " generation %d";
+        }
+        return str;
+      }
+    },
+    { //a=end-of-candidates (keep after the candidates line for readability)
+      name: 'endOfCandidates',
+      reg: /^(end-of-candidates)/
+    },
+    { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
+      name: 'remoteCandidates',
+      reg: /^remote-candidates:(.*)/,
+      format: "remote-candidates:%s"
+    },
+    { //a=ice-options:google-ice
+      name: 'iceOptions',
+      reg: /^ice-options:(\S*)/,
+      format: "ice-options:%s"
+    },
+    { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
+      push: "ssrcs",
+      reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
+      names: ['id', 'attribute', 'value'],
+      format: "ssrc:%d %s:%s"
+    },
+    { //a=ssrc-group:FEC 1 2
+      push: "ssrcGroups",
+      reg: /^ssrc-group:(\w*) (.*)/,
+      names: ['semantics', 'ssrcs'],
+      format: "ssrc-group:%s %s"
+    },
+    { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
+      name: "msidSemantic",
+      reg: /^msid-semantic:\s?(\w*) (\S*)/,
+      names: ['semantic', 'token'],
+      format: "msid-semantic: %s %s" // space after ":" is not accidental
+    },
+    { //a=group:BUNDLE audio video
+      push: 'groups',
+      reg: /^group:(\w*) (.*)/,
+      names: ['type', 'mids'],
+      format: "group:%s %s"
+    },
+    { //a=rtcp-mux
+      name: 'rtcpMux',
+      reg: /^(rtcp-mux)/
+    },
+    { //a=rtcp-rsize
+      name: 'rtcpRsize',
+      reg: /^(rtcp-rsize)/
+    },
+    { // any a= that we don't understand is kepts verbatim on media.invalid
+      push: 'invalid',
+      names: ["value"]
+    }
+  ]
+};
+
+// set sensible defaults to avoid polluting the grammar with boring details
+Object.keys(grammar).forEach(function (key) {
+  var objs = grammar[key];
+  objs.forEach(function (obj) {
+    if (!obj.reg) {
+      obj.reg = /(.*)/;
+    }
+    if (!obj.format) {
+      obj.format = "%s";
+    }
+  });
+});
+
+},{}],14:[function(require,module,exports){
+var parser = require('./parser');
+var writer = require('./writer');
+
+exports.write = writer;
+exports.parse = parser.parse;
+exports.parseFmtpConfig = parser.parseFmtpConfig;
+exports.parsePayloads = parser.parsePayloads;
+exports.parseRemoteCandidates = parser.parseRemoteCandidates;
+
+},{"./parser":15,"./writer":16}],15:[function(require,module,exports){
+var toIntIfInt = function (v) {
+  return String(Number(v)) === v ? Number(v) : v;
+};
+
+var attachProperties = function (match, location, names, rawName) {
+  if (rawName && !names) {
+    location[rawName] = toIntIfInt(match[1]);
+  }
+  else {
+    for (var i = 0; i < names.length; i += 1) {
+      if (match[i+1] != null) {
+        location[names[i]] = toIntIfInt(match[i+1]);
+      }
+    }
+  }
+};
+
+var parseReg = function (obj, location, content) {
+  var needsBlank = obj.name && obj.names;
+  if (obj.push && !location[obj.push]) {
+    location[obj.push] = [];
+  }
+  else if (needsBlank && !location[obj.name]) {
+    location[obj.name] = {};
+  }
+  var keyLocation = obj.push ?
+    {} :  // blank object that will be pushed
+    needsBlank ? location[obj.name] : location; // otherwise, named location or root
+
+  attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
+
+  if (obj.push) {
+    location[obj.push].push(keyLocation);
+  }
+};
+
+var grammar = require('./grammar');
+var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
+
+exports.parse = function (sdp) {
+  var session = {}
+    , media = []
+    , location = session; // points at where properties go under (one of the above)
+
+  // parse lines we understand
+  sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
+    var type = l[0];
+    var content = l.slice(2);
+    if (type === 'm') {
+      media.push({rtp: [], fmtp: []});
+      location = media[media.length-1]; // point at latest media line
+    }
+
+    for (var j = 0; j < (grammar[type] || []).length; j += 1) {
+      var obj = grammar[type][j];
+      if (obj.reg.test(content)) {
+        return parseReg(obj, location, content);
+      }
+    }
+  });
+
+  session.media = media; // link it up
+  return session;
+};
+
+var fmtpReducer = function (acc, expr) {
+  var s = expr.split('=');
+  if (s.length === 2) {
+    acc[s[0]] = toIntIfInt(s[1]);
+  }
+  return acc;
+};
+
+exports.parseFmtpConfig = function (str) {
+  return str.split(/\;\s?/).reduce(fmtpReducer, {});
+};
+
+exports.parsePayloads = function (str) {
+  return str.split(' ').map(Number);
+};
+
+exports.parseRemoteCandidates = function (str) {
+  var candidates = [];
+  var parts = str.split(' ').map(toIntIfInt);
+  for (var i = 0; i < parts.length; i += 3) {
+    candidates.push({
+      component: parts[i],
+      ip: parts[i + 1],
+      port: parts[i + 2]
+    });
+  }
+  return candidates;
+};
+
+},{"./grammar":13}],16:[function(require,module,exports){
+var grammar = require('./grammar');
+
+// customized util.format - discards excess arguments and can void middle ones
+var formatRegExp = /%[sdv%]/g;
+var format = function (formatStr) {
+  var i = 1;
+  var args = arguments;
+  var len = args.length;
+  return formatStr.replace(formatRegExp, function (x) {
+    if (i >= len) {
+      return x; // missing argument
+    }
+    var arg = args[i];
+    i += 1;
+    switch (x) {
+      case '%%':
+        return '%';
+      case '%s':
+        return String(arg);
+      case '%d':
+        return Number(arg);
+      case '%v':
+        return '';
+    }
+  });
+  // NB: we discard excess arguments - they are typically undefined from makeLine
+};
+
+var makeLine = function (type, obj, location) {
+  var str = obj.format instanceof Function ?
+    (obj.format(obj.push ? location : location[obj.name])) :
+    obj.format;
+
+  var args = [type + '=' + str];
+  if (obj.names) {
+    for (var i = 0; i < obj.names.length; i += 1) {
+      var n = obj.names[i];
+      if (obj.name) {
+        args.push(location[obj.name][n]);
+      }
+      else { // for mLine and push attributes
+        args.push(location[obj.names[i]]);
+      }
+    }
+  }
+  else {
+    args.push(location[obj.name]);
+  }
+  return format.apply(null, args);
+};
+
+// RFC specified order
+// TODO: extend this with all the rest
+var defaultOuterOrder = [
+  'v', 'o', 's', 'i',
+  'u', 'e', 'p', 'c',
+  'b', 't', 'r', 'z', 'a'
+];
+var defaultInnerOrder = ['i', 'c', 'b', 'a'];
+
+
+module.exports = function (session, opts) {
+  opts = opts || {};
+  // ensure certain properties exist
+  if (session.version == null) {
+    session.version = 0; // "v=0" must be there (only defined version atm)
+  }
+  if (session.name == null) {
+    session.name = " "; // "s= " must be there if no meaningful name set
+  }
+  session.media.forEach(function (mLine) {
+    if (mLine.payloads == null) {
+      mLine.payloads = "";
+    }
+  });
+
+  var outerOrder = opts.outerOrder || defaultOuterOrder;
+  var innerOrder = opts.innerOrder || defaultInnerOrder;
+  var sdp = [];
+
+  // loop through outerOrder for matching properties on session
+  outerOrder.forEach(function (type) {
+    grammar[type].forEach(function (obj) {
+      if (obj.name in session && session[obj.name] != null) {
+        sdp.push(makeLine(type, obj, session));
+      }
+      else if (obj.push in session && session[obj.push] != null) {
+        session[obj.push].forEach(function (el) {
+          sdp.push(makeLine(type, obj, el));
+        });
+      }
+    });
+  });
+
+  // then for each media line, follow the innerOrder
+  session.media.forEach(function (mLine) {
+    sdp.push(makeLine('m', grammar.m[0], mLine));
+
+    innerOrder.forEach(function (type) {
+      grammar[type].forEach(function (obj) {
+        if (obj.name in mLine && mLine[obj.name] != null) {
+          sdp.push(makeLine(type, obj, mLine));
+        }
+        else if (obj.push in mLine && mLine[obj.push] != null) {
+          mLine[obj.push].forEach(function (el) {
+            sdp.push(makeLine(type, obj, el));
+          });
+        }
+      });
+    });
+  });
+
+  return sdp.join('\r\n') + '\r\n';
+};
+
+},{"./grammar":13}],17:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.exports = function arrayEquals(array) {
+    // if the other array is a falsy value, return
+    if (!array)
+        return false;
+
+    // compare lengths - can save a lot of time
+    if (this.length != array.length)
+        return false;
+
+    for (var i = 0, l = this.length; i < l; i++) {
+        // Check if we have nested arrays
+        if (this[i] instanceof Array && array[i] instanceof Array) {
+            // recurse into the nested arrays
+            if (!arrayEquals.apply(this[i], [array[i]]))
+                return false;
+        } else if (this[i] != array[i]) {
+            // Warning - two different object instances will never be equal:
+            // {x:20} != {x:20}
+            return false;
+        }
+    }
+    return true;
+};
+
+
+},{}],18:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+exports.Interop = require('./interop');
+
+},{"./interop":19}],19:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global RTCSessionDescription */
+/* global RTCIceCandidate */
+/* jshint -W097 */
+"use strict";
+
+var transform = require('./transform');
+var arrayEquals = require('./array-equals');
+
+function Interop() {
+
+    /**
+     * This map holds the most recent Unified Plan offer/answer SDP that was
+     * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and
+     * the SDP string as values.
+     *
+     * @type {{}}
+     */
+    this.cache = {
+        mlB2UMap : {},
+        mlU2BMap : {}
+    };
+}
+
+module.exports = Interop;
+
+/**
+ * Changes the candidate args to match with the related Unified Plan
+ */
+Interop.prototype.candidateToUnifiedPlan = function(candidate) {
+    var cand = new RTCIceCandidate(candidate);
+
+    cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex];
+    /* TODO: change sdpMid to (audio|video)-SSRC */
+
+    return cand;
+};
+
+/**
+ * Changes the candidate args to match with the related Plan B
+ */
+Interop.prototype.candidateToPlanB = function(candidate) {
+    var cand = new RTCIceCandidate(candidate);
+
+    if (cand.sdpMid.indexOf('audio') === 0) {
+      cand.sdpMid = 'audio';
+    } else if (cand.sdpMid.indexOf('video') === 0) {
+      cand.sdpMid = 'video';
+    } else {
+      throw new Error('candidate with ' + cand.sdpMid + ' not allowed');
+    }
+
+    cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex];
+
+    return cand;
+};
+
+/**
+ * Returns the index of the first m-line with the given media type and with a
+ * direction which allows sending, in the last Unified Plan description with
+ * type "answer" converted to Plan B. Returns {null} if there is no saved
+ * answer, or if none of its m-lines with the given type allow sending.
+ * @param type the media type ("audio" or "video").
+ * @returns {*}
+ */
+Interop.prototype.getFirstSendingIndexFromAnswer = function(type) {
+    if (!this.cache.answer) {
+        return null;
+    }
+
+    var session = transform.parse(this.cache.answer);
+    if (session && session.media && Array.isArray(session.media)){
+        for (var i = 0; i < session.media.length; i++) {
+            if (session.media[i].type == type &&
+                (!session.media[i].direction /* default to sendrecv */ ||
+                    session.media[i].direction === 'sendrecv' ||
+                    session.media[i].direction === 'sendonly')){
+                return i;
+            }
+        }
+    }
+
+    return null;
+};
+
+/**
+ * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A
+ * PeerConnection wrapper transforms the SDP to Plan B before passing it to the
+ * application.
+ *
+ * @param desc
+ * @returns {*}
+ */
+Interop.prototype.toPlanB = function(desc) {
+    var self = this;
+    //#region Preliminary input validation.
+
+    if (typeof desc !== 'object' || desc === null ||
+        typeof desc.sdp !== 'string') {
+        console.warn('An empty description was passed as an argument.');
+        return desc;
+    }
+
+    // Objectify the SDP for easier manipulation.
+    var session = transform.parse(desc.sdp);
+
+    // If the SDP contains no media, there's nothing to transform.
+    if (typeof session.media === 'undefined' ||
+        !Array.isArray(session.media) || session.media.length === 0) {
+        console.warn('The description has no media.');
+        return desc;
+    }
+
+    // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B
+    // SDP has a video, an audio and a data "channel" at most.
+    if (session.media.length <= 3 && session.media.every(function(m) {
+            return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
+        })) {
+        console.warn('This description does not look like Unified Plan.');
+        return desc;
+    }
+
+    //#endregion
+
+    // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443
+    var sdp = desc.sdp;
+    var rewrite = false;
+    for (var i = 0; i < session.media.length; i++) {
+        var uLine = session.media[i];
+        uLine.rtp.forEach(function(rtp) {
+            if (rtp.codec === 'NULL')
+            {
+                rewrite = true;
+                var offer = transform.parse(self.cache.offer);
+                rtp.codec = offer.media[i].rtp[0].codec;
+            }
+        });
+    }
+    if (rewrite) {
+        sdp = transform.write(session);
+    }
+
+    // Unified Plan SDP is our "precious". Cache it for later use in the Plan B
+    // -> Unified Plan transformation.
+    this.cache[desc.type] = sdp;
+
+    //#region Convert from Unified Plan to Plan B.
+
+    // We rebuild the session.media array.
+    var media = session.media;
+    session.media = [];
+
+    // Associative array that maps channel types to channel objects for fast
+    // access to channel objects by their type, e.g. type2bl['audio']->channel
+    // obj.
+    var type2bl = {};
+
+    // Used to build the group:BUNDLE value after the channels construction
+    // loop.
+    var types = [];
+
+    media.forEach(function(uLine) {
+        // rtcp-mux is required in the Plan B SDP.
+        if ((typeof uLine.rtcpMux !== 'string' ||
+            uLine.rtcpMux !== 'rtcp-mux') &&
+            uLine.direction !== 'inactive') {
+            throw new Error('Cannot convert to Plan B because m-lines ' +
+                'without the rtcp-mux attribute were found.');
+        }
+
+        // If we don't have a channel for this uLine.type OR the selected is
+        // inactive, then select this uLine as the channel basis.
+        if (typeof type2bl[uLine.type] === 'undefined' ||
+            type2bl[uLine.type].direction === 'inactive') {
+            type2bl[uLine.type] = uLine;
+        }
+
+        if (uLine.protocol != type2bl[uLine.type].protocol) {
+          throw new Error('Cannot convert to Plan B because m-lines ' +
+              'have different protocols and this library does not have ' +
+              'support for that');
+        }
+
+        if (uLine.payloads != type2bl[uLine.type].payloads) {
+          throw new Error('Cannot convert to Plan B because m-lines ' +
+              'have different payloads and this library does not have ' +
+              'support for that');
+        }
+
+    });
+
+    // Implode the Unified Plan m-lines/tracks into Plan B channels.
+    media.forEach(function(uLine) {
+        if (uLine.type === 'application') {
+            session.media.push(uLine);
+            types.push(uLine.mid);
+            return;
+        }
+
+        // Add sources to the channel and handle a=msid.
+        if (typeof uLine.sources === 'object') {
+            Object.keys(uLine.sources).forEach(function(ssrc) {
+                if (typeof type2bl[uLine.type].sources !== 'object')
+                    type2bl[uLine.type].sources = {};
+
+                // Assign the sources to the channel.
+                type2bl[uLine.type].sources[ssrc] =
+                    uLine.sources[ssrc];
+
+                if (typeof uLine.msid !== 'undefined') {
+                    // In Plan B the msid is an SSRC attribute. Also, we don't
+                    // care about the obsolete label and mslabel attributes.
+                    //
+                    // Note that it is not guaranteed that the uLine will
+                    // have an msid. recvonly channels in particular don't have
+                    // one.
+                    type2bl[uLine.type].sources[ssrc].msid =
+                        uLine.msid;
+                }
+                // NOTE ssrcs in ssrc groups will share msids, as
+                // draft-uberti-rtcweb-plan-00 mandates.
+            });
+        }
+
+        // Add ssrc groups to the channel.
+        if (typeof uLine.ssrcGroups !== 'undefined' &&
+                Array.isArray(uLine.ssrcGroups)) {
+
+            // Create the ssrcGroups array, if it's not defined.
+            if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' ||
+                    !Array.isArray(type2bl[uLine.type].ssrcGroups)) {
+                type2bl[uLine.type].ssrcGroups = [];
+            }
+
+            type2bl[uLine.type].ssrcGroups =
+                type2bl[uLine.type].ssrcGroups.concat(
+                    uLine.ssrcGroups);
+        }
+
+        if (type2bl[uLine.type] === uLine) {
+            // Plan B mids are in ['audio', 'video', 'data']
+            uLine.mid = uLine.type;
+
+            // Plan B doesn't support/need the bundle-only attribute.
+            delete uLine.bundleOnly;
+
+            // In Plan B the msid is an SSRC attribute.
+            delete uLine.msid;
+
+	    if (uLine.type == media[0].type) {
+	      types.unshift(uLine.type);
+	      // Add the channel to the new media array.
+	      session.media.unshift(uLine);
+	    } else {
+	      types.push(uLine.type);
+	      // Add the channel to the new media array.
+	      session.media.push(uLine);
+	    }
+        }
+    });
+
+    if (typeof session.groups !== 'undefined') {
+      // We regenerate the BUNDLE group with the new mids.
+      session.groups.some(function(group) {
+	  if (group.type === 'BUNDLE') {
+	      group.mids = types.join(' ');
+	      return true;
+	  }
+      });
+    }
+
+    // msid semantic
+    session.msidSemantic = {
+        semantic: 'WMS',
+        token: '*'
+    };
+
+    var resStr = transform.write(session);
+
+    return new RTCSessionDescription({
+        type: desc.type,
+        sdp: resStr
+    });
+
+    //#endregion
+};
+
+/* follow rules defined in RFC4145 */
+function addSetupAttr(uLine) {
+    if (typeof uLine.setup === 'undefined') {
+        return;
+    }
+
+    if (uLine.setup === "active") {
+            uLine.setup = "passive";
+    } else if (uLine.setup === "passive") {
+        uLine.setup = "active";
+    }
+}
+
+/**
+ * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A
+ * PeerConnection wrapper transforms the SDP to Unified Plan before passing it
+ * to FF.
+ *
+ * @param desc
+ * @returns {*}
+ */
+Interop.prototype.toUnifiedPlan = function(desc) {
+    var self = this;
+    //#region Preliminary input validation.
+
+    if (typeof desc !== 'object' || desc === null ||
+        typeof desc.sdp !== 'string') {
+        console.warn('An empty description was passed as an argument.');
+        return desc;
+    }
+
+    var session = transform.parse(desc.sdp);
+
+    // If the SDP contains no media, there's nothing to transform.
+    if (typeof session.media === 'undefined' ||
+        !Array.isArray(session.media) || session.media.length === 0) {
+        console.warn('The description has no media.');
+        return desc;
+    }
+
+    // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has
+    // a video, an audio and a data "channel" at most.
+    if (session.media.length > 3 || !session.media.every(function(m) {
+            return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
+        })) {
+        console.warn('This description does not look like Plan B.');
+        return desc;
+    }
+
+    // Make sure this Plan B SDP can be converted to a Unified Plan SDP.
+    var mids = [];
+    session.media.forEach(function(m) {
+        mids.push(m.mid);
+    });
+
+    var hasBundle = false;
+    if (typeof session.groups !== 'undefined' &&
+        Array.isArray(session.groups)) {
+        hasBundle = session.groups.every(function(g) {
+            return g.type !== 'BUNDLE' ||
+                arrayEquals.apply(g.mids.sort(), [mids.sort()]);
+        });
+    }
+
+    if (!hasBundle) {
+        var mustBeBundle = false;
+
+        session.media.forEach(function(m) {
+            if (m.direction !== 'inactive') {
+                mustBeBundle = true;
+            }
+        });
+
+        if (mustBeBundle) {
+            throw new Error("Cannot convert to Unified Plan because m-lines that" +
+              " are not bundled were found.");
+        }
+    }
+
+    //#endregion
+
+
+    //#region Convert from Plan B to Unified Plan.
+
+    // Unfortunately, a Plan B offer/answer doesn't have enough information to
+    // rebuild an equivalent Unified Plan offer/answer.
+    //
+    // For example, if this is a local answer (in Unified Plan style) that we
+    // convert to Plan B prior to handing it over to the application (the
+    // PeerConnection wrapper called us, for instance, after a successful
+    // createAnswer), we want to remember the m-line at which we've seen the
+    // (local) SSRC. That's because when the application wants to do call the
+    // SLD method, forcing us to do the inverse transformation (from Plan B to
+    // Unified Plan), we need to know to which m-line to assign the (local)
+    // SSRC. We also need to know all the other m-lines that the original
+    // answer had and include them in the transformed answer as well.
+    //
+    // Another example is if this is a remote offer that we convert to Plan B
+    // prior to giving it to the application, we want to remember the mid at
+    // which we've seen the (remote) SSRC.
+    //
+    // In the iteration that follows, we use the cached Unified Plan (if it
+    // exists) to assign mids to ssrcs.
+
+    var type;
+    if (desc.type === 'answer') {
+        type = 'offer';
+    } else if (desc.type === 'offer') {
+        type = 'answer';
+    } else {
+        throw new Error("Type '" + desc.type + "' not supported.");
+    }
+
+    var cached;
+    if (typeof this.cache[type] !== 'undefined') {
+        cached = transform.parse(this.cache[type]);
+    }
+
+    var recvonlySsrcs = {
+        audio: {},
+        video: {}
+    };
+
+    // A helper map that sends mids to m-line objects. We use it later to
+    // rebuild the Unified Plan style session.media array.
+    var mid2ul = {};
+    var bIdx = 0;
+    var uIdx = 0;
+
+    var sources2ul = {};
+
+    var candidates;
+    var iceUfrag;
+    var icePwd;
+    var fingerprint;
+    var payloads = {};
+    var rtcpFb = {};
+    var rtp = {};
+
+    session.media.forEach(function(bLine) {
+        if ((typeof bLine.rtcpMux !== 'string' ||
+            bLine.rtcpMux !== 'rtcp-mux') &&
+            bLine.direction !== 'inactive') {
+            throw new Error("Cannot convert to Unified Plan because m-lines " +
+                "without the rtcp-mux attribute were found.");
+        }
+
+        if (bLine.type === 'application') {
+            mid2ul[bLine.mid] = bLine;
+            return;
+        }
+
+        // With rtcp-mux and bundle all the channels should have the same ICE
+        // stuff.
+        var sources = bLine.sources;
+        var ssrcGroups = bLine.ssrcGroups;
+        var port = bLine.port;
+
+        /* Chrome adds different candidates even using bundle, so we concat the candidates list */
+        if (typeof bLine.candidates != 'undefined') {
+            if (typeof candidates != 'undefined') {
+                candidates = candidates.concat(bLine.candidates);
+            } else {
+                candidates = bLine.candidates;
+            }
+        }
+
+        if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) {
+            throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" +
+                            "\tLast iceUfrag: " + iceUfrag + "\n" +
+                            "\tNew iceUfrag: " + bLine.iceUfrag
+            );
+        }
+
+        if (typeof bLine.iceUfrag != 'undefined') {
+                iceUfrag = bLine.iceUfrag;
+        }
+
+        if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) {
+            throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" +
+                            "\tLast icePwd: " + icePwd + "\n" +
+                            "\tNew icePwd: " + bLine.icePwd
+            );
+        }
+
+        if (typeof bLine.icePwd != 'undefined') {
+                icePwd = bLine.icePwd;
+        }
+
+        if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') &&
+            (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) {
+            throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" +
+                            "\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" +
+                            "\tNew fingerprint: " + JSON.stringify(bLine.fingerprint)
+            );
+        }
+
+        if (typeof bLine.fingerprint != 'undefined') {
+                fingerprint = bLine.fingerprint;
+        }
+
+        payloads[bLine.type] = bLine.payloads;
+        rtcpFb[bLine.type] = bLine.rtcpFb;
+        rtp[bLine.type] = bLine.rtp;
+
+        // inverted ssrc group map
+        var ssrc2group = {};
+        if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
+            ssrcGroups.forEach(function (ssrcGroup) {
+                // XXX This might brake if an SSRC is in more than one group
+                // for some reason.
+                if (typeof ssrcGroup.ssrcs !== 'undefined' &&
+                    Array.isArray(ssrcGroup.ssrcs)) {
+                    ssrcGroup.ssrcs.forEach(function (ssrc) {
+                        if (typeof ssrc2group[ssrc] === 'undefined') {
+                            ssrc2group[ssrc] = [];
+                        }
+
+                        ssrc2group[ssrc].push(ssrcGroup);
+                    });
+                }
+            });
+        }
+
+        // ssrc to m-line index.
+        var ssrc2ml = {};
+
+        if (typeof sources === 'object') {
+
+            // We'll use the "bLine" object as a prototype for each new "mLine"
+            // that we create, but first we need to clean it up a bit.
+            delete bLine.sources;
+            delete bLine.ssrcGroups;
+            delete bLine.candidates;
+            delete bLine.iceUfrag;
+            delete bLine.icePwd;
+            delete bLine.fingerprint;
+            delete bLine.port;
+            delete bLine.mid;
+
+            // Explode the Plan B channel sources with one m-line per source.
+            Object.keys(sources).forEach(function(ssrc) {
+
+                // The (unified) m-line for this SSRC. We either create it from
+                // scratch or, if it's a grouped SSRC, we re-use a related
+                // mline. In other words, if the source is grouped with another
+                // source, put the two together in the same m-line.
+                var uLine;
+
+                // We assume here that we are the answerer in the O/A, so any
+                // offers which we translate come from the remote side, while
+                // answers are local. So the check below is to make that we
+                // handle receive-only SSRCs in a special way only if they come
+                // from the remote side.
+                if (desc.type==='offer') {
+                    // We want to detect SSRCs which are used by a remote peer
+                    // in an m-line with direction=recvonly (i.e. they are
+                    // being used for RTCP only).
+                    // This information would have gotten lost if the remote
+                    // peer used Unified Plan and their local description was
+                    // translated to Plan B. So we use the lack of an MSID
+                    // attribute to deduce a "receive only" SSRC.
+                    if (!sources[ssrc].msid) {
+                        recvonlySsrcs[bLine.type][ssrc] = sources[ssrc];
+                        // Receive-only SSRCs must not create new m-lines. We
+                        // will assign them to an existing m-line later.
+                        return;
+                    }
+                }
+
+                if (typeof ssrc2group[ssrc] !== 'undefined' &&
+                    Array.isArray(ssrc2group[ssrc])) {
+                    ssrc2group[ssrc].some(function (ssrcGroup) {
+                        // ssrcGroup.ssrcs *is* an Array, no need to check
+                        // again here.
+                        return ssrcGroup.ssrcs.some(function (related) {
+                            if (typeof ssrc2ml[related] === 'object') {
+                                uLine = ssrc2ml[related];
+                                return true;
+                            }
+                        });
+                    });
+                }
+
+                if (typeof uLine === 'object') {
+                    // the m-line already exists. Just add the source.
+                    uLine.sources[ssrc] = sources[ssrc];
+                    delete sources[ssrc].msid;
+                } else {
+                    // Use the "bLine" as a prototype for the "uLine".
+                    uLine = Object.create(bLine);
+                    ssrc2ml[ssrc] = uLine;
+
+                    if (typeof sources[ssrc].msid !== 'undefined') {
+                        // Assign the msid of the source to the m-line. Note
+                        // that it is not guaranteed that the source will have
+                        // msid. In particular "recvonly" sources don't have an
+                        // msid. Note that "recvonly" is a term only defined
+                        // for m-lines.
+                        uLine.msid = sources[ssrc].msid;
+                        delete sources[ssrc].msid;
+                    }
+
+                    // We assign one SSRC per media line.
+                    uLine.sources = {};
+                    uLine.sources[ssrc] = sources[ssrc];
+                    uLine.ssrcGroups = ssrc2group[ssrc];
+
+                    // Use the cached Unified Plan SDP (if it exists) to assign
+                    // SSRCs to mids.
+                    if (typeof cached !== 'undefined' &&
+                        typeof cached.media !== 'undefined' &&
+                        Array.isArray(cached.media)) {
+
+                        cached.media.forEach(function (m) {
+                            if (typeof m.sources === 'object') {
+                                Object.keys(m.sources).forEach(function (s) {
+                                    if (s === ssrc) {
+                                        uLine.mid = m.mid;
+                                    }
+                                });
+                            }
+                        });
+                    }
+
+                    if (typeof uLine.mid === 'undefined') {
+
+                        // If this is an SSRC that we see for the first time
+                        // assign it a new mid. This is typically the case when
+                        // this method is called to transform a remote
+                        // description for the first time or when there is a
+                        // new SSRC in the remote description because a new
+                        // peer has joined the conference. Local SSRCs should
+                        // have already been added to the map in the toPlanB
+                        // method.
+                        //
+                        // Because FF generates answers in Unified Plan style,
+                        // we MUST already have a cached answer with all the
+                        // local SSRCs mapped to some m-line/mid.
+
+                        uLine.mid = [bLine.type, '-', ssrc].join('');
+                    }
+
+                    // Include the candidates in the 1st media line.
+                    uLine.candidates = candidates;
+                    uLine.iceUfrag = iceUfrag;
+                    uLine.icePwd = icePwd;
+                    uLine.fingerprint = fingerprint;
+                    uLine.port = port;
+
+                    mid2ul[uLine.mid] = uLine;
+                    sources2ul[uIdx] = uLine.sources;
+
+                    self.cache.mlU2BMap[uIdx] = bIdx;
+                    if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
+                      self.cache.mlB2UMap[bIdx] = uIdx;
+                    }
+                    uIdx++;
+                }
+            });
+        } else {
+          var uLine = bLine;
+
+          uLine.candidates = candidates;
+          uLine.iceUfrag = iceUfrag;
+          uLine.icePwd = icePwd;
+          uLine.fingerprint = fingerprint;
+          uLine.port = port;
+
+          mid2ul[uLine.mid] = uLine;
+
+          self.cache.mlU2BMap[uIdx] = bIdx;
+          if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
+            self.cache.mlB2UMap[bIdx] = uIdx;
+          }
+        }
+
+        bIdx++;
+    });
+
+    // Rebuild the media array in the right order and add the missing mLines
+    // (missing from the Plan B SDP).
+    session.media = [];
+    mids = []; // reuse
+
+    if (desc.type === 'answer') {
+
+        // The media lines in the answer must match the media lines in the
+        // offer. The order is important too. Here we assume that Firefox is
+        // the answerer, so we merely have to use the reconstructed (unified)
+        // answer to update the cached (unified) answer accordingly.
+        //
+        // In the general case, one would have to use the cached (unified)
+        // offer to find the m-lines that are missing from the reconstructed
+        // answer, potentially grabbing them from the cached (unified) answer.
+        // One has to be careful with this approach because inactive m-lines do
+        // not always have an mid, making it tricky (impossible?) to find where
+        // exactly and which m-lines are missing from the reconstructed answer.
+
+        for (var i = 0; i < cached.media.length; i++) {
+            var uLine = cached.media[i];
+
+            delete uLine.msid;
+            delete uLine.sources;
+            delete uLine.ssrcGroups;
+
+            if (typeof sources2ul[i] === 'undefined') {
+              if (!uLine.direction
+                  || uLine.direction === 'sendrecv')
+                  uLine.direction = 'recvonly';
+              else if (uLine.direction === 'sendonly')
+                  uLine.direction = 'inactive';
+            } else {
+              if (!uLine.direction
+                  || uLine.direction === 'sendrecv')
+                  uLine.direction = 'sendrecv';
+              else if (uLine.direction === 'recvonly')
+                  uLine.direction = 'sendonly';
+            }
+
+            uLine.sources = sources2ul[i];
+            uLine.candidates = candidates;
+            uLine.iceUfrag = iceUfrag;
+            uLine.icePwd = icePwd;
+            uLine.fingerprint = fingerprint;
+
+            uLine.rtp = rtp[uLine.type];
+            uLine.payloads = payloads[uLine.type];
+            uLine.rtcpFb = rtcpFb[uLine.type];
+
+            session.media.push(uLine);
+
+            if (typeof uLine.mid === 'string') {
+                // inactive lines don't/may not have an mid.
+                mids.push(uLine.mid);
+            }
+        }
+    } else {
+
+        // SDP offer/answer (and the JSEP spec) forbids removing an m-section
+        // under any circumstances. If we are no longer interested in sending a
+        // track, we just remove the msid and ssrc attributes and set it to
+        // either a=recvonly (as the reofferer, we must use recvonly if the
+        // other side was previously sending on the m-section, but we can also
+        // leave the possibility open if it wasn't previously in use), or
+        // a=inactive.
+
+        if (typeof cached !== 'undefined' &&
+            typeof cached.media !== 'undefined' &&
+            Array.isArray(cached.media)) {
+            cached.media.forEach(function(uLine) {
+                mids.push(uLine.mid);
+                if (typeof mid2ul[uLine.mid] !== 'undefined') {
+                    session.media.push(mid2ul[uLine.mid]);
+                } else {
+                    delete uLine.msid;
+                    delete uLine.sources;
+                    delete uLine.ssrcGroups;
+
+                    if (!uLine.direction
+                        || uLine.direction === 'sendrecv') {
+                        uLine.direction = 'sendonly';
+                    }
+                    if (!uLine.direction
+                        || uLine.direction === 'recvonly') {
+                        uLine.direction = 'inactive';
+                    }
+
+                    addSetupAttr (uLine);
+                    session.media.push(uLine);
+                }
+            });
+        }
+
+        // Add all the remaining (new) m-lines of the transformed SDP.
+        Object.keys(mid2ul).forEach(function(mid) {
+            if (mids.indexOf(mid) === -1) {
+                mids.push(mid);
+                if (mid2ul[mid].direction === 'recvonly') {
+                    // This is a remote recvonly channel. Add its SSRC to the
+                    // appropriate sendrecv or sendonly channel.
+                    // TODO(gp) what if we don't have sendrecv/sendonly
+                    // channel?
+
+                    var done = false;
+
+                    session.media.some(function (uLine) {
+                        if ((uLine.direction === 'sendrecv' ||
+                            uLine.direction === 'sendonly') &&
+                            uLine.type === mid2ul[mid].type) {
+                            // mid2ul[mid] shouldn't have any ssrc-groups
+                            Object.keys(mid2ul[mid].sources).forEach(
+                                function (ssrc) {
+                                uLine.sources[ssrc] =
+                                    mid2ul[mid].sources[ssrc];
+                            });
+
+                            done = true;
+                            return true;
+                        }
+                    });
+
+                    if (!done) {
+                        session.media.push(mid2ul[mid]);
+                    }
+                } else {
+                    session.media.push(mid2ul[mid]);
+                }
+            }
+        });
+    }
+
+    // After we have constructed the Plan Unified m-lines we can figure out
+    // where (in which m-line) to place the 'recvonly SSRCs'.
+    // Note: we assume here that we are the answerer in the O/A, so any offers
+    // which we translate come from the remote side, while answers are local
+    // (and so our last local description is cached as an 'answer').
+    ["audio", "video"].forEach(function (type) {
+        if (!session || !session.media || !Array.isArray(session.media))
+            return;
+
+        var idx = null;
+        if (Object.keys(recvonlySsrcs[type]).length > 0) {
+            idx = self.getFirstSendingIndexFromAnswer(type);
+            if (idx === null){
+                // If this is the first offer we receive, we don't have a
+                // cached answer. Assume that we will be sending media using
+                // the first m-line for each media type.
+
+                for (var i = 0; i < session.media.length; i++) {
+                    if (session.media[i].type === type) {
+                        idx = i;
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (idx && session.media.length > idx) {
+            var mLine = session.media[idx];
+            Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) {
+                if (mLine.sources && mLine.sources[ssrc]) {
+                    console.warn("Replacing an existing SSRC.");
+                }
+                if (!mLine.sources) {
+                    mLine.sources = {};
+                }
+
+                mLine.sources[ssrc] = recvonlySsrcs[type][ssrc];
+            });
+        }
+    });
+
+    if (typeof session.groups !== 'undefined') {
+      // We regenerate the BUNDLE group (since we regenerated the mids)
+      session.groups.some(function(group) {
+	  if (group.type === 'BUNDLE') {
+	      group.mids = mids.join(' ');
+	      return true;
+	  }
+      });
+    }
+
+    // msid semantic
+    session.msidSemantic = {
+        semantic: 'WMS',
+        token: '*'
+    };
+
+    var resStr = transform.write(session);
+
+    // Cache the transformed SDP (Unified Plan) for later re-use in this
+    // function.
+    this.cache[desc.type] = resStr;
+
+    return new RTCSessionDescription({
+        type: desc.type,
+        sdp: resStr
+    });
+
+    //#endregion
+};
+
+},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){
+/* Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var transform = require('sdp-transform');
+
+exports.write = function(session, opts) {
+
+  if (typeof session !== 'undefined' &&
+      typeof session.media !== 'undefined' &&
+      Array.isArray(session.media)) {
+
+    session.media.forEach(function (mLine) {
+      // expand sources to ssrcs
+      if (typeof mLine.sources !== 'undefined' &&
+        Object.keys(mLine.sources).length !== 0) {
+          mLine.ssrcs = [];
+          Object.keys(mLine.sources).forEach(function (ssrc) {
+            var source = mLine.sources[ssrc];
+            Object.keys(source).forEach(function (attribute) {
+              mLine.ssrcs.push({
+                id: ssrc,
+                attribute: attribute,
+                value: source[attribute]
+              });
+            });
+          });
+          delete mLine.sources;
+        }
+
+      // join ssrcs in ssrc groups
+      if (typeof mLine.ssrcGroups !== 'undefined' &&
+        Array.isArray(mLine.ssrcGroups)) {
+          mLine.ssrcGroups.forEach(function (ssrcGroup) {
+            if (typeof ssrcGroup.ssrcs !== 'undefined' &&
+                Array.isArray(ssrcGroup.ssrcs)) {
+              ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ');
+            }
+          });
+        }
+    });
+  }
+
+  // join group mids
+  if (typeof session !== 'undefined' &&
+      typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
+
+    session.groups.forEach(function (g) {
+      if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
+        g.mids = g.mids.join(' ');
+      }
+    });
+  }
+
+  return transform.write(session, opts);
+};
+
+exports.parse = function(sdp) {
+  var session = transform.parse(sdp);
+
+  if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
+      Array.isArray(session.media)) {
+
+    session.media.forEach(function (mLine) {
+      // group sources attributes by ssrc
+      if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
+        mLine.sources = {};
+        mLine.ssrcs.forEach(function (ssrc) {
+          if (!mLine.sources[ssrc.id])
+          mLine.sources[ssrc.id] = {};
+        mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value;
+        });
+
+        delete mLine.ssrcs;
+      }
+
+      // split ssrcs in ssrc groups
+      if (typeof mLine.ssrcGroups !== 'undefined' &&
+        Array.isArray(mLine.ssrcGroups)) {
+          mLine.ssrcGroups.forEach(function (ssrcGroup) {
+            if (typeof ssrcGroup.ssrcs === 'string') {
+              ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ');
+            }
+          });
+        }
+    });
+  }
+  // split group mids
+  if (typeof session !== 'undefined' &&
+      typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
+
+    session.groups.forEach(function (g) {
+      if (typeof g.mids === 'string') {
+        g.mids = g.mids.split(' ');
+      }
+    });
+  }
+
+  return session;
+};
+
+
+},{"sdp-transform":14}],21:[function(require,module,exports){
+/**
+ * UAParser.js v0.7.17
+ * Lightweight JavaScript-based User-Agent string parser
+ * https://github.com/faisalman/ua-parser-js
+ *
+ * Copyright © 2012-2016 Faisal Salman <fyzlman@gmail.com>
+ * Dual licensed under GPLv2 & MIT
+ */
+
+(function (window, undefined) {
+
+    'use strict';
+
+    //////////////
+    // Constants
+    /////////////
+
+
+    var LIBVERSION  = '0.7.17',
+        EMPTY       = '',
+        UNKNOWN     = '?',
+        FUNC_TYPE   = 'function',
+        UNDEF_TYPE  = 'undefined',
+        OBJ_TYPE    = 'object',
+        STR_TYPE    = 'string',
+        MAJOR       = 'major', // deprecated
+        MODEL       = 'model',
+        NAME        = 'name',
+        TYPE        = 'type',
+        VENDOR      = 'vendor',
+        VERSION     = 'version',
+        ARCHITECTURE= 'architecture',
+        CONSOLE     = 'console',
+        MOBILE      = 'mobile',
+        TABLET      = 'tablet',
+        SMARTTV     = 'smarttv',
+        WEARABLE    = 'wearable',
+        EMBEDDED    = 'embedded';
+
+
+    ///////////
+    // Helper
+    //////////
+
+
+    var util = {
+        extend : function (regexes, extensions) {
+            var margedRegexes = {};
+            for (var i in regexes) {
+                if (extensions[i] && extensions[i].length % 2 === 0) {
+                    margedRegexes[i] = extensions[i].concat(regexes[i]);
+                } else {
+                    margedRegexes[i] = regexes[i];
+                }
+            }
+            return margedRegexes;
+        },
+        has : function (str1, str2) {
+          if (typeof str1 === "string") {
+            return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1;
+          } else {
+            return false;
+          }
+        },
+        lowerize : function (str) {
+            return str.toLowerCase();
+        },
+        major : function (version) {
+            return typeof(version) === STR_TYPE ? version.replace(/[^\d\.]/g,'').split(".")[0] : undefined;
+        },
+        trim : function (str) {
+          return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
+        }
+    };
+
+
+    ///////////////
+    // Map helper
+    //////////////
+
+
+    var mapper = {
+
+        rgx : function (ua, arrays) {
+
+            //var result = {},
+            var i = 0, j, k, p, q, matches, match;//, args = arguments;
+
+            /*// construct object barebones
+            for (p = 0; p < args[1].length; p++) {
+                q = args[1][p];
+                result[typeof q === OBJ_TYPE ? q[0] : q] = undefined;
+            }*/
+
+            // loop through all regexes maps
+            while (i < arrays.length && !matches) {
+
+                var regex = arrays[i],       // even sequence (0,2,4,..)
+                    props = arrays[i + 1];   // odd sequence (1,3,5,..)
+                j = k = 0;
+
+                // try matching uastring with regexes
+                while (j < regex.length && !matches) {
+
+                    matches = regex[j++].exec(ua);
+
+                    if (!!matches) {
+                        for (p = 0; p < props.length; p++) {
+                            match = matches[++k];
+                            q = props[p];
+                            // check if given property is actually array
+                            if (typeof q === OBJ_TYPE && q.length > 0) {
+                                if (q.length == 2) {
+                                    if (typeof q[1] == FUNC_TYPE) {
+                                        // assign modified match
+                                        this[q[0]] = q[1].call(this, match);
+                                    } else {
+                                        // assign given value, ignore regex match
+                                        this[q[0]] = q[1];
+                                    }
+                                } else if (q.length == 3) {
+                                    // check whether function or regex
+                                    if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
+                                        // call function (usually string mapper)
+                                        this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined;
+                                    } else {
+                                        // sanitize match using given regex
+                                        this[q[0]] = match ? match.replace(q[1], q[2]) : undefined;
+                                    }
+                                } else if (q.length == 4) {
+                                        this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined;
+                                }
+                            } else {
+                                this[q] = match ? match : undefined;
+                            }
+                        }
+                    }
+                }
+                i += 2;
+            }
+            // console.log(this);
+            //return this;
+        },
+
+        str : function (str, map) {
+
+            for (var i in map) {
+                // check if array
+                if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
+                    for (var j = 0; j < map[i].length; j++) {
+                        if (util.has(map[i][j], str)) {
+                            return (i === UNKNOWN) ? undefined : i;
+                        }
+                    }
+                } else if (util.has(map[i], str)) {
+                    return (i === UNKNOWN) ? undefined : i;
+                }
+            }
+            return str;
+        }
+    };
+
+
+    ///////////////
+    // String map
+    //////////////
+
+
+    var maps = {
+
+        browser : {
+            oldsafari : {
+                version : {
+                    '1.0'   : '/8',
+                    '1.2'   : '/1',
+                    '1.3'   : '/3',
+                    '2.0'   : '/412',
+                    '2.0.2' : '/416',
+                    '2.0.3' : '/417',
+                    '2.0.4' : '/419',
+                    '?'     : '/'
+                }
+            }
+        },
+
+        device : {
+            amazon : {
+                model : {
+                    'Fire Phone' : ['SD', 'KF']
+                }
+            },
+            sprint : {
+                model : {
+                    'Evo Shift 4G' : '7373KT'
+                },
+                vendor : {
+                    'HTC'       : 'APA',
+                    'Sprint'    : 'Sprint'
+                }
+            }
+        },
+
+        os : {
+            windows : {
+                version : {
+                    'ME'        : '4.90',
+                    'NT 3.11'   : 'NT3.51',
+                    'NT 4.0'    : 'NT4.0',
+                    '2000'      : 'NT 5.0',
+                    'XP'        : ['NT 5.1', 'NT 5.2'],
+                    'Vista'     : 'NT 6.0',
+                    '7'         : 'NT 6.1',
+                    '8'         : 'NT 6.2',
+                    '8.1'       : 'NT 6.3',
+                    '10'        : ['NT 6.4', 'NT 10.0'],
+                    'RT'        : 'ARM'
+                }
+            }
+        }
+    };
+
+
+    //////////////
+    // Regex map
+    /////////////
+
+
+    var regexes = {
+
+        browser : [[
+
+            // Presto based
+            /(opera\smini)\/([\w\.-]+)/i,                                       // Opera Mini
+            /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i,                      // Opera Mobi/Tablet
+            /(opera).+version\/([\w\.]+)/i,                                     // Opera > 9.80
+            /(opera)[\/\s]+([\w\.]+)/i                                          // Opera < 9.80
+            ], [NAME, VERSION], [
+
+            /(opios)[\/\s]+([\w\.]+)/i                                          // Opera mini on iphone >= 8.0
+            ], [[NAME, 'Opera Mini'], VERSION], [
+
+            /\s(opr)\/([\w\.]+)/i                                               // Opera Webkit
+            ], [[NAME, 'Opera'], VERSION], [
+
+            // Mixed
+            /(kindle)\/([\w\.]+)/i,                                             // Kindle
+            /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i,
+                                                                                // Lunascape/Maxthon/Netfront/Jasmine/Blazer
+
+            // Trident based
+            /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i,
+                                                                                // Avant/IEMobile/SlimBrowser/Baidu
+            /(?:ms|\()(ie)\s([\w\.]+)/i,                                        // Internet Explorer
+
+            // Webkit/KHTML based
+            /(rekonq)\/([\w\.]+)*/i,                                            // Rekonq
+            /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser)\/([\w\.-]+)/i
+                                                                                // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser
+            ], [NAME, VERSION], [
+
+            /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i                         // IE11
+            ], [[NAME, 'IE'], VERSION], [
+
+            /(edge)\/((\d+)?[\w\.]+)/i                                          // Microsoft Edge
+            ], [NAME, VERSION], [
+
+            /(yabrowser)\/([\w\.]+)/i                                           // Yandex
+            ], [[NAME, 'Yandex'], VERSION], [
+
+            /(puffin)\/([\w\.]+)/i                                              // Puffin
+            ], [[NAME, 'Puffin'], VERSION], [
+
+            /((?:[\s\/])uc?\s?browser|(?:juc.+)ucweb)[\/\s]?([\w\.]+)/i
+                                                                                // UCBrowser
+            ], [[NAME, 'UCBrowser'], VERSION], [
+
+            /(comodo_dragon)\/([\w\.]+)/i                                       // Comodo Dragon
+            ], [[NAME, /_/g, ' '], VERSION], [
+
+            /(micromessenger)\/([\w\.]+)/i                                      // WeChat
+            ], [[NAME, 'WeChat'], VERSION], [
+
+            /(QQ)\/([\d\.]+)/i                                                  // QQ, aka ShouQ
+            ], [NAME, VERSION], [
+
+            /m?(qqbrowser)[\/\s]?([\w\.]+)/i                                    // QQBrowser
+            ], [NAME, VERSION], [
+
+            /xiaomi\/miuibrowser\/([\w\.]+)/i                                   // MIUI Browser
+            ], [VERSION, [NAME, 'MIUI Browser']], [
+
+            /;fbav\/([\w\.]+);/i                                                // Facebook App for iOS & Android
+            ], [VERSION, [NAME, 'Facebook']], [
+
+            /headlesschrome(?:\/([\w\.]+)|\s)/i                                 // Chrome Headless
+            ], [VERSION, [NAME, 'Chrome Headless']], [
+
+            /\swv\).+(chrome)\/([\w\.]+)/i                                      // Chrome WebView
+            ], [[NAME, /(.+)/, '$1 WebView'], VERSION], [
+
+            /((?:oculus|samsung)browser)\/([\w\.]+)/i
+            ], [[NAME, /(.+(?:g|us))(.+)/, '$1 $2'], VERSION], [                // Oculus / Samsung Browser
+
+            /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i        // Android Browser
+            ], [VERSION, [NAME, 'Android Browser']], [
+
+            /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i
+                                                                                // Chrome/OmniWeb/Arora/Tizen/Nokia
+            ], [NAME, VERSION], [
+
+            /(dolfin)\/([\w\.]+)/i                                              // Dolphin
+            ], [[NAME, 'Dolphin'], VERSION], [
+
+            /((?:android.+)crmo|crios)\/([\w\.]+)/i                             // Chrome for Android/iOS
+            ], [[NAME, 'Chrome'], VERSION], [
+
+            /(coast)\/([\w\.]+)/i                                               // Opera Coast
+            ], [[NAME, 'Opera Coast'], VERSION], [
+
+            /fxios\/([\w\.-]+)/i                                                // Firefox for iOS
+            ], [VERSION, [NAME, 'Firefox']], [
+
+            /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i                       // Mobile Safari
+            ], [VERSION, [NAME, 'Mobile Safari']], [
+
+            /version\/([\w\.]+).+?(mobile\s?safari|safari)/i                    // Safari & Safari Mobile
+            ], [VERSION, NAME], [
+
+            /webkit.+?(gsa)\/([\w\.]+).+?(mobile\s?safari|safari)(\/[\w\.]+)/i  // Google Search Appliance on iOS
+            ], [[NAME, 'GSA'], VERSION], [
+
+            /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i                     // Safari < 3.0
+            ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [
+
+            /(konqueror)\/([\w\.]+)/i,                                          // Konqueror
+            /(webkit|khtml)\/([\w\.]+)/i
+            ], [NAME, VERSION], [
+
+            // Gecko based
+            /(navigator|netscape)\/([\w\.-]+)/i                                 // Netscape
+            ], [[NAME, 'Netscape'], VERSION], [
+            /(swiftfox)/i,                                                      // Swiftfox
+            /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,
+                                                                                // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
+            /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i,
+                                                                                // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
+            /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i,                          // Mozilla
+
+            // Other
+            /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,
+                                                                                // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir
+            /(links)\s\(([\w\.]+)/i,                                            // Links
+            /(gobrowser)\/?([\w\.]+)*/i,                                        // GoBrowser
+            /(ice\s?browser)\/v?([\w\._]+)/i,                                   // ICE Browser
+            /(mosaic)[\/\s]([\w\.]+)/i                                          // Mosaic
+            ], [NAME, VERSION]
+
+            /* /////////////////////
+            // Media players BEGIN
+            ////////////////////////
+
+            , [
+
+            /(apple(?:coremedia|))\/((\d+)[\w\._]+)/i,                          // Generic Apple CoreMedia
+            /(coremedia) v((\d+)[\w\._]+)/i
+            ], [NAME, VERSION], [
+
+            /(aqualung|lyssna|bsplayer)\/((\d+)?[\w\.-]+)/i                     // Aqualung/Lyssna/BSPlayer
+            ], [NAME, VERSION], [
+
+            /(ares|ossproxy)\s((\d+)[\w\.-]+)/i                                 // Ares/OSSProxy
+            ], [NAME, VERSION], [
+
+            /(audacious|audimusicstream|amarok|bass|core|dalvik|gnomemplayer|music on console|nsplayer|psp-internetradioplayer|videos)\/((\d+)[\w\.-]+)/i,
+                                                                                // Audacious/AudiMusicStream/Amarok/BASS/OpenCORE/Dalvik/GnomeMplayer/MoC
+                                                                                // NSPlayer/PSP-InternetRadioPlayer/Videos
+            /(clementine|music player daemon)\s((\d+)[\w\.-]+)/i,               // Clementine/MPD
+            /(lg player|nexplayer)\s((\d+)[\d\.]+)/i,
+            /player\/(nexplayer|lg player)\s((\d+)[\w\.-]+)/i                   // NexPlayer/LG Player
+            ], [NAME, VERSION], [
+            /(nexplayer)\s((\d+)[\w\.-]+)/i                                     // Nexplayer
+            ], [NAME, VERSION], [
+
+            /(flrp)\/((\d+)[\w\.-]+)/i                                          // Flip Player
+            ], [[NAME, 'Flip Player'], VERSION], [
+
+            /(fstream|nativehost|queryseekspider|ia-archiver|facebookexternalhit)/i
+                                                                                // FStream/NativeHost/QuerySeekSpider/IA Archiver/facebookexternalhit
+            ], [NAME], [
+
+            /(gstreamer) souphttpsrc (?:\([^\)]+\)){0,1} libsoup\/((\d+)[\w\.-]+)/i
+                                                                                // Gstreamer
+            ], [NAME, VERSION], [
+
+            /(htc streaming player)\s[\w_]+\s\/\s((\d+)[\d\.]+)/i,              // HTC Streaming Player
+            /(java|python-urllib|python-requests|wget|libcurl)\/((\d+)[\w\.-_]+)/i,
+                                                                                // Java/urllib/requests/wget/cURL
+            /(lavf)((\d+)[\d\.]+)/i                                             // Lavf (FFMPEG)
+            ], [NAME, VERSION], [
+
+            /(htc_one_s)\/((\d+)[\d\.]+)/i                                      // HTC One S
+            ], [[NAME, /_/g, ' '], VERSION], [
+
+            /(mplayer)(?:\s|\/)(?:(?:sherpya-){0,1}svn)(?:-|\s)(r\d+(?:-\d+[\w\.-]+){0,1})/i
+                                                                                // MPlayer SVN
+            ], [NAME, VERSION], [
+
+            /(mplayer)(?:\s|\/|[unkow-]+)((\d+)[\w\.-]+)/i                      // MPlayer
+            ], [NAME, VERSION], [
+
+            /(mplayer)/i,                                                       // MPlayer (no other info)
+            /(yourmuze)/i,                                                      // YourMuze
+            /(media player classic|nero showtime)/i                             // Media Player Classic/Nero ShowTime
+            ], [NAME], [
+
+            /(nero (?:home|scout))\/((\d+)[\w\.-]+)/i                           // Nero Home/Nero Scout
+            ], [NAME, VERSION], [
+
+            /(nokia\d+)\/((\d+)[\w\.-]+)/i                                      // Nokia
+            ], [NAME, VERSION], [
+
+            /\s(songbird)\/((\d+)[\w\.-]+)/i                                    // Songbird/Philips-Songbird
+            ], [NAME, VERSION], [
+
+            /(winamp)3 version ((\d+)[\w\.-]+)/i,                               // Winamp
+            /(winamp)\s((\d+)[\w\.-]+)/i,
+            /(winamp)mpeg\/((\d+)[\w\.-]+)/i
+            ], [NAME, VERSION], [
+
+            /(ocms-bot|tapinradio|tunein radio|unknown|winamp|inlight radio)/i  // OCMS-bot/tap in radio/tunein/unknown/winamp (no other info)
+                                                                                // inlight radio
+            ], [NAME], [
+
+            /(quicktime|rma|radioapp|radioclientapplication|soundtap|totem|stagefright|streamium)\/((\d+)[\w\.-]+)/i
+                                                                                // QuickTime/RealMedia/RadioApp/RadioClientApplication/
+                                                                                // SoundTap/Totem/Stagefright/Streamium
+            ], [NAME, VERSION], [
+
+            /(smp)((\d+)[\d\.]+)/i                                              // SMP
+            ], [NAME, VERSION], [
+
+            /(vlc) media player - version ((\d+)[\w\.]+)/i,                     // VLC Videolan
+            /(vlc)\/((\d+)[\w\.-]+)/i,
+            /(xbmc|gvfs|xine|xmms|irapp)\/((\d+)[\w\.-]+)/i,                    // XBMC/gvfs/Xine/XMMS/irapp
+            /(foobar2000)\/((\d+)[\d\.]+)/i,                                    // Foobar2000
+            /(itunes)\/((\d+)[\d\.]+)/i                                         // iTunes
+            ], [NAME, VERSION], [
+
+            /(wmplayer)\/((\d+)[\w\.-]+)/i,                                     // Windows Media Player
+            /(windows-media-player)\/((\d+)[\w\.-]+)/i
+            ], [[NAME, /-/g, ' '], VERSION], [
+
+            /windows\/((\d+)[\w\.-]+) upnp\/[\d\.]+ dlnadoc\/[\d\.]+ (home media server)/i
+                                                                                // Windows Media Server
+            ], [VERSION, [NAME, 'Windows']], [
+
+            /(com\.riseupradioalarm)\/((\d+)[\d\.]*)/i                          // RiseUP Radio Alarm
+            ], [NAME, VERSION], [
+
+            /(rad.io)\s((\d+)[\d\.]+)/i,                                        // Rad.io
+            /(radio.(?:de|at|fr))\s((\d+)[\d\.]+)/i
+            ], [[NAME, 'rad.io'], VERSION]
+
+            //////////////////////
+            // Media players END
+            ////////////////////*/
+
+        ],
+
+        cpu : [[
+
+            /(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i                     // AMD64
+            ], [[ARCHITECTURE, 'amd64']], [
+
+            /(ia32(?=;))/i                                                      // IA32 (quicktime)
+            ], [[ARCHITECTURE, util.lowerize]], [
+
+            /((?:i[346]|x)86)[;\)]/i                                            // IA32
+            ], [[ARCHITECTURE, 'ia32']], [
+
+            // PocketPC mistakenly identified as PowerPC
+            /windows\s(ce|mobile);\sppc;/i
+            ], [[ARCHITECTURE, 'arm']], [
+
+            /((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i                           // PowerPC
+            ], [[ARCHITECTURE, /ower/, '', util.lowerize]], [
+
+            /(sun4\w)[;\)]/i                                                    // SPARC
+            ], [[ARCHITECTURE, 'sparc']], [
+
+            /((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i
+                                                                                // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
+            ], [[ARCHITECTURE, util.lowerize]]
+        ],
+
+        device : [[
+
+            /\((ipad|playbook);[\w\s\);-]+(rim|apple)/i                         // iPad/PlayBook
+            ], [MODEL, VENDOR, [TYPE, TABLET]], [
+
+            /applecoremedia\/[\w\.]+ \((ipad)/                                  // iPad
+            ], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [
+
+            /(apple\s{0,1}tv)/i                                                 // Apple TV
+            ], [[MODEL, 'Apple TV'], [VENDOR, 'Apple']], [
+
+            /(archos)\s(gamepad2?)/i,                                           // Archos
+            /(hp).+(touchpad)/i,                                                // HP TouchPad
+            /(hp).+(tablet)/i,                                                  // HP Tablet
+            /(kindle)\/([\w\.]+)/i,                                             // Kindle
+            /\s(nook)[\w\s]+build\/(\w+)/i,                                     // Nook
+            /(dell)\s(strea[kpr\s\d]*[\dko])/i                                  // Dell Streak
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i                               // Kindle Fire HD
+            ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
+            /(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i                  // Fire Phone
+            ], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [
+
+            /\((ip[honed|\s\w*]+);.+(apple)/i                                   // iPod/iPhone
+            ], [MODEL, VENDOR, [TYPE, MOBILE]], [
+            /\((ip[honed|\s\w*]+);/i                                            // iPod/iPhone
+            ], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [
+
+            /(blackberry)[\s-]?(\w+)/i,                                         // BlackBerry
+            /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i,
+                                                                                // BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron
+            /(hp)\s([\w\s]+\w)/i,                                               // HP iPAQ
+            /(asus)-?(\w+)/i                                                    // Asus
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /\(bb10;\s(\w+)/i                                                   // BlackBerry 10
+            ], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [
+                                                                                // Asus Tablets
+            /android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone)/i
+            ], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [
+
+            /(sony)\s(tablet\s[ps])\sbuild\//i,                                  // Sony
+            /(sony)?(?:sgp.+)\sbuild\//i
+            ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [
+            /android.+\s([c-g]\d{4}|so[-l]\w+)\sbuild\//i
+            ], [MODEL, [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+
+            /\s(ouya)\s/i,                                                      // Ouya
+            /(nintendo)\s([wids3u]+)/i                                          // Nintendo
+            ], [VENDOR, MODEL, [TYPE, CONSOLE]], [
+
+            /android.+;\s(shield)\sbuild/i                                      // Nvidia
+            ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
+
+            /(playstation\s[34portablevi]+)/i                                   // Playstation
+            ], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [
+
+            /(sprint\s(\w+))/i                                                  // Sprint Phones
+            ], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [
+
+            /(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i                         // Lenovo tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i,                               // HTC
+            /(zte)-(\w+)*/i,                                                    // ZTE
+            /(alcatel|geeksphone|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i
+                                                                                // Alcatel/GeeksPhone/Lenovo/Nexian/Panasonic/Sony
+            ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [
+
+            /(nexus\s9)/i                                                       // HTC Nexus 9
+            ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
+
+            /d\/huawei([\w\s-]+)[;\)]/i,
+            /(nexus\s6p)/i                                                      // Huawei
+            ], [MODEL, [VENDOR, 'Huawei'], [TYPE, MOBILE]], [
+
+            /(microsoft);\s(lumia[\s\w]+)/i                                     // Microsoft Lumia
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /[\s\(;](xbox(?:\sone)?)[\s\);]/i                                   // Microsoft Xbox
+            ], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [
+            /(kin\.[onetw]{3})/i                                                // Microsoft Kin
+            ], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [
+
+                                                                                // Motorola
+            /\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i,
+            /mot[\s-]?(\w+)*/i,
+            /(XT\d{3,4}) build\//i,
+            /(nexus\s6)/i
+            ], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [
+            /android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i
+            ], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [
+
+            /hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i            // HbbTV devices
+            ], [[VENDOR, util.trim], [MODEL, util.trim], [TYPE, SMARTTV]], [
+
+            /hbbtv.+maple;(\d+)/i
+            ], [[MODEL, /^/, 'SmartTV'], [VENDOR, 'Samsung'], [TYPE, SMARTTV]], [
+
+            /\(dtv[\);].+(aquos)/i                                              // Sharp
+            ], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [
+
+            /android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,
+            /((SM-T\w+))/i
+            ], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [                  // Samsung
+            /smart-tv.+(samsung)/i
+            ], [VENDOR, [TYPE, SMARTTV], MODEL], [
+            /((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,
+            /(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i,
+            /sec-((sgh\w+))/i
+            ], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [
+
+            /sie-(\w+)*/i                                                       // Siemens
+            ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
+
+            /(maemo|nokia).*(n900|lumia\s\d+)/i,                                // Nokia
+            /(nokia)[\s_-]?([\w-]+)*/i
+            ], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [
+
+            /android\s3\.[\s\w;-]{10}(a\d{3})/i                                 // Acer
+            ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [
+
+            /android.+([vl]k\-?\d{3})\s+build/i                                 // LG Tablet
+            ], [MODEL, [VENDOR, 'LG'], [TYPE, TABLET]], [
+            /android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i                     // LG Tablet
+            ], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [
+            /(lg) netcast\.tv/i                                                 // LG SmartTV
+            ], [VENDOR, MODEL, [TYPE, SMARTTV]], [
+            /(nexus\s[45])/i,                                                   // LG
+            /lg[e;\s\/-]+(\w+)*/i,
+            /android.+lg(\-?[\d\w]+)\s+build/i
+            ], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [
+
+            /android.+(ideatab[a-z0-9\-\s]+)/i                                  // Lenovo
+            ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [
+
+            /linux;.+((jolla));/i                                               // Jolla
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /((pebble))app\/[\d\.]+\s/i                                         // Pebble
+            ], [VENDOR, MODEL, [TYPE, WEARABLE]], [
+
+            /android.+;\s(oppo)\s?([\w\s]+)\sbuild/i                            // OPPO
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+
+            /crkey/i                                                            // Google Chromecast
+            ], [[MODEL, 'Chromecast'], [VENDOR, 'Google']], [
+
+            /android.+;\s(glass)\s\d/i                                          // Google Glass
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [
+
+            /android.+;\s(pixel c)\s/i                                          // Google Pixel C
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, TABLET]], [
+
+            /android.+;\s(pixel xl|pixel)\s/i                                   // Google Pixel
+            ], [MODEL, [VENDOR, 'Google'], [TYPE, MOBILE]], [
+
+            /android.+(\w+)\s+build\/hm\1/i,                                    // Xiaomi Hongmi 'numeric' models
+            /android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i,               // Xiaomi Hongmi
+            /android.+(mi[\s\-_]*(?:one|one[\s_]plus|note lte)?[\s_]*(?:\d\w)?)\s+build/i,    // Xiaomi Mi
+            /android.+(redmi[\s\-_]*(?:note)?(?:[\s_]*[\w\s]+)?)\s+build/i      // Redmi Phones
+            ], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [
+            /android.+(mi[\s\-_]*(?:pad)?(?:[\s_]*[\w\s]+)?)\s+build/i          // Mi Pad tablets
+            ],[[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, TABLET]], [
+            /android.+;\s(m[1-5]\snote)\sbuild/i                                // Meizu Tablet
+            ], [MODEL, [VENDOR, 'Meizu'], [TYPE, TABLET]], [
+
+            /android.+a000(1)\s+build/i                                         // OnePlus
+            ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(RCT[\d\w]+)\s+build/i                            // RCA Tablets
+            ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Venue[\d\s]*)\s+build/i                          // Dell Venue Tablets
+            ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Q[T|M][\d\w]+)\s+build/i                         // Verizon Tablet
+            ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s+(Barnes[&\s]+Noble\s+|BN[RT])(V?.*)\s+build/i     // Barnes & Noble Tablet
+            ], [[VENDOR, 'Barnes & Noble'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s+(TM\d{3}.*\b)\s+build/i                           // Barnes & Noble Tablet
+            ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(zte)?.+(k\d{2})\s+build/i                        // ZTE K Series Tablet
+            ], [[VENDOR, 'ZTE'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(gen\d{3})\s+build.*49h/i                         // Swiss GEN Mobile
+            ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(zur\d{3})\s+build/i                              // Swiss ZUR Tablet
+            ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*((Zeki)?TB.*\b)\s+build/i                         // Zeki Tablets
+            ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [
+
+            /(android).+[;\/]\s+([YR]\d{2}x?.*)\s+build/i,
+            /android.+[;\/]\s+(Dragon[\-\s]+Touch\s+|DT)(.+)\s+build/i          // Dragon Touch Tablet
+            ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(NS-?.+)\s+build/i                                // Insignia Tablets
+            ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*((NX|Next)-?.+)\s+build/i                         // NextBook Tablets
+            ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Xtreme\_?)?(V(1[045]|2[015]|30|40|60|7[05]|90))\s+build/i
+            ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [                    // Voice Xtreme Phones
+
+            /android.+[;\/]\s*(LVTEL\-?)?(V1[12])\s+build/i                     // LvTel Phones
+            ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [
+
+            /android.+[;\/]\s*(V(100MD|700NA|7011|917G).*\b)\s+build/i          // Envizen Tablets
+            ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Le[\s\-]+Pan)[\s\-]+(.*\b)\s+build/i             // Le Pan Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Trio[\s\-]*.*)\s+build/i                         // MachSpeed Tablets
+            ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*(Trinity)[\-\s]*(T\d{3})\s+build/i                // Trinity Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /android.+[;\/]\s*TU_(1491)\s+build/i                               // Rotor Tablets
+            ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [
+
+            /android.+(KS(.+))\s+build/i                                        // Amazon Kindle Tablets
+            ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
+
+            /android.+(Gigaset)[\s\-]+(Q.+)\s+build/i                           // Gigaset Tablets
+            ], [VENDOR, MODEL, [TYPE, TABLET]], [
+
+            /\s(tablet|tab)[;\/]/i,                                             // Unidentifiable Tablet
+            /\s(mobile)(?:[;\/]|\ssafari)/i                                     // Unidentifiable Mobile
+            ], [[TYPE, util.lowerize], VENDOR, MODEL], [
+
+            /(android.+)[;\/].+build/i                                          // Generic Android Device
+            ], [MODEL, [VENDOR, 'Generic']]
+
+
+        /*//////////////////////////
+            // TODO: move to string map
+            ////////////////////////////
+
+            /(C6603)/i                                                          // Sony Xperia Z C6603
+            ], [[MODEL, 'Xperia Z C6603'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+            /(C6903)/i                                                          // Sony Xperia Z 1
+            ], [[MODEL, 'Xperia Z 1'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [
+
+            /(SM-G900[F|H])/i                                                   // Samsung Galaxy S5
+            ], [[MODEL, 'Galaxy S5'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G7102)/i                                                       // Samsung Galaxy Grand 2
+            ], [[MODEL, 'Galaxy Grand 2'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G530H)/i                                                       // Samsung Galaxy Grand Prime
+            ], [[MODEL, 'Galaxy Grand Prime'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-G313HZ)/i                                                      // Samsung Galaxy V
+            ], [[MODEL, 'Galaxy V'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-T805)/i                                                        // Samsung Galaxy Tab S 10.5
+            ], [[MODEL, 'Galaxy Tab S 10.5'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
+            /(SM-G800F)/i                                                       // Samsung Galaxy S5 Mini
+            ], [[MODEL, 'Galaxy S5 Mini'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [
+            /(SM-T311)/i                                                        // Samsung Galaxy Tab 3 8.0
+            ], [[MODEL, 'Galaxy Tab 3 8.0'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [
+
+            /(T3C)/i                                                            // Advan Vandroid T3C
+            ], [MODEL, [VENDOR, 'Advan'], [TYPE, TABLET]], [
+            /(ADVAN T1J\+)/i                                                    // Advan Vandroid T1J+
+            ], [[MODEL, 'Vandroid T1J+'], [VENDOR, 'Advan'], [TYPE, TABLET]], [
+            /(ADVAN S4A)/i                                                      // Advan Vandroid S4A
+            ], [[MODEL, 'Vandroid S4A'], [VENDOR, 'Advan'], [TYPE, MOBILE]], [
+
+            /(V972M)/i                                                          // ZTE V972M
+            ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [
+
+            /(i-mobile)\s(IQ\s[\d\.]+)/i                                        // i-mobile IQ
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /(IQ6.3)/i                                                          // i-mobile IQ IQ 6.3
+            ], [[MODEL, 'IQ 6.3'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
+            /(i-mobile)\s(i-style\s[\d\.]+)/i                                   // i-mobile i-STYLE
+            ], [VENDOR, MODEL, [TYPE, MOBILE]], [
+            /(i-STYLE2.1)/i                                                     // i-mobile i-STYLE 2.1
+            ], [[MODEL, 'i-STYLE 2.1'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [
+
+            /(mobiistar touch LAI 512)/i                                        // mobiistar touch LAI 512
+            ], [[MODEL, 'Touch LAI 512'], [VENDOR, 'mobiistar'], [TYPE, MOBILE]], [
+
+            /////////////
+            // END TODO
+            ///////////*/
+
+        ],
+
+        engine : [[
+
+            /windows.+\sedge\/([\w\.]+)/i                                       // EdgeHTML
+            ], [VERSION, [NAME, 'EdgeHTML']], [
+
+            /(presto)\/([\w\.]+)/i,                                             // Presto
+            /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i,     // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m
+            /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i,                          // KHTML/Tasman/Links
+            /(icab)[\/\s]([23]\.[\d\.]+)/i                                      // iCab
+            ], [NAME, VERSION], [
+
+            /rv\:([\w\.]+).*(gecko)/i                                           // Gecko
+            ], [VERSION, NAME]
+        ],
+
+        os : [[
+
+            // Windows based
+            /microsoft\s(windows)\s(vista|xp)/i                                 // Windows (iTunes)
+            ], [NAME, VERSION], [
+            /(windows)\snt\s6\.2;\s(arm)/i,                                     // Windows RT
+            /(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s]+\w)*/i,                  // Windows Phone
+            /(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i
+            ], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [
+            /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i
+            ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [
+
+            // Mobile/Embedded OS
+            /\((bb)(10);/i                                                      // BlackBerry 10
+            ], [[NAME, 'BlackBerry'], VERSION], [
+            /(blackberry)\w*\/?([\w\.]+)*/i,                                    // Blackberry
+            /(tizen)[\/\s]([\w\.]+)/i,                                          // Tizen
+            /(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i,
+                                                                                // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki
+            /linux;.+(sailfish);/i                                              // Sailfish OS
+            ], [NAME, VERSION], [
+            /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i                 // Symbian
+            ], [[NAME, 'Symbian'], VERSION], [
+            /\((series40);/i                                                    // Series 40
+            ], [NAME], [
+            /mozilla.+\(mobile;.+gecko.+firefox/i                               // Firefox OS
+            ], [[NAME, 'Firefox OS'], VERSION], [
+
+            // Console
+            /(nintendo|playstation)\s([wids34portablevu]+)/i,                   // Nintendo/Playstation
+
+            // GNU/Linux based
+            /(mint)[\/\s\(]?(\w+)*/i,                                           // Mint
+            /(mageia|vectorlinux)[;\s]/i,                                       // Mageia/VectorLinux
+            /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]+)*/i,
+                                                                                // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware
+                                                                                // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus
+            /(hurd|linux)\s?([\w\.]+)*/i,                                       // Hurd/Linux
+            /(gnu)\s?([\w\.]+)*/i                                               // GNU
+            ], [NAME, VERSION], [
+
+            /(cros)\s[\w]+\s([\w\.]+\w)/i                                       // Chromium OS
+            ], [[NAME, 'Chromium OS'], VERSION],[
+
+            // Solaris
+            /(sunos)\s?([\w\.]+\d)*/i                                           // Solaris
+            ], [[NAME, 'Solaris'], VERSION], [
+
+            // BSD based
+            /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i                   // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly
+            ], [NAME, VERSION],[
+
+            /(haiku)\s(\w+)/i                                                  // Haiku
+            ], [NAME, VERSION],[
+
+            /cfnetwork\/.+darwin/i,
+            /ip[honead]+(?:.*os\s([\w]+)\slike\smac|;\sopera)/i                 // iOS
+            ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
+
+            /(mac\sos\sx)\s?([\w\s\.]+\w)*/i,
+            /(macintosh|mac(?=_powerpc)\s)/i                                    // Mac OS
+            ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [
+
+            // Other
+            /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i,                            // Solaris
+            /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i,                               // AIX
+            /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i,
+                                                                                // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS
+            /(unix)\s?([\w\.]+)*/i                                              // UNIX
+            ], [NAME, VERSION]
+        ]
+    };
+
+
+    /////////////////
+    // Constructor
+    ////////////////
+    /*
+    var Browser = function (name, version) {
+        this[NAME] = name;
+        this[VERSION] = version;
+    };
+    var CPU = function (arch) {
+        this[ARCHITECTURE] = arch;
+    };
+    var Device = function (vendor, model, type) {
+        this[VENDOR] = vendor;
+        this[MODEL] = model;
+        this[TYPE] = type;
+    };
+    var Engine = Browser;
+    var OS = Browser;
+    */
+    var UAParser = function (uastring, extensions) {
+
+        if (typeof uastring === 'object') {
+            extensions = uastring;
+            uastring = undefined;
+        }
+
+        if (!(this instanceof UAParser)) {
+            return new UAParser(uastring, extensions).getResult();
+        }
+
+        var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY);
+        var rgxmap = extensions ? util.extend(regexes, extensions) : regexes;
+        //var browser = new Browser();
+        //var cpu = new CPU();
+        //var device = new Device();
+        //var engine = new Engine();
+        //var os = new OS();
+
+        this.getBrowser = function () {
+            var browser = { name: undefined, version: undefined };
+            mapper.rgx.call(browser, ua, rgxmap.browser);
+            browser.major = util.major(browser.version); // deprecated
+            return browser;
+        };
+        this.getCPU = function () {
+            var cpu = { architecture: undefined };
+            mapper.rgx.call(cpu, ua, rgxmap.cpu);
+            return cpu;
+        };
+        this.getDevice = function () {
+            var device = { vendor: undefined, model: undefined, type: undefined };
+            mapper.rgx.call(device, ua, rgxmap.device);
+            return device;
+        };
+        this.getEngine = function () {
+            var engine = { name: undefined, version: undefined };
+            mapper.rgx.call(engine, ua, rgxmap.engine);
+            return engine;
+        };
+        this.getOS = function () {
+            var os = { name: undefined, version: undefined };
+            mapper.rgx.call(os, ua, rgxmap.os);
+            return os;
+        };
+        this.getResult = function () {
+            return {
+                ua      : this.getUA(),
+                browser : this.getBrowser(),
+                engine  : this.getEngine(),
+                os      : this.getOS(),
+                device  : this.getDevice(),
+                cpu     : this.getCPU()
+            };
+        };
+        this.getUA = function () {
+            return ua;
+        };
+        this.setUA = function (uastring) {
+            ua = uastring;
+            //browser = new Browser();
+            //cpu = new CPU();
+            //device = new Device();
+            //engine = new Engine();
+            //os = new OS();
+            return this;
+        };
+        return this;
+    };
+
+    UAParser.VERSION = LIBVERSION;
+    UAParser.BROWSER = {
+        NAME    : NAME,
+        MAJOR   : MAJOR, // deprecated
+        VERSION : VERSION
+    };
+    UAParser.CPU = {
+        ARCHITECTURE : ARCHITECTURE
+    };
+    UAParser.DEVICE = {
+        MODEL   : MODEL,
+        VENDOR  : VENDOR,
+        TYPE    : TYPE,
+        CONSOLE : CONSOLE,
+        MOBILE  : MOBILE,
+        SMARTTV : SMARTTV,
+        TABLET  : TABLET,
+        WEARABLE: WEARABLE,
+        EMBEDDED: EMBEDDED
+    };
+    UAParser.ENGINE = {
+        NAME    : NAME,
+        VERSION : VERSION
+    };
+    UAParser.OS = {
+        NAME    : NAME,
+        VERSION : VERSION
+    };
+    //UAParser.Utils = util;
+
+    ///////////
+    // Export
+    //////////
+
+
+    // check js environment
+    if (typeof(exports) !== UNDEF_TYPE) {
+        // nodejs env
+        if (typeof module !== UNDEF_TYPE && module.exports) {
+            exports = module.exports = UAParser;
+        }
+        // TODO: test!!!!!!!!
+        /*
+        if (require && require.main === module && process) {
+            // cli
+            var jsonize = function (arr) {
+                var res = [];
+                for (var i in arr) {
+                    res.push(new UAParser(arr[i]).getResult());
+                }
+                process.stdout.write(JSON.stringify(res, null, 2) + '\n');
+            };
+            if (process.stdin.isTTY) {
+                // via args
+                jsonize(process.argv.slice(2));
+            } else {
+                // via pipe
+                var str = '';
+                process.stdin.on('readable', function() {
+                    var read = process.stdin.read();
+                    if (read !== null) {
+                        str += read;
+                    }
+                });
+                process.stdin.on('end', function () {
+                    jsonize(str.replace(/\n$/, '').split('\n'));
+                });
+            }
+        }
+        */
+        exports.UAParser = UAParser;
+    } else {
+        // requirejs env (optional)
+        if (typeof(define) === FUNC_TYPE && define.amd) {
+            define(function () {
+                return UAParser;
+            });
+        } else if (window) {
+            // browser env
+            window.UAParser = UAParser;
+        }
+    }
+
+    // jQuery/Zepto specific (optional)
+    // Note:
+    //   In AMD env the global scope should be kept clean, but jQuery is an exception.
+    //   jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
+    //   and we should catch that.
+    var $ = window && (window.jQuery || window.Zepto);
+    if (typeof $ !== UNDEF_TYPE) {
+        var parser = new UAParser();
+        $.ua = parser.getResult();
+        $.ua.get = function () {
+            return parser.getUA();
+        };
+        $.ua.set = function (uastring) {
+            parser.setUA(uastring);
+            var result = parser.getResult();
+            for (var prop in result) {
+                $.ua[prop] = result[prop];
+            }
+        };
+    }
+
+})(typeof window === 'object' ? window : this);
+
+},{}],22:[function(require,module,exports){
+(function (global){
+
+var rng;
+
+var crypto = global.crypto || global.msCrypto; // for IE 11
+if (crypto && crypto.getRandomValues) {
+  // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
+  // Moderately fast, high quality
+  var _rnds8 = new Uint8Array(16);
+  rng = function whatwgRNG() {
+    crypto.getRandomValues(_rnds8);
+    return _rnds8;
+  };
+}
+
+if (!rng) {
+  // Math.random()-based (RNG)
+  //
+  // If all else fails, use Math.random().  It's fast, but is of unspecified
+  // quality.
+  var  _rnds = new Array(16);
+  rng = function() {
+    for (var i = 0, r; i < 16; i++) {
+      if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
+      _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
+    }
+
+    return _rnds;
+  };
+}
+
+module.exports = rng;
+
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],23:[function(require,module,exports){
+//     uuid.js
+//
+//     Copyright (c) 2010-2012 Robert Kieffer
+//     MIT License - http://opensource.org/licenses/mit-license.php
+
+// Unique ID creation requires a high quality random # generator.  We feature
+// detect to determine the best RNG source, normalizing to a function that
+// returns 128-bits of randomness, since that's what's usually required
+var _rng = require('./rng');
+
+// Maps for number <-> hex string conversion
+var _byteToHex = [];
+var _hexToByte = {};
+for (var i = 0; i < 256; i++) {
+  _byteToHex[i] = (i + 0x100).toString(16).substr(1);
+  _hexToByte[_byteToHex[i]] = i;
+}
+
+// **`parse()` - Parse a UUID into it's component bytes**
+function parse(s, buf, offset) {
+  var i = (buf && offset) || 0, ii = 0;
+
+  buf = buf || [];
+  s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
+    if (ii < 16) { // Don't overflow!
+      buf[i + ii++] = _hexToByte[oct];
+    }
+  });
+
+  // Zero out remaining bytes if string was short
+  while (ii < 16) {
+    buf[i + ii++] = 0;
+  }
+
+  return buf;
+}
+
+// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
+function unparse(buf, offset) {
+  var i = offset || 0, bth = _byteToHex;
+  return  bth[buf[i++]] + bth[buf[i++]] +
+          bth[buf[i++]] + bth[buf[i++]] + '-' +
+          bth[buf[i++]] + bth[buf[i++]] + '-' +
+          bth[buf[i++]] + bth[buf[i++]] + '-' +
+          bth[buf[i++]] + bth[buf[i++]] + '-' +
+          bth[buf[i++]] + bth[buf[i++]] +
+          bth[buf[i++]] + bth[buf[i++]] +
+          bth[buf[i++]] + bth[buf[i++]];
+}
+
+// **`v1()` - Generate time-based UUID**
+//
+// Inspired by https://github.com/LiosK/UUID.js
+// and http://docs.python.org/library/uuid.html
+
+// random #'s we need to init node and clockseq
+var _seedBytes = _rng();
+
+// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+var _nodeId = [
+  _seedBytes[0] | 0x01,
+  _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
+];
+
+// Per 4.2.2, randomize (14 bit) clockseq
+var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+
+// Previous uuid creation time
+var _lastMSecs = 0, _lastNSecs = 0;
+
+// See https://github.com/broofa/node-uuid for API details
+function v1(options, buf, offset) {
+  var i = buf && offset || 0;
+  var b = buf || [];
+
+  options = options || {};
+
+  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
+
+  // UUID timestamps are 100 nano-second units since the Gregorian epoch,
+  // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
+  // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
+  // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
+  var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
+
+  // Per 4.2.1.2, use count of uuid's generated during the current clock
+  // cycle to simulate higher resolution clock
+  var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
+
+  // Time since last uuid creation (in msecs)
+  var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
+
+  // Per 4.2.1.2, Bump clockseq on clock regression
+  if (dt < 0 && options.clockseq === undefined) {
+    clockseq = clockseq + 1 & 0x3fff;
+  }
+
+  // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
+  // time interval
+  if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
+    nsecs = 0;
+  }
+
+  // Per 4.2.1.2 Throw error if too many uuids are requested
+  if (nsecs >= 10000) {
+    throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
+  }
+
+  _lastMSecs = msecs;
+  _lastNSecs = nsecs;
+  _clockseq = clockseq;
+
+  // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
+  msecs += 12219292800000;
+
+  // `time_low`
+  var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
+  b[i++] = tl >>> 24 & 0xff;
+  b[i++] = tl >>> 16 & 0xff;
+  b[i++] = tl >>> 8 & 0xff;
+  b[i++] = tl & 0xff;
+
+  // `time_mid`
+  var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
+  b[i++] = tmh >>> 8 & 0xff;
+  b[i++] = tmh & 0xff;
+
+  // `time_high_and_version`
+  b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
+  b[i++] = tmh >>> 16 & 0xff;
+
+  // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
+  b[i++] = clockseq >>> 8 | 0x80;
+
+  // `clock_seq_low`
+  b[i++] = clockseq & 0xff;
+
+  // `node`
+  var node = options.node || _nodeId;
+  for (var n = 0; n < 6; n++) {
+    b[i + n] = node[n];
+  }
+
+  return buf ? buf : unparse(b);
+}
+
+// **`v4()` - Generate random UUID**
+
+// See https://github.com/broofa/node-uuid for API details
+function v4(options, buf, offset) {
+  // Deprecated - 'format' argument, as supported in v1.2
+  var i = buf && offset || 0;
+
+  if (typeof(options) == 'string') {
+    buf = options == 'binary' ? new Array(16) : null;
+    options = null;
+  }
+  options = options || {};
+
+  var rnds = options.random || (options.rng || _rng)();
+
+  // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
+  rnds[6] = (rnds[6] & 0x0f) | 0x40;
+  rnds[8] = (rnds[8] & 0x3f) | 0x80;
+
+  // Copy bytes to buffer, if provided
+  if (buf) {
+    for (var ii = 0; ii < 16; ii++) {
+      buf[i + ii] = rnds[ii];
+    }
+  }
+
+  return buf || unparse(rnds);
+}
+
+// Export public API
+var uuid = v4;
+uuid.v1 = v1;
+uuid.v4 = v4;
+uuid.parse = parse;
+uuid.unparse = unparse;
+
+module.exports = uuid;
+
+},{"./rng":22}],24:[function(require,module,exports){
+/*
+WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
+on @visionmedia's Emitter from UI Kit.
+
+Why? I wanted it standalone.
+
+I also wanted support for wildcard emitters like this:
+
+emitter.on('*', function (eventName, other, event, payloads) {
+
+});
+
+emitter.on('somenamespace*', function (eventName, payloads) {
+
+});
+
+Please note that callbacks triggered by wildcard registered events also get
+the event name as the first argument.
+*/
+
+module.exports = WildEmitter;
+
+function WildEmitter() { }
+
+WildEmitter.mixin = function (constructor) {
+    var prototype = constructor.prototype || constructor;
+
+    prototype.isWildEmitter= true;
+
+    // Listen on the given `event` with `fn`. Store a group name if present.
+    prototype.on = function (event, groupName, fn) {
+        this.callbacks = this.callbacks || {};
+        var hasGroup = (arguments.length === 3),
+            group = hasGroup ? arguments[1] : undefined,
+            func = hasGroup ? arguments[2] : arguments[1];
+        func._groupName = group;
+        (this.callbacks[event] = this.callbacks[event] || []).push(func);
+        return this;
+    };
+
+    // Adds an `event` listener that will be invoked a single
+    // time then automatically removed.
+    prototype.once = function (event, groupName, fn) {
+        var self = this,
+            hasGroup = (arguments.length === 3),
+            group = hasGroup ? arguments[1] : undefined,
+            func = hasGroup ? arguments[2] : arguments[1];
+        function on() {
+            self.off(event, on);
+            func.apply(this, arguments);
+        }
+        this.on(event, group, on);
+        return this;
+    };
+
+    // Unbinds an entire group
+    prototype.releaseGroup = function (groupName) {
+        this.callbacks = this.callbacks || {};
+        var item, i, len, handlers;
+        for (item in this.callbacks) {
+            handlers = this.callbacks[item];
+            for (i = 0, len = handlers.length; i < len; i++) {
+                if (handlers[i]._groupName === groupName) {
+                    //console.log('removing');
+                    // remove it and shorten the array we're looping through
+                    handlers.splice(i, 1);
+                    i--;
+                    len--;
+                }
+            }
+        }
+        return this;
+    };
+
+    // Remove the given callback for `event` or all
+    // registered callbacks.
+    prototype.off = function (event, fn) {
+        this.callbacks = this.callbacks || {};
+        var callbacks = this.callbacks[event],
+            i;
+
+        if (!callbacks) return this;
+
+        // remove all handlers
+        if (arguments.length === 1) {
+            delete this.callbacks[event];
+            return this;
+        }
+
+        // remove specific handler
+        i = callbacks.indexOf(fn);
+        callbacks.splice(i, 1);
+        if (callbacks.length === 0) {
+            delete this.callbacks[event];
+        }
+        return this;
+    };
+
+    /// Emit `event` with the given args.
+    // also calls any `*` handlers
+    prototype.emit = function (event) {
+        this.callbacks = this.callbacks || {};
+        var args = [].slice.call(arguments, 1),
+            callbacks = this.callbacks[event],
+            specialCallbacks = this.getWildcardCallbacks(event),
+            i,
+            len,
+            item,
+            listeners;
+
+        if (callbacks) {
+            listeners = callbacks.slice();
+            for (i = 0, len = listeners.length; i < len; ++i) {
+                if (!listeners[i]) {
+                    break;
+                }
+                listeners[i].apply(this, args);
+            }
+        }
+
+        if (specialCallbacks) {
+            len = specialCallbacks.length;
+            listeners = specialCallbacks.slice();
+            for (i = 0, len = listeners.length; i < len; ++i) {
+                if (!listeners[i]) {
+                    break;
+                }
+                listeners[i].apply(this, [event].concat(args));
+            }
+        }
+
+        return this;
+    };
+
+    // Helper for for finding special wildcard event handlers that match the event
+    prototype.getWildcardCallbacks = function (eventName) {
+        this.callbacks = this.callbacks || {};
+        var item,
+            split,
+            result = [];
+
+        for (item in this.callbacks) {
+            split = item.split('*');
+            if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
+                result = result.concat(this.callbacks[item]);
+            }
+        }
+        return result;
+    };
+
+};
+
+WildEmitter.mixin(WildEmitter);
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js
deleted file mode 100644
index 4e88a14cb52432fe068dd62adf6ec94350103ae2..0000000000000000000000000000000000000000
--- a/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js
+++ /dev/null
@@ -1,56 +0,0 @@
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kurentoUtils = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-function noop(e){e&&logger.error(e)}function trackStop(e){e.stop&&e.stop()}function streamStop(e){e.getTracks().forEach(trackStop)}function bufferizeCandidates(e,n){var t=[];return e.addEventListener("signalingstatechange",function(){if("stable"===this.signalingState)for(;t.length;){var e=t.shift();this.addIceCandidate(e.candidate,e.callback,e.callback)}}),function(r,i){switch(i=i||n,e.signalingState){case"closed":i(new Error("PeerConnection object is closed"));break;case"stable":if(e.remoteDescription){e.addIceCandidate(r,i,i);break}default:t.push({candidate:r,callback:i})}}}function removeFIDFromOffer(e){var n=e.indexOf("a=ssrc-group:FID");return n>0?e.slice(0,n):e}function getSimulcastInfo(e){var n=e.getVideoTracks();if(!n.length)return logger.warn("No video tracks available in the video stream"),"";var t=["a=x-google-flag:conference","a=ssrc-group:SIM 1 2 3","a=ssrc:1 cname:localVideo","a=ssrc:1 msid:"+e.id+" "+n[0].id,"a=ssrc:1 mslabel:"+e.id,"a=ssrc:1 label:"+n[0].id,"a=ssrc:2 cname:localVideo","a=ssrc:2 msid:"+e.id+" "+n[0].id,"a=ssrc:2 mslabel:"+e.id,"a=ssrc:2 label:"+n[0].id,"a=ssrc:3 cname:localVideo","a=ssrc:3 msid:"+e.id+" "+n[0].id,"a=ssrc:3 mslabel:"+e.id,"a=ssrc:3 label:"+n[0].id];return t.push(""),t.join("\n")}function WebRtcPeer(e,n,t){function r(){if(l){var e=p.getRemoteStreams()[0],n=e?URL.createObjectURL(e):"";l.pause(),l.src=n,l.load(),logger.info("Remote URL:",n)}}function i(e){return w&&("Chrome"===browser.name||"Chromium"===browser.name?(logger.info("Adding multicast info"),e=new RTCSessionDescription({type:e.type,sdp:removeFIDFromOffer(e.sdp)+getSimulcastInfo(u)})):logger.warn("Simulcast is only available in Chrome browser.")),e}function o(){"closed"===p.signalingState&&t('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'),u&&d&&c.showLocalVideo(),u&&p.addStream(u),f&&p.addStream(f);var n=parser.getBrowser();"sendonly"!==e||"Chrome"!==n.name&&"Chromium"!==n.name||39!==n.major||(e="sendrecv"),t()}function a(e){void 0===e&&(e=MEDIA_CONSTRAINTS),navigator.mediaDevices.getUserMedia(e).then(function(e){u=e,o()}).catch(t)}if(!(this instanceof WebRtcPeer))return new WebRtcPeer(e,n,t);WebRtcPeer.super_.call(this),n instanceof Function&&(t=n,n=void 0),n=n||{},t=(t||noop).bind(this);var s,c=this,d=n.localVideo,l=n.remoteVideo,u=n.videoStream,f=n.audioStream,g=n.mediaConstraints,p=(n.connectionConstraints,n.peerConnection),h=n.sendSource||"webcam",m=n.dataChannelConfig,v=n.dataChannels||!1,b=uuid.v4(),P=recursive({iceServers:freeice()},n.configuration),S=n.onicecandidate;S&&this.on("icecandidate",S);var R=n.oncandidategatheringdone;R&&this.on("candidategatheringdone",R);var w=n.simulcast,C=n.multistream,y=new sdpTranslator.Interop,D=[],W=!1;if(Object.defineProperties(this,{peerConnection:{get:function(){return p}},id:{value:n.id||b,writable:!1},remoteVideo:{get:function(){return l}},localVideo:{get:function(){return d}},dataChannel:{get:function(){return s}},currentFrame:{get:function(){if(l){if(l.readyState<l.HAVE_CURRENT_DATA)throw new Error("No video stream data available");var e=document.createElement("canvas");return e.width=l.videoWidth,e.height=l.videoHeight,e.getContext("2d").drawImage(l,0,0),e}}}}),!p&&(p=new RTCPeerConnection(P),v&&!s)){var E="WebRtcPeer-"+c.id,T=void 0;m&&(E=m.id||E,T=m.options),s=p.createDataChannel(E,T),m&&(s.onopen=m.onopen,s.onclose=m.onclose,s.onmessage=m.onmessage,s.onbufferedamountlow=m.onbufferedamountlow,s.onerror=m.onerror||noop)}p.addEventListener("icecandidate",function(e){var n=e.candidate;if(EventEmitter.listenerCount(c,"icecandidate")||EventEmitter.listenerCount(c,"candidategatheringdone"))if(n){var t;t=C&&usePlanB?y.candidateToUnifiedPlan(n):n,c.emit("icecandidate",t),W=!1}else W||(c.emit("candidategatheringdone"),W=!0);else W||(D.push(n),n||(W=!0))}),p.ontrack=n.onaddstream,p.onnegotiationneeded=n.onnegotiationneeded,this.on("newListener",function(e,n){if("icecandidate"===e||"candidategatheringdone"===e)for(;D.length;){var t=D.shift();!t==("candidategatheringdone"===e)&&n(t)}});var k=bufferizeCandidates(p);this.addIceCandidate=function(e,n){var t;t=C&&usePlanB?y.candidateToPlanB(e):new RTCIceCandidate(e),logger.debug("Remote ICE candidate received",e),n=(n||noop).bind(this),k(t,n)},this.generateOffer=function(n){n=n.bind(this);var t=!0,r=!0;g&&(t="boolean"!=typeof g.audio||g.audio,r="boolean"!=typeof g.video||g.video);var o={offerToReceiveAudio:"sendonly"!==e&&t,offerToReceiveVideo:"sendonly"!==e&&r},a=o;logger.info("constraints: "+JSON.stringify(a)),p.createOffer(a).then(function(e){return logger.info("Created SDP offer"),e=i(e),p.setLocalDescription(e)}).then(function(){var e=p.localDescription;logger.info("Local description set",e.sdp),C&&usePlanB&&(e=y.toUnifiedPlan(e),logger.info("offer::origPlanB->UnifiedPlan",dumpSDP(e))),n(null,e.sdp,c.processAnswer.bind(c))}).catch(n)},this.getLocalSessionDescriptor=function(){return p.localDescription},this.getRemoteSessionDescriptor=function(){return p.remoteDescription},this.showLocalVideo=function(){d.src=URL.createObjectURL(u),d.muted=!0},this.send=function(e){s&&"open"===s.readyState?s.send(e):logger.warn("Trying to send data over a non-existing or closed data channel")},this.processAnswer=function(e,n){n=(n||noop).bind(this);var t=new RTCSessionDescription({type:"answer",sdp:e});if(C&&usePlanB){var i=y.toPlanB(t);logger.info("asnwer::planB",dumpSDP(i)),t=i}if(logger.info("SDP answer received, setting remote description"),"closed"===p.signalingState)return n("PeerConnection is closed");p.setRemoteDescription(t,function(){r(),n()},n)},this.processOffer=function(e,n){n=n.bind(this);var t=new RTCSessionDescription({type:"offer",sdp:e});if(C&&usePlanB){var o=y.toPlanB(t);logger.info("offer::planB",dumpSDP(o)),t=o}if(logger.info("SDP offer received, setting remote description"),"closed"===p.signalingState)return n("PeerConnection is closed");p.setRemoteDescription(t).then(function(){return r()}).then(function(){return p.createAnswer()}).then(function(e){return e=i(e),logger.info("Created SDP answer"),p.setLocalDescription(e)}).then(function(){var e=p.localDescription;C&&usePlanB&&(e=y.toUnifiedPlan(e),logger.info("answer::origPlanB->UnifiedPlan",dumpSDP(e))),logger.info("Local description set",e.sdp),n(null,e.sdp)}).catch(n)},"recvonly"===e||u||f?setTimeout(o,0):"webcam"===h?a(g):getScreenConstraints(h,function(e,n){if(e)return t(e);constraints=[g],constraints.unshift(n),a(recursive.apply(void 0,constraints))},b),this.on("_dispose",function(){d&&(d.pause(),d.src="",d.load(),d.muted=!1),l&&(l.pause(),l.src="",l.load()),c.removeAllListeners(),void 0!==window.cancelChooseDesktopMedia&&window.cancelChooseDesktopMedia(b)})}function createEnableDescriptor(e){var n="get"+e+"Tracks";return{enumerable:!0,get:function(){if(this.peerConnection){var e=this.peerConnection.getLocalStreams();if(e.length){for(var t,r=0;t=e[r];r++)for(var i,o=t[n](),a=0;i=o[a];a++)if(!i.enabled)return!1;return!0}}},set:function(e){function t(n){n.enabled=e}this.peerConnection.getLocalStreams().forEach(function(e){e[n]().forEach(t)})}}}function WebRtcPeerRecvonly(e,n){if(!(this instanceof WebRtcPeerRecvonly))return new WebRtcPeerRecvonly(e,n);WebRtcPeerRecvonly.super_.call(this,"recvonly",e,n)}function WebRtcPeerSendonly(e,n){if(!(this instanceof WebRtcPeerSendonly))return new WebRtcPeerSendonly(e,n);WebRtcPeerSendonly.super_.call(this,"sendonly",e,n)}function WebRtcPeerSendrecv(e,n){if(!(this instanceof WebRtcPeerSendrecv))return new WebRtcPeerSendrecv(e,n);WebRtcPeerSendrecv.super_.call(this,"sendrecv",e,n)}function harkUtils(e,n){return hark(e,n)}var freeice=require("freeice"),inherits=require("inherits"),UAParser=require("ua-parser-js"),uuid=require("uuid"),hark=require("hark"),EventEmitter=require("events").EventEmitter,recursive=require("merge").recursive.bind(void 0,!0),sdpTranslator=require("sdp-translator"),logger=window.Logger||console;try{require("kurento-browser-extensions")}catch(e){"undefined"==typeof getScreenConstraints&&(logger.warn("screen sharing is not available"),getScreenConstraints=function(e,n){n(new Error("This library is not enabled for screen sharing"))})}var MEDIA_CONSTRAINTS={audio:!0,video:{width:640,framerate:15}},ua=window&&window.navigator?window.navigator.userAgent:"",parser=new UAParser(ua),browser=parser.getBrowser(),usePlanB=!1;"Chrome"!==browser.name&&"Chromium"!==browser.name||(logger.info(browser.name+": using SDP PlanB"),usePlanB=!0);var dumpSDP=function(e){return void 0===e||null===e?"":"type: "+e.type+"\r\n"+e.sdp};inherits(WebRtcPeer,EventEmitter),Object.defineProperties(WebRtcPeer.prototype,{enabled:{enumerable:!0,get:function(){return this.audioEnabled&&this.videoEnabled},set:function(e){this.audioEnabled=this.videoEnabled=e}},audioEnabled:createEnableDescriptor("Audio"),videoEnabled:createEnableDescriptor("Video")}),WebRtcPeer.prototype.getLocalStream=function(e){if(this.peerConnection)return this.peerConnection.getLocalStreams()[e||0]},WebRtcPeer.prototype.getRemoteStream=function(e){if(this.peerConnection)return this.peerConnection.getRemoteStreams()[e||0]},WebRtcPeer.prototype.dispose=function(){logger.info("Disposing WebRtcPeer");var e=this.peerConnection,n=this.dataChannel;try{if(n){if("closed"===n.signalingState)return;n.close()}if(e){if("closed"===e.signalingState)return;e.getLocalStreams().forEach(streamStop),e.close()}}catch(e){logger.warn("Exception disposing webrtc peer "+e)}this.emit("_dispose")},inherits(WebRtcPeerRecvonly,WebRtcPeer),inherits(WebRtcPeerSendonly,WebRtcPeer),inherits(WebRtcPeerSendrecv,WebRtcPeer),exports.bufferizeCandidates=bufferizeCandidates,exports.WebRtcPeerRecvonly=WebRtcPeerRecvonly,exports.WebRtcPeerSendonly=WebRtcPeerSendonly,exports.WebRtcPeerSendrecv=WebRtcPeerSendrecv,exports.hark=harkUtils;
-},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){
-window.addEventListener&&(module.exports=require("./index"));
-},{"./index":3}],3:[function(require,module,exports){
-var WebRtcPeer=require("./WebRtcPeer");exports.WebRtcPeer=WebRtcPeer;
-},{"./WebRtcPeer":1}],4:[function(require,module,exports){
-function EventEmitter(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function isFunction(e){return"function"==typeof e}function isNumber(e){return"number"==typeof e}function isObject(e){return"object"==typeof e&&null!==e}function isUndefined(e){return void 0===e}module.exports=EventEmitter,EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._maxListeners=void 0,EventEmitter.defaultMaxListeners=10,EventEmitter.prototype.setMaxListeners=function(e){if(!isNumber(e)||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},EventEmitter.prototype.emit=function(e){var t,i,n,s,r,o;if(this._events||(this._events={}),"error"===e&&(!this._events.error||isObject(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var h=new Error('Uncaught, unspecified "error" event. ('+t+")");throw h.context=t,h}if(i=this._events[e],isUndefined(i))return!1;if(isFunction(i))switch(arguments.length){case 1:i.call(this);break;case 2:i.call(this,arguments[1]);break;case 3:i.call(this,arguments[1],arguments[2]);break;default:s=Array.prototype.slice.call(arguments,1),i.apply(this,s)}else if(isObject(i))for(s=Array.prototype.slice.call(arguments,1),o=i.slice(),n=o.length,r=0;r<n;r++)o[r].apply(this,s);return!0},EventEmitter.prototype.addListener=function(e,t){var i;if(!isFunction(t))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,isFunction(t.listener)?t.listener:t),this._events[e]?isObject(this._events[e])?this._events[e].push(t):this._events[e]=[this._events[e],t]:this._events[e]=t,isObject(this._events[e])&&!this._events[e].warned&&(i=isUndefined(this._maxListeners)?EventEmitter.defaultMaxListeners:this._maxListeners)&&i>0&&this._events[e].length>i&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},EventEmitter.prototype.on=EventEmitter.prototype.addListener,EventEmitter.prototype.once=function(e,t){function i(){this.removeListener(e,i),n||(n=!0,t.apply(this,arguments))}if(!isFunction(t))throw TypeError("listener must be a function");var n=!1;return i.listener=t,this.on(e,i),this},EventEmitter.prototype.removeListener=function(e,t){var i,n,s,r;if(!isFunction(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(i=this._events[e],s=i.length,n=-1,i===t||isFunction(i.listener)&&i.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(isObject(i)){for(r=s;r-- >0;)if(i[r]===t||i[r].listener&&i[r].listener===t){n=r;break}if(n<0)return this;1===i.length?(i.length=0,delete this._events[e]):i.splice(n,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},EventEmitter.prototype.removeAllListeners=function(e){var t,i;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(i=this._events[e],isFunction(i))this.removeListener(e,i);else if(i)for(;i.length;)this.removeListener(e,i[i.length-1]);return delete this._events[e],this},EventEmitter.prototype.listeners=function(e){return this._events&&this._events[e]?isFunction(this._events[e])?[this._events[e]]:this._events[e].slice():[]},EventEmitter.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(isFunction(t))return 1;if(t)return t.length}return 0},EventEmitter.listenerCount=function(e,t){return e.listenerCount(t)};
-},{}],5:[function(require,module,exports){
-"use strict";var normalice=require("normalice"),freeice=module.exports=function(n){function t(n,t){for(var r,u=[],o=[].concat(e[n]);o.length&&u.length<t;)r=Math.random()*o.length|0,u=u.concat(o.splice(r,1));return u.map(function(t){return"string"==typeof t||t instanceof String?normalice(n+":"+t):t})}var r,e={stun:(n||{}).stun||require("./stun.json"),turn:(n||{}).turn||require("./turn.json")},u=(n||{}).stunCount||2,o=(n||{}).turnCount||0;return r=[].concat(t("stun",u)),o&&(r=r.concat(t("turn",o))),r};
-},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){
-module.exports=["stun.l.google.com:19302","stun1.l.google.com:19302","stun2.l.google.com:19302","stun3.l.google.com:19302","stun4.l.google.com:19302","stun.ekiga.net","stun.ideasip.com","stun.schlund.de","stun.stunprotocol.org:3478","stun.voiparound.com","stun.voipbuster.com","stun.voipstunt.com","stun.voxgratia.org","stun.services.mozilla.com"]
-},{}],7:[function(require,module,exports){
-module.exports=[]
-},{}],8:[function(require,module,exports){
-function getMaxVolume(e,t){var i=-1/0;e.getFloatFrequencyData(t);for(var n=4,o=t.length;n<o;n++)t[n]>i&&t[n]<0&&(i=t[n]);return i}var WildEmitter=require("wildemitter"),audioContextType=window.AudioContext||window.webkitAudioContext,audioContext=null;module.exports=function(e,t){var i=new WildEmitter;if(!audioContextType)return i;var t=t||{},n=t.smoothing||.1,o=t.interval||50,a=t.threshold,r=t.play,s=t.history||10,u=!0;audioContext||(audioContext=new audioContextType);var p,g,d;d=audioContext.createAnalyser(),d.fftSize=512,d.smoothingTimeConstant=n,g=new Float32Array(d.fftSize),e.jquery&&(e=e[0]),e instanceof HTMLAudioElement||e instanceof HTMLVideoElement?(p=audioContext.createMediaElementSource(e),void 0===r&&(r=!0),a=a||-50):(p=audioContext.createMediaStreamSource(e),a=a||-50),p.connect(d),r&&d.connect(audioContext.destination),i.speaking=!1,i.setThreshold=function(e){a=e},i.setInterval=function(e){o=e},i.stop=function(){u=!1,i.emit("volume_change",-100,a),i.speaking&&(i.speaking=!1,i.emit("stopped_speaking"))},i.speakingHistory=[];for(var l=0;l<s;l++)i.speakingHistory.push(0);var f=function(){setTimeout(function(){if(u){var e=getMaxVolume(d,g);i.emit("volume_change",e,a);var t=0;if(e>a&&!i.speaking){for(var n=i.speakingHistory.length-3;n<i.speakingHistory.length;n++)t+=i.speakingHistory[n];t>=2&&(i.speaking=!0,i.emit("speaking"))}else if(e<a&&i.speaking){for(var n=0;n<i.speakingHistory.length;n++)t+=i.speakingHistory[n];0==t&&(i.speaking=!1,i.emit("stopped_speaking"))}i.speakingHistory.shift(),i.speakingHistory.push(0+(e>a)),f()}},o)};return f(),i};
-},{"wildemitter":24}],9:[function(require,module,exports){
-"function"==typeof Object.create?module.exports=function(t,e){t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}})}:module.exports=function(t,e){t.super_=e;var o=function(){};o.prototype=e.prototype,t.prototype=new o,t.prototype.constructor=t};
-},{}],10:[function(require,module,exports){
-
-},{}],11:[function(require,module,exports){
-!function(e){function o(e,r){if("object"!==n(e))return r;for(var t in r)"object"===n(e[t])&&"object"===n(r[t])?e[t]=o(e[t],r[t]):e[t]=r[t];return e}function r(e,r,c){var u=c[0],f=c.length;(e||"object"!==n(u))&&(u={});for(var i=0;i<f;++i){var l=c[i];if("object"===n(l))for(var a in l){var v=e?t.clone(l[a]):l[a];u[a]=r?o(u[a],v):v}}return u}function n(e){return{}.toString.call(e).slice(8,-1).toLowerCase()}var t=function(e){return r(!0===e,!1,arguments)};t.recursive=function(e){return r(!0===e,!0,arguments)},t.clone=function(e){var o,r,c=e,u=n(e);if("array"===u)for(c=[],r=e.length,o=0;o<r;++o)c[o]=t.clone(e[o]);else if("object"===u){c={};for(o in e)c[o]=t.clone(e[o])}return c},e?module.exports=t:window.merge=t}("object"==typeof module&&module&&"object"==typeof module.exports&&module.exports);
-},{}],12:[function(require,module,exports){
-var protocols=["stun:","turn:"];module.exports=function(e){var r,t,n=(e||{}).url||e,l={};return"string"==typeof n||n instanceof String?(n=n.trim(),(r=protocols[protocols.indexOf(n.slice(0,5))])?(n=n.slice(5),t=n.split("@"),l.username=e.username,l.credential=e.credential,t.length>1&&(n=t[1],t=t[0].split(":"),l.username=t[0],l.credential=(e||{}).credential||t[1]||""),l.url=r+n,l.urls=[l.url],l):e):e};
-},{}],13:[function(require,module,exports){
-var grammar=module.exports={v:[{name:"version",reg:/^(\d*)$/}],o:[{name:"origin",reg:/^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,names:["username","sessionId","sessionVersion","netType","ipVer","address"],format:"%s %s %d %s IP%d %s"}],s:[{name:"name"}],i:[{name:"description"}],u:[{name:"uri"}],e:[{name:"email"}],p:[{name:"phone"}],z:[{name:"timezones"}],r:[{name:"repeats"}],t:[{name:"timing",reg:/^(\d*) (\d*)/,names:["start","stop"],format:"%d %d"}],c:[{name:"connection",reg:/^IN IP(\d) (\S*)/,names:["version","ip"],format:"IN IP%d %s"}],b:[{push:"bandwidth",reg:/^(TIAS|AS|CT|RR|RS):(\d*)/,names:["type","limit"],format:"%s:%s"}],m:[{reg:/^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,names:["type","port","protocol","payloads"],format:"%s %d %s %s"}],a:[{push:"rtp",reg:/^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,names:["payload","codec","rate","encoding"],format:function(e){return e.encoding?"rtpmap:%d %s/%s/%s":e.rate?"rtpmap:%d %s/%s":"rtpmap:%d %s"}},{push:"fmtp",reg:/^fmtp:(\d*) ([\S| ]*)/,names:["payload","config"],format:"fmtp:%d %s"},{name:"control",reg:/^control:(.*)/,format:"control:%s"},{name:"rtcp",reg:/^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,names:["port","netType","ipVer","address"],format:function(e){return null!=e.address?"rtcp:%d %s IP%d %s":"rtcp:%d"}},{push:"rtcpFbTrrInt",reg:/^rtcp-fb:(\*|\d*) trr-int (\d*)/,names:["payload","value"],format:"rtcp-fb:%d trr-int %d"},{push:"rtcpFb",reg:/^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,names:["payload","type","subtype"],format:function(e){return null!=e.subtype?"rtcp-fb:%s %s %s":"rtcp-fb:%s %s"}},{push:"ext",reg:/^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,names:["value","uri","config"],format:function(e){return null!=e.config?"extmap:%s %s %s":"extmap:%s %s"}},{push:"crypto",reg:/^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,names:["id","suite","config","sessionConfig"],format:function(e){return null!=e.sessionConfig?"crypto:%d %s %s %s":"crypto:%d %s %s"}},{name:"setup",reg:/^setup:(\w*)/,format:"setup:%s"},{name:"mid",reg:/^mid:([^\s]*)/,format:"mid:%s"},{name:"msid",reg:/^msid:(.*)/,format:"msid:%s"},{name:"ptime",reg:/^ptime:(\d*)/,format:"ptime:%d"},{name:"maxptime",reg:/^maxptime:(\d*)/,format:"maxptime:%d"},{name:"direction",reg:/^(sendrecv|recvonly|sendonly|inactive)/},{name:"icelite",reg:/^(ice-lite)/},{name:"iceUfrag",reg:/^ice-ufrag:(\S*)/,format:"ice-ufrag:%s"},{name:"icePwd",reg:/^ice-pwd:(\S*)/,format:"ice-pwd:%s"},{name:"fingerprint",reg:/^fingerprint:(\S*) (\S*)/,names:["type","hash"],format:"fingerprint:%s %s"},{push:"candidates",reg:/^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,names:["foundation","component","transport","priority","ip","port","type","raddr","rport","tcptype","generation"],format:function(e){var r="candidate:%s %d %s %d %s %d typ %s";return r+=null!=e.raddr?" raddr %s rport %d":"%v%v",r+=null!=e.tcptype?" tcptype %s":"%v",null!=e.generation&&(r+=" generation %d"),r}},{name:"endOfCandidates",reg:/^(end-of-candidates)/},{name:"remoteCandidates",reg:/^remote-candidates:(.*)/,format:"remote-candidates:%s"},{name:"iceOptions",reg:/^ice-options:(\S*)/,format:"ice-options:%s"},{push:"ssrcs",reg:/^ssrc:(\d*) ([\w_]*):(.*)/,names:["id","attribute","value"],format:"ssrc:%d %s:%s"},{push:"ssrcGroups",reg:/^ssrc-group:(\w*) (.*)/,names:["semantics","ssrcs"],format:"ssrc-group:%s %s"},{name:"msidSemantic",reg:/^msid-semantic:\s?(\w*) (\S*)/,names:["semantic","token"],format:"msid-semantic: %s %s"},{push:"groups",reg:/^group:(\w*) (.*)/,names:["type","mids"],format:"group:%s %s"},{name:"rtcpMux",reg:/^(rtcp-mux)/},{name:"rtcpRsize",reg:/^(rtcp-rsize)/},{push:"invalid",names:["value"]}]};Object.keys(grammar).forEach(function(e){grammar[e].forEach(function(e){e.reg||(e.reg=/(.*)/),e.format||(e.format="%s")})});
-},{}],14:[function(require,module,exports){
-var parser=require("./parser"),writer=require("./writer");exports.write=writer,exports.parse=parser.parse,exports.parseFmtpConfig=parser.parseFmtpConfig,exports.parsePayloads=parser.parsePayloads,exports.parseRemoteCandidates=parser.parseRemoteCandidates;
-},{"./parser":15,"./writer":16}],15:[function(require,module,exports){
-var toIntIfInt=function(t){return String(Number(t))===t?Number(t):t},attachProperties=function(t,r,e,n){if(n&&!e)r[n]=toIntIfInt(t[1]);else for(var a=0;a<e.length;a+=1)null!=t[a+1]&&(r[e[a]]=toIntIfInt(t[a+1]))},parseReg=function(t,r,e){var n=t.name&&t.names;t.push&&!r[t.push]?r[t.push]=[]:n&&!r[t.name]&&(r[t.name]={});var a=t.push?{}:n?r[t.name]:r;attachProperties(e.match(t.reg),a,t.names,t.name),t.push&&r[t.push].push(a)},grammar=require("./grammar"),validLine=RegExp.prototype.test.bind(/^([a-z])=(.*)/);exports.parse=function(t){var r={},e=[],n=r;return t.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function(t){var r=t[0],a=t.slice(2);"m"===r&&(e.push({rtp:[],fmtp:[]}),n=e[e.length-1]);for(var p=0;p<(grammar[r]||[]).length;p+=1){var s=grammar[r][p];if(s.reg.test(a))return parseReg(s,n,a)}}),r.media=e,r};var fmtpReducer=function(t,r){var e=r.split("=");return 2===e.length&&(t[e[0]]=toIntIfInt(e[1])),t};exports.parseFmtpConfig=function(t){return t.split(/\;\s?/).reduce(fmtpReducer,{})},exports.parsePayloads=function(t){return t.split(" ").map(Number)},exports.parseRemoteCandidates=function(t){for(var r=[],e=t.split(" ").map(toIntIfInt),n=0;n<e.length;n+=3)r.push({component:e[n],ip:e[n+1],port:e[n+2]});return r};
-},{"./grammar":13}],16:[function(require,module,exports){
-var grammar=require("./grammar"),formatRegExp=/%[sdv%]/g,format=function(n){var r=1,e=arguments,a=e.length;return n.replace(formatRegExp,function(n){if(r>=a)return n;var u=e[r];switch(r+=1,n){case"%%":return"%";case"%s":return String(u);case"%d":return Number(u);case"%v":return""}})},makeLine=function(n,r,e){var a=r.format instanceof Function?r.format(r.push?e:e[r.name]):r.format,u=[n+"="+a];if(r.names)for(var m=0;m<r.names.length;m+=1){var t=r.names[m];r.name?u.push(e[r.name][t]):u.push(e[r.names[m]])}else u.push(e[r.name]);return format.apply(null,u)},defaultOuterOrder=["v","o","s","i","u","e","p","c","b","t","r","z","a"],defaultInnerOrder=["i","c","b","a"];module.exports=function(n,r){r=r||{},null==n.version&&(n.version=0),null==n.name&&(n.name=" "),n.media.forEach(function(n){null==n.payloads&&(n.payloads="")});var e=r.outerOrder||defaultOuterOrder,a=r.innerOrder||defaultInnerOrder,u=[];return e.forEach(function(r){grammar[r].forEach(function(e){e.name in n&&null!=n[e.name]?u.push(makeLine(r,e,n)):e.push in n&&null!=n[e.push]&&n[e.push].forEach(function(n){u.push(makeLine(r,e,n))})})}),n.media.forEach(function(n){u.push(makeLine("m",grammar.m[0],n)),a.forEach(function(r){grammar[r].forEach(function(e){e.name in n&&null!=n[e.name]?u.push(makeLine(r,e,n)):e.push in n&&null!=n[e.push]&&n[e.push].forEach(function(n){u.push(makeLine(r,e,n))})})})}),u.join("\r\n")+"\r\n"};
-},{"./grammar":13}],17:[function(require,module,exports){
-module.exports=function r(t){if(!t)return!1;if(this.length!=t.length)return!1;for(var e=0,n=this.length;e<n;e++)if(this[e]instanceof Array&&t[e]instanceof Array){if(!r.apply(this[e],[t[e]]))return!1}else if(this[e]!=t[e])return!1;return!0};
-},{}],18:[function(require,module,exports){
-exports.Interop=require("./interop");
-},{"./interop":19}],19:[function(require,module,exports){
-"use strict";function Interop(){this.cache={mlB2UMap:{},mlU2BMap:{}}}function addSetupAttr(e){void 0!==e.setup&&("active"===e.setup?e.setup="passive":"passive"===e.setup&&(e.setup="active"))}var transform=require("./transform"),arrayEquals=require("./array-equals");module.exports=Interop,Interop.prototype.candidateToUnifiedPlan=function(e){var r=new RTCIceCandidate(e);return r.sdpMLineIndex=this.cache.mlB2UMap[r.sdpMLineIndex],r},Interop.prototype.candidateToPlanB=function(e){var r=new RTCIceCandidate(e);if(0===r.sdpMid.indexOf("audio"))r.sdpMid="audio";else{if(0!==r.sdpMid.indexOf("video"))throw new Error("candidate with "+r.sdpMid+" not allowed");r.sdpMid="video"}return r.sdpMLineIndex=this.cache.mlU2BMap[r.sdpMLineIndex],r},Interop.prototype.getFirstSendingIndexFromAnswer=function(e){if(!this.cache.answer)return null;var r=transform.parse(this.cache.answer);if(r&&r.media&&Array.isArray(r.media))for(var i=0;i<r.media.length;i++)if(r.media[i].type==e&&(!r.media[i].direction||"sendrecv"===r.media[i].direction||"sendonly"===r.media[i].direction))return i;return null},Interop.prototype.toPlanB=function(e){var r=this;if("object"!=typeof e||null===e||"string"!=typeof e.sdp)return console.warn("An empty description was passed as an argument."),e;var i=transform.parse(e.sdp);if(void 0===i.media||!Array.isArray(i.media)||0===i.media.length)return console.warn("The description has no media."),e;if(i.media.length<=3&&i.media.every(function(e){return-1!==["video","audio","data"].indexOf(e.mid)}))return console.warn("This description does not look like Unified Plan."),e;for(var t=e.sdp,o=!1,n=0;n<i.media.length;n++){i.media[n].rtp.forEach(function(e){if("NULL"===e.codec){o=!0;var i=transform.parse(r.cache.offer);e.codec=i.media[n].rtp[0].codec}})}o&&(t=transform.write(i)),this.cache[e.type]=t;var s=i.media;i.media=[];var a={},d=[];s.forEach(function(e){if(("string"!=typeof e.rtcpMux||"rtcp-mux"!==e.rtcpMux)&&"inactive"!==e.direction)throw new Error("Cannot convert to Plan B because m-lines without the rtcp-mux attribute were found.");if(void 0!==a[e.type]&&"inactive"!==a[e.type].direction||(a[e.type]=e),e.protocol!=a[e.type].protocol)throw new Error("Cannot convert to Plan B because m-lines have different protocols and this library does not have support for that");if(e.payloads!=a[e.type].payloads)throw new Error("Cannot convert to Plan B because m-lines have different payloads and this library does not have support for that")}),s.forEach(function(e){if("application"===e.type)return i.media.push(e),void d.push(e.mid);"object"==typeof e.sources&&Object.keys(e.sources).forEach(function(r){"object"!=typeof a[e.type].sources&&(a[e.type].sources={}),a[e.type].sources[r]=e.sources[r],void 0!==e.msid&&(a[e.type].sources[r].msid=e.msid)}),void 0!==e.ssrcGroups&&Array.isArray(e.ssrcGroups)&&(void 0!==a[e.type].ssrcGroups&&Array.isArray(a[e.type].ssrcGroups)||(a[e.type].ssrcGroups=[]),a[e.type].ssrcGroups=a[e.type].ssrcGroups.concat(e.ssrcGroups)),a[e.type]===e&&(e.mid=e.type,delete e.bundleOnly,delete e.msid,e.type==s[0].type?(d.unshift(e.type),i.media.unshift(e)):(d.push(e.type),i.media.push(e)))}),void 0!==i.groups&&i.groups.some(function(e){if("BUNDLE"===e.type)return e.mids=d.join(" "),!0}),i.msidSemantic={semantic:"WMS",token:"*"};var c=transform.write(i);return new RTCSessionDescription({type:e.type,sdp:c})},Interop.prototype.toUnifiedPlan=function(e){var r=this;if("object"!=typeof e||null===e||"string"!=typeof e.sdp)return console.warn("An empty description was passed as an argument."),e;var i=transform.parse(e.sdp);if(void 0===i.media||!Array.isArray(i.media)||0===i.media.length)return console.warn("The description has no media."),e;if(i.media.length>3||!i.media.every(function(e){return-1!==["video","audio","data"].indexOf(e.mid)}))return console.warn("This description does not look like Plan B."),e;var t=[];i.media.forEach(function(e){t.push(e.mid)});var o=!1;if(void 0!==i.groups&&Array.isArray(i.groups)&&(o=i.groups.every(function(e){return"BUNDLE"!==e.type||arrayEquals.apply(e.mids.sort(),[t.sort()])})),!o){var n=!1;if(i.media.forEach(function(e){"inactive"!==e.direction&&(n=!0)}),n)throw new Error("Cannot convert to Unified Plan because m-lines that are not bundled were found.")}var s;if("answer"===e.type)s="offer";else{if("offer"!==e.type)throw new Error("Type '"+e.type+"' not supported.");s="answer"}var a;void 0!==this.cache[s]&&(a=transform.parse(this.cache[s]));var d,c,p,u,f={audio:{},video:{}},m={},y=0,l=0,v={},h={},w={},g={};if(i.media.forEach(function(i){if(("string"!=typeof i.rtcpMux||"rtcp-mux"!==i.rtcpMux)&&"inactive"!==i.direction)throw new Error("Cannot convert to Unified Plan because m-lines without the rtcp-mux attribute were found.");if("application"===i.type)return void(m[i.mid]=i);var t=i.sources,o=i.ssrcGroups,n=i.port;if(void 0!==i.candidates&&(d=void 0!==d?d.concat(i.candidates):i.candidates),void 0!==c&&void 0!==i.iceUfrag&&c!=i.iceUfrag)throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n\tLast iceUfrag: "+c+"\n\tNew iceUfrag: "+i.iceUfrag);if(void 0!==i.iceUfrag&&(c=i.iceUfrag),void 0!==p&&void 0!==i.icePwd&&p!=i.icePwd)throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n\tLast icePwd: "+p+"\n\tNew icePwd: "+i.icePwd);if(void 0!==i.icePwd&&(p=i.icePwd),void 0!==u&&void 0!==i.fingerprint&&(u.type!=i.fingerprint.type||u.hash!=i.fingerprint.hash))throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n\tLast fingerprint: "+JSON.stringify(u)+"\n\tNew fingerprint: "+JSON.stringify(i.fingerprint));void 0!==i.fingerprint&&(u=i.fingerprint),h[i.type]=i.payloads,w[i.type]=i.rtcpFb,g[i.type]=i.rtp;var s={};void 0!==o&&Array.isArray(o)&&o.forEach(function(e){void 0!==e.ssrcs&&Array.isArray(e.ssrcs)&&e.ssrcs.forEach(function(r){void 0===s[r]&&(s[r]=[]),s[r].push(e)})});var E={};if("object"==typeof t)delete i.sources,delete i.ssrcGroups,delete i.candidates,delete i.iceUfrag,delete i.icePwd,delete i.fingerprint,delete i.port,delete i.mid,Object.keys(t).forEach(function(o){var h;if("offer"===e.type&&!t[o].msid)return void(f[i.type][o]=t[o]);void 0!==s[o]&&Array.isArray(s[o])&&s[o].some(function(e){return e.ssrcs.some(function(e){if("object"==typeof E[e])return h=E[e],!0})}),"object"==typeof h?(h.sources[o]=t[o],delete t[o].msid):(h=Object.create(i),E[o]=h,void 0!==t[o].msid&&(h.msid=t[o].msid,delete t[o].msid),h.sources={},h.sources[o]=t[o],h.ssrcGroups=s[o],void 0!==a&&void 0!==a.media&&Array.isArray(a.media)&&a.media.forEach(function(e){"object"==typeof e.sources&&Object.keys(e.sources).forEach(function(r){r===o&&(h.mid=e.mid)})}),void 0===h.mid&&(h.mid=[i.type,"-",o].join("")),h.candidates=d,h.iceUfrag=c,h.icePwd=p,h.fingerprint=u,h.port=n,m[h.mid]=h,v[l]=h.sources,r.cache.mlU2BMap[l]=y,void 0===r.cache.mlB2UMap[y]&&(r.cache.mlB2UMap[y]=l),l++)});else{var U=i;U.candidates=d,U.iceUfrag=c,U.icePwd=p,U.fingerprint=u,U.port=n,m[U.mid]=U,r.cache.mlU2BMap[l]=y,void 0===r.cache.mlB2UMap[y]&&(r.cache.mlB2UMap[y]=l)}y++}),i.media=[],t=[],"answer"===e.type)for(var E=0;E<a.media.length;E++){var U=a.media[E];delete U.msid,delete U.sources,delete U.ssrcGroups,void 0===v[E]?U.direction&&"sendrecv"!==U.direction?"sendonly"===U.direction&&(U.direction="inactive"):U.direction="recvonly":U.direction&&"sendrecv"!==U.direction?"recvonly"===U.direction&&(U.direction="sendonly"):U.direction="sendrecv",U.sources=v[E],U.candidates=d,U.iceUfrag=c,U.icePwd=p,U.fingerprint=u,U.rtp=g[U.type],U.payloads=h[U.type],U.rtcpFb=w[U.type],i.media.push(U),"string"==typeof U.mid&&t.push(U.mid)}else void 0!==a&&void 0!==a.media&&Array.isArray(a.media)&&a.media.forEach(function(e){t.push(e.mid),void 0!==m[e.mid]?i.media.push(m[e.mid]):(delete e.msid,delete e.sources,delete e.ssrcGroups,e.direction&&"sendrecv"!==e.direction||(e.direction="sendonly"),e.direction&&"recvonly"!==e.direction||(e.direction="inactive"),addSetupAttr(e),i.media.push(e))}),Object.keys(m).forEach(function(e){if(-1===t.indexOf(e))if(t.push(e),"recvonly"===m[e].direction){var r=!1;i.media.some(function(i){if(("sendrecv"===i.direction||"sendonly"===i.direction)&&i.type===m[e].type)return Object.keys(m[e].sources).forEach(function(r){i.sources[r]=m[e].sources[r]}),r=!0,!0}),r||i.media.push(m[e])}else i.media.push(m[e])});["audio","video"].forEach(function(e){if(i&&i.media&&Array.isArray(i.media)){var t=null;if(Object.keys(f[e]).length>0&&null===(t=r.getFirstSendingIndexFromAnswer(e)))for(var o=0;o<i.media.length;o++)if(i.media[o].type===e){t=o;break}if(t&&i.media.length>t){var n=i.media[t];Object.keys(f[e]).forEach(function(r){n.sources&&n.sources[r]&&console.warn("Replacing an existing SSRC."),n.sources||(n.sources={}),n.sources[r]=f[e][r]})}}}),void 0!==i.groups&&i.groups.some(function(e){if("BUNDLE"===e.type)return e.mids=t.join(" "),!0}),i.msidSemantic={semantic:"WMS",token:"*"};var b=transform.write(i);return this.cache[e.type]=b,new RTCSessionDescription({type:e.type,sdp:b})};
-},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){
-var transform=require("sdp-transform");exports.write=function(s,r){return void 0!==s&&void 0!==s.media&&Array.isArray(s.media)&&s.media.forEach(function(s){void 0!==s.sources&&0!==Object.keys(s.sources).length&&(s.ssrcs=[],Object.keys(s.sources).forEach(function(r){var o=s.sources[r];Object.keys(o).forEach(function(i){s.ssrcs.push({id:r,attribute:i,value:o[i]})})}),delete s.sources),void 0!==s.ssrcGroups&&Array.isArray(s.ssrcGroups)&&s.ssrcGroups.forEach(function(s){void 0!==s.ssrcs&&Array.isArray(s.ssrcs)&&(s.ssrcs=s.ssrcs.join(" "))})}),void 0!==s&&void 0!==s.groups&&Array.isArray(s.groups)&&s.groups.forEach(function(s){void 0!==s.mids&&Array.isArray(s.mids)&&(s.mids=s.mids.join(" "))}),transform.write(s,r)},exports.parse=function(s){var r=transform.parse(s);return void 0!==r&&void 0!==r.media&&Array.isArray(r.media)&&r.media.forEach(function(s){void 0!==s.ssrcs&&Array.isArray(s.ssrcs)&&(s.sources={},s.ssrcs.forEach(function(r){s.sources[r.id]||(s.sources[r.id]={}),s.sources[r.id][r.attribute]=r.value}),delete s.ssrcs),void 0!==s.ssrcGroups&&Array.isArray(s.ssrcGroups)&&s.ssrcGroups.forEach(function(s){"string"==typeof s.ssrcs&&(s.ssrcs=s.ssrcs.split(" "))})}),void 0!==r&&void 0!==r.groups&&Array.isArray(r.groups)&&r.groups.forEach(function(s){"string"==typeof s.mids&&(s.mids=s.mids.split(" "))}),r};
-},{"sdp-transform":14}],21:[function(require,module,exports){
-!function(i,e){"use strict";var s="model",o="name",r="type",n="vendor",a="version",t="mobile",w="tablet",d={extend:function(i,e){var s={};for(var o in i)e[o]&&e[o].length%2==0?s[o]=e[o].concat(i[o]):s[o]=i[o];return s},has:function(i,e){return"string"==typeof i&&-1!==e.toLowerCase().indexOf(i.toLowerCase())},lowerize:function(i){return i.toLowerCase()},major:function(i){return"string"==typeof i?i.replace(/[^\d\.]/g,"").split(".")[0]:void 0},trim:function(i){return i.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},l={rgx:function(){for(var i,e,s,o,r,n,a,t=0,w=arguments;t<w.length&&!n;){var d=w[t],l=w[t+1];if(void 0===i){i={};for(o in l)l.hasOwnProperty(o)&&(r=l[o],"object"==typeof r?i[r[0]]=void 0:i[r]=void 0)}for(e=s=0;e<d.length&&!n;)if(n=d[e++].exec(this.getUA()))for(o=0;o<l.length;o++)a=n[++s],r=l[o],"object"==typeof r&&r.length>0?2==r.length?"function"==typeof r[1]?i[r[0]]=r[1].call(this,a):i[r[0]]=r[1]:3==r.length?"function"!=typeof r[1]||r[1].exec&&r[1].test?i[r[0]]=a?a.replace(r[1],r[2]):void 0:i[r[0]]=a?r[1].call(this,a,r[2]):void 0:4==r.length&&(i[r[0]]=a?r[3].call(this,a.replace(r[1],r[2])):void 0):i[r]=a||void 0;t+=2}return i},str:function(i,e){for(var s in e)if("object"==typeof e[s]&&e[s].length>0){for(var o=0;o<e[s].length;o++)if(d.has(e[s][o],i))return"?"===s?void 0:s}else if(d.has(e[s],i))return"?"===s?void 0:s;return i}},c={browser:{oldsafari:{version:{"1.0":"/8",1.2:"/1",1.3:"/3","2.0":"/412","2.0.2":"/416","2.0.3":"/417","2.0.4":"/419","?":"/"}}},device:{amazon:{model:{"Fire Phone":["SD","KF"]}},sprint:{model:{"Evo Shift 4G":"7373KT"},vendor:{HTC:"APA",Sprint:"Sprint"}}},os:{windows:{version:{ME:"4.90","NT 3.11":"NT3.51","NT 4.0":"NT4.0",2000:"NT 5.0",XP:["NT 5.1","NT 5.2"],Vista:"NT 6.0",7:"NT 6.1",8:"NT 6.2",8.1:"NT 6.3",10:["NT 6.4","NT 10.0"],RT:"ARM"}}}},u={browser:[[/(opera\smini)\/([\w\.-]+)/i,/(opera\s[mobiletab]+).+version\/([\w\.-]+)/i,/(opera).+version\/([\w\.]+)/i,/(opera)[\/\s]+([\w\.]+)/i],[o,a],[/(opios)[\/\s]+([\w\.]+)/i],[[o,"Opera Mini"],a],[/\s(opr)\/([\w\.]+)/i],[[o,"Opera"],a],[/(kindle)\/([\w\.]+)/i,/(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i,/(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i,/(?:ms|\()(ie)\s([\w\.]+)/i,/(rekonq)\/([\w\.]+)*/i,/(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs)\/([\w\.-]+)/i],[o,a],[/(trident).+rv[:\s]([\w\.]+).+like\sgecko/i],[[o,"IE"],a],[/(edge)\/((\d+)?[\w\.]+)/i],[o,a],[/(yabrowser)\/([\w\.]+)/i],[[o,"Yandex"],a],[/(comodo_dragon)\/([\w\.]+)/i],[[o,/_/g," "],a],[/(micromessenger)\/([\w\.]+)/i],[[o,"WeChat"],a],[/xiaomi\/miuibrowser\/([\w\.]+)/i],[a,[o,"MIUI Browser"]],[/\swv\).+(chrome)\/([\w\.]+)/i],[[o,/(.+)/,"$1 WebView"],a],[/android.+samsungbrowser\/([\w\.]+)/i,/android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i],[a,[o,"Android Browser"]],[/(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i,/(qqbrowser)[\/\s]?([\w\.]+)/i],[o,a],[/(uc\s?browser)[\/\s]?([\w\.]+)/i,/ucweb.+(ucbrowser)[\/\s]?([\w\.]+)/i,/juc.+(ucweb)[\/\s]?([\w\.]+)/i],[[o,"UCBrowser"],a],[/(dolfin)\/([\w\.]+)/i],[[o,"Dolphin"],a],[/((?:android.+)crmo|crios)\/([\w\.]+)/i],[[o,"Chrome"],a],[/;fbav\/([\w\.]+);/i],[a,[o,"Facebook"]],[/fxios\/([\w\.-]+)/i],[a,[o,"Firefox"]],[/version\/([\w\.]+).+?mobile\/\w+\s(safari)/i],[a,[o,"Mobile Safari"]],[/version\/([\w\.]+).+?(mobile\s?safari|safari)/i],[a,o],[/webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i],[o,[a,l.str,c.browser.oldsafari.version]],[/(konqueror)\/([\w\.]+)/i,/(webkit|khtml)\/([\w\.]+)/i],[o,a],[/(navigator|netscape)\/([\w\.-]+)/i],[[o,"Netscape"],a],[/(swiftfox)/i,/(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,/(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i,/(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i,/(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,/(links)\s\(([\w\.]+)/i,/(gobrowser)\/?([\w\.]+)*/i,/(ice\s?browser)\/v?([\w\._]+)/i,/(mosaic)[\/\s]([\w\.]+)/i],[o,a]],cpu:[[/(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i],[["architecture","amd64"]],[/(ia32(?=;))/i],[["architecture",d.lowerize]],[/((?:i[346]|x)86)[;\)]/i],[["architecture","ia32"]],[/windows\s(ce|mobile);\sppc;/i],[["architecture","arm"]],[/((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i],[["architecture",/ower/,"",d.lowerize]],[/(sun4\w)[;\)]/i],[["architecture","sparc"]],[/((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i],[["architecture",d.lowerize]]],device:[[/\((ipad|playbook);[\w\s\);-]+(rim|apple)/i],[s,n,[r,w]],[/applecoremedia\/[\w\.]+ \((ipad)/],[s,[n,"Apple"],[r,w]],[/(apple\s{0,1}tv)/i],[[s,"Apple TV"],[n,"Apple"]],[/(archos)\s(gamepad2?)/i,/(hp).+(touchpad)/i,/(hp).+(tablet)/i,/(kindle)\/([\w\.]+)/i,/\s(nook)[\w\s]+build\/(\w+)/i,/(dell)\s(strea[kpr\s\d]*[\dko])/i],[n,s,[r,w]],[/(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i],[s,[n,"Amazon"],[r,w]],[/(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i],[[s,l.str,c.device.amazon.model],[n,"Amazon"],[r,t]],[/\((ip[honed|\s\w*]+);.+(apple)/i],[s,n,[r,t]],[/\((ip[honed|\s\w*]+);/i],[s,[n,"Apple"],[r,t]],[/(blackberry)[\s-]?(\w+)/i,/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|huawei|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i,/(hp)\s([\w\s]+\w)/i,/(asus)-?(\w+)/i],[n,s,[r,t]],[/\(bb10;\s(\w+)/i],[s,[n,"BlackBerry"],[r,t]],[/android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone)/i],[s,[n,"Asus"],[r,w]],[/(sony)\s(tablet\s[ps])\sbuild\//i,/(sony)?(?:sgp.+)\sbuild\//i],[[n,"Sony"],[s,"Xperia Tablet"],[r,w]],[/(?:sony)?(?:(?:(?:c|d)\d{4})|(?:so[-l].+))\sbuild\//i],[[n,"Sony"],[s,"Xperia Phone"],[r,t]],[/\s(ouya)\s/i,/(nintendo)\s([wids3u]+)/i],[n,s,[r,"console"]],[/android.+;\s(shield)\sbuild/i],[s,[n,"Nvidia"],[r,"console"]],[/(playstation\s[34portablevi]+)/i],[s,[n,"Sony"],[r,"console"]],[/(sprint\s(\w+))/i],[[n,l.str,c.device.sprint.vendor],[s,l.str,c.device.sprint.model],[r,t]],[/(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i],[n,s,[r,w]],[/(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i,/(zte)-(\w+)*/i,/(alcatel|geeksphone|huawei|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i],[n,[s,/_/g," "],[r,t]],[/(nexus\s9)/i],[s,[n,"HTC"],[r,w]],[/(nexus\s6p)/i],[s,[n,"Huawei"],[r,t]],[/(microsoft);\s(lumia[\s\w]+)/i],[n,s,[r,t]],[/[\s\(;](xbox(?:\sone)?)[\s\);]/i],[s,[n,"Microsoft"],[r,"console"]],[/(kin\.[onetw]{3})/i],[[s,/\./g," "],[n,"Microsoft"],[r,t]],[/\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i,/mot[\s-]?(\w+)*/i,/(XT\d{3,4}) build\//i,/(nexus\s6)/i],[s,[n,"Motorola"],[r,t]],[/android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i],[s,[n,"Motorola"],[r,w]],[/hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i],[[n,d.trim],[s,d.trim],[r,"smarttv"]],[/hbbtv.+maple;(\d+)/i],[[s,/^/,"SmartTV"],[n,"Samsung"],[r,"smarttv"]],[/\(dtv[\);].+(aquos)/i],[s,[n,"Sharp"],[r,"smarttv"]],[/android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,/((SM-T\w+))/i],[[n,"Samsung"],s,[r,w]],[/smart-tv.+(samsung)/i],[n,[r,"smarttv"],s],[/((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,/(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i,/sec-((sgh\w+))/i],[[n,"Samsung"],s,[r,t]],[/sie-(\w+)*/i],[s,[n,"Siemens"],[r,t]],[/(maemo|nokia).*(n900|lumia\s\d+)/i,/(nokia)[\s_-]?([\w-]+)*/i],[[n,"Nokia"],s,[r,t]],[/android\s3\.[\s\w;-]{10}(a\d{3})/i],[s,[n,"Acer"],[r,w]],[/android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i],[[n,"LG"],s,[r,w]],[/(lg) netcast\.tv/i],[n,s,[r,"smarttv"]],[/(nexus\s[45])/i,/lg[e;\s\/-]+(\w+)*/i],[s,[n,"LG"],[r,t]],[/android.+(ideatab[a-z0-9\-\s]+)/i],[s,[n,"Lenovo"],[r,w]],[/linux;.+((jolla));/i],[n,s,[r,t]],[/((pebble))app\/[\d\.]+\s/i],[n,s,[r,"wearable"]],[/android.+;\s(glass)\s\d/i],[s,[n,"Google"],[r,"wearable"]],[/android.+(\w+)\s+build\/hm\1/i,/android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i,/android.+(mi[\s\-_]*(?:one|one[\s_]plus|note lte)?[\s_]*(?:\d\w)?)\s+build/i],[[s,/_/g," "],[n,"Xiaomi"],[r,t]],[/android.+a000(1)\s+build/i],[s,[n,"OnePlus"],[r,t]],[/\s(tablet)[;\/]/i,/\s(mobile)(?:[;\/]|\ssafari)/i],[[r,d.lowerize],n,s]],engine:[[/windows.+\sedge\/([\w\.]+)/i],[a,[o,"EdgeHTML"]],[/(presto)\/([\w\.]+)/i,/(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i,/(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i,/(icab)[\/\s]([23]\.[\d\.]+)/i],[o,a],[/rv\:([\w\.]+).*(gecko)/i],[a,o]],os:[[/microsoft\s(windows)\s(vista|xp)/i],[o,a],[/(windows)\snt\s6\.2;\s(arm)/i,/(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s]+\w)*/i,/(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i],[o,[a,l.str,c.os.windows.version]],[/(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i],[[o,"Windows"],[a,l.str,c.os.windows.version]],[/\((bb)(10);/i],[[o,"BlackBerry"],a],[/(blackberry)\w*\/?([\w\.]+)*/i,/(tizen)[\/\s]([\w\.]+)/i,/(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i,/linux;.+(sailfish);/i],[o,a],[/(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i],[[o,"Symbian"],a],[/\((series40);/i],[o],[/mozilla.+\(mobile;.+gecko.+firefox/i],[[o,"Firefox OS"],a],[/(nintendo|playstation)\s([wids34portablevu]+)/i,/(mint)[\/\s\(]?(\w+)*/i,/(mageia|vectorlinux)[;\s]/i,/(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]+)*/i,/(hurd|linux)\s?([\w\.]+)*/i,/(gnu)\s?([\w\.]+)*/i],[o,a],[/(cros)\s[\w]+\s([\w\.]+\w)/i],[[o,"Chromium OS"],a],[/(sunos)\s?([\w\.]+\d)*/i],[[o,"Solaris"],a],[/\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i],[o,a],[/(haiku)\s(\w+)/i],[o,a],[/(ip[honead]+)(?:.*os\s([\w]+)*\slike\smac|;\sopera)/i],[[o,"iOS"],[a,/_/g,"."]],[/(mac\sos\sx)\s?([\w\s\.]+\w)*/i,/(macintosh|mac(?=_powerpc)\s)/i],[[o,"Mac OS"],[a,/_/g,"."]],[/((?:open)?solaris)[\/\s-]?([\w\.]+)*/i,/(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i,/(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i,/(unix)\s?([\w\.]+)*/i],[o,a]]},p=function(e,s){if(!(this instanceof p))return new p(e,s).getResult();var o=e||(i&&i.navigator&&i.navigator.userAgent?i.navigator.userAgent:""),r=s?d.extend(u,s):u;return this.getBrowser=function(){var i=l.rgx.apply(this,r.browser);return i.major=d.major(i.version),i},this.getCPU=function(){return l.rgx.apply(this,r.cpu)},this.getDevice=function(){return l.rgx.apply(this,r.device)},this.getEngine=function(){return l.rgx.apply(this,r.engine)},this.getOS=function(){return l.rgx.apply(this,r.os)},this.getResult=function(){return{ua:this.getUA(),browser:this.getBrowser(),engine:this.getEngine(),os:this.getOS(),device:this.getDevice(),cpu:this.getCPU()}},this.getUA=function(){return o},this.setUA=function(i){return o=i,this},this};p.VERSION="0.7.12",p.BROWSER={NAME:o,MAJOR:"major",VERSION:a},p.CPU={ARCHITECTURE:"architecture"},p.DEVICE={MODEL:s,VENDOR:n,TYPE:r,CONSOLE:"console",MOBILE:t,SMARTTV:"smarttv",TABLET:w,WEARABLE:"wearable",EMBEDDED:"embedded"},p.ENGINE={NAME:o,VERSION:a},p.OS={NAME:o,VERSION:a},"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=p),exports.UAParser=p):"function"==typeof define&&define.amd?define(function(){return p}):i.UAParser=p;var m=i.jQuery||i.Zepto;if(void 0!==m){var b=new p;m.ua=b.getResult(),m.ua.get=function(){return b.getUA()},m.ua.set=function(i){b.setUA(i);var e=b.getResult();for(var s in e)m.ua[s]=e[s]}}}("object"==typeof window?window:this);
-},{}],22:[function(require,module,exports){
-(function (global){
-var rng,crypto=global.crypto||global.msCrypto;if(crypto&&crypto.getRandomValues){var _rnds8=new Uint8Array(16);rng=function(){return crypto.getRandomValues(_rnds8),_rnds8}}if(!rng){var _rnds=new Array(16);rng=function(){for(var r,n=0;n<16;n++)0==(3&n)&&(r=4294967296*Math.random()),_rnds[n]=r>>>((3&n)<<3)&255;return _rnds}}module.exports=rng;
-}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-
-},{}],23:[function(require,module,exports){
-function parse(e,s,r){var t=s&&r||0,n=0;for(s=s||[],e.toLowerCase().replace(/[0-9a-f]{2}/g,function(e){n<16&&(s[t+n++]=_hexToByte[e])});n<16;)s[t+n++]=0;return s}function unparse(e,s){var r=s||0,t=_byteToHex;return t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]}function v1(e,s,r){var t=s&&r||0,n=s||[];e=e||{};var o=void 0!==e.clockseq?e.clockseq:_clockseq,a=void 0!==e.msecs?e.msecs:(new Date).getTime(),u=void 0!==e.nsecs?e.nsecs:_lastNSecs+1,c=a-_lastMSecs+(u-_lastNSecs)/1e4;if(c<0&&void 0===e.clockseq&&(o=o+1&16383),(c<0||a>_lastMSecs)&&void 0===e.nsecs&&(u=0),u>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");_lastMSecs=a,_lastNSecs=u,_clockseq=o,a+=122192928e5;var i=(1e4*(268435455&a)+u)%4294967296;n[t++]=i>>>24&255,n[t++]=i>>>16&255,n[t++]=i>>>8&255,n[t++]=255&i;var _=a/4294967296*1e4&268435455;n[t++]=_>>>8&255,n[t++]=255&_,n[t++]=_>>>24&15|16,n[t++]=_>>>16&255,n[t++]=o>>>8|128,n[t++]=255&o;for(var d=e.node||_nodeId,v=0;v<6;v++)n[t+v]=d[v];return s||unparse(n)}function v4(e,s,r){var t=s&&r||0;"string"==typeof e&&(s="binary"==e?new Array(16):null,e=null),e=e||{};var n=e.random||(e.rng||_rng)();if(n[6]=15&n[6]|64,n[8]=63&n[8]|128,s)for(var o=0;o<16;o++)s[t+o]=n[o];return s||unparse(n)}for(var _rng=require("./rng"),_byteToHex=[],_hexToByte={},i=0;i<256;i++)_byteToHex[i]=(i+256).toString(16).substr(1),_hexToByte[_byteToHex[i]]=i;var _seedBytes=_rng(),_nodeId=[1|_seedBytes[0],_seedBytes[1],_seedBytes[2],_seedBytes[3],_seedBytes[4],_seedBytes[5]],_clockseq=16383&(_seedBytes[6]<<8|_seedBytes[7]),_lastMSecs=0,_lastNSecs=0,uuid=v4;uuid.v1=v1,uuid.v4=v4,uuid.parse=parse,uuid.unparse=unparse,module.exports=uuid;
-},{"./rng":22}],24:[function(require,module,exports){
-function WildEmitter(){}module.exports=WildEmitter,WildEmitter.mixin=function(t){var l=t.prototype||t;l.isWildEmitter=!0,l.on=function(t,l,i){this.callbacks=this.callbacks||{};var s=3===arguments.length,c=s?arguments[1]:void 0,a=s?arguments[2]:arguments[1];return a._groupName=c,(this.callbacks[t]=this.callbacks[t]||[]).push(a),this},l.once=function(t,l,i){function s(){c.off(t,s),h.apply(this,arguments)}var c=this,a=3===arguments.length,e=a?arguments[1]:void 0,h=a?arguments[2]:arguments[1];return this.on(t,e,s),this},l.releaseGroup=function(t){this.callbacks=this.callbacks||{};var l,i,s,c;for(l in this.callbacks)for(c=this.callbacks[l],i=0,s=c.length;i<s;i++)c[i]._groupName===t&&(c.splice(i,1),i--,s--);return this},l.off=function(t,l){this.callbacks=this.callbacks||{};var i,s=this.callbacks[t];return s?1===arguments.length?(delete this.callbacks[t],this):(i=s.indexOf(l),s.splice(i,1),0===s.length&&delete this.callbacks[t],this):this},l.emit=function(t){this.callbacks=this.callbacks||{};var l,i,s,c=[].slice.call(arguments,1),a=this.callbacks[t],e=this.getWildcardCallbacks(t);if(a)for(s=a.slice(),l=0,i=s.length;l<i&&s[l];++l)s[l].apply(this,c);if(e)for(i=e.length,s=e.slice(),l=0,i=s.length;l<i&&s[l];++l)s[l].apply(this,[t].concat(c));return this},l.getWildcardCallbacks=function(t){this.callbacks=this.callbacks||{};var l,i,s=[];for(l in this.callbacks)i=l.split("*"),("*"===l||2===i.length&&t.slice(0,i[0].length)===i[0])&&(s=s.concat(this.callbacks[l]));return s}},WildEmitter.mixin(WildEmitter);
-},{}]},{},[2])(2)
-});
-
-
-//# sourceMappingURL=kurento-utils.map
\ No newline at end of file
diff --git a/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js b/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..3015099ac17bb3ec057e545e21a4db524265d74f
--- /dev/null
+++ b/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js
@@ -0,0 +1 @@
+!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a});
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as
index 1100f5c2ca28630f96da3e04c27b1259e39a16de..e22cb1a50273812f9b238b00b3a4ecf8c9f31914 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as
@@ -487,7 +487,7 @@ package org.bigbluebutton.modules.users.services
       logData.tags = ["users"];
       logData.status = "user_ejected";
       logData.message = "User ejected from meeting.";
-      LOGGER.info(JSON.stringify(logData));
+      LOGGER.debug(JSON.stringify(logData));
     }
     
     private function handleUserLocked(msg:Object):void {
@@ -698,21 +698,24 @@ package org.bigbluebutton.modules.users.services
     private function handleUserBroadcastCamStartedEvtMsg(msg:Object):void {
       var userId: String = msg.body.userId as String; 
       var streamId: String = msg.body.stream as String;
+      var isHtml5Client: Boolean = msg.body.isHtml5Client as Boolean;
       
       var logData:Object = UsersUtil.initLogData();
       logData.tags = ["webcam"];
       logData.message = "UserBroadcastCamStartedEvtMsg server message";
       logData.user.webcamStream = streamId;
+      logData.user.isHtml5Client = isHtml5Client;
       LOGGER.info(JSON.stringify(logData));
-      
-      var mediaStream: MediaStream = new MediaStream(streamId, userId)
-      LiveMeeting.inst().webcams.add(mediaStream);
-      
-      var webUser: User2x = UsersUtil.getUser(userId);
-      if (webUser != null) {
-        sendStreamStartedEvent(userId, webUser.name, streamId);
+
+      if (!isHtml5Client) {
+        var mediaStream: MediaStream = new MediaStream(streamId, userId)
+          LiveMeeting.inst().webcams.add(mediaStream);
+
+        var webUser: User2x = UsersUtil.getUser(userId);
+        if (webUser != null) {
+          sendStreamStartedEvent(userId, webUser.name, streamId);
+        }
       }
-      
     }
     
     private function sendStreamStartedEvent(userId: String, name: String, stream: String):void{
diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
index 8344297a488e8a26ce5370de565ce88f34071844..e29b01b158425285740c30daf634096ab0ae608f 100644
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -53,4 +53,9 @@
   <script src="/client/lib/jquery.json-2.4.min.js"></script>
   <script src="/client/lib/verto-min.js"></script>
   <script src="/client/lib/verto_extension.js"></script>
+  <script src="/client/lib/reconnecting-websocket.min.js"></script>
+  <script src="/client/lib/kurento-utils.js"></script>
+  <script src="/client/lib/kurento-extension.js"></script>
+  <script src="/html5client/js/adapter.js"></script>
+  <script src="/html5client/js/adjust-videos.js"></script>
 </body>
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js
index 5b953b2ea43f93c1add309adf8b5eb185d52ae17..2c6f548690c4ffbe20d8352b7ef5dab2392630e5 100644
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js
@@ -1,5 +1,7 @@
 import VertoBridge from './verto';
+import KurentoBridge from './kurento';
 
-const screenshareBridge = new VertoBridge();
+//const screenshareBridge = new VertoBridge();
+const screenshareBridge = new KurentoBridge();
 
 export default screenshareBridge;
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
new file mode 100755
index 0000000000000000000000000000000000000000..77e10ef49f17aa622bb35ba843f86e073fd885d6
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -0,0 +1,51 @@
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import BridgeService from './service';
+
+const CHROME_EXTENSION_KEY = Meteor.settings.public.kurento.chromeExtensionKey;
+
+const getUserId = () => {
+  const userID = Auth.userID;
+  return userID;
+}
+
+const getMeetingId = () => {
+  const meetingID = Auth.meetingID;
+  return meetingID;
+}
+
+const getUsername = () => {
+  return Users.findOne({ userId: getUserId() }).name;
+}
+
+export default class KurentoScreenshareBridge {
+  kurentoWatchVideo() {
+    window.kurentoWatchVideo(
+      'screenshareVideo',
+      BridgeService.getConferenceBridge(),
+      getUsername(),
+      getMeetingId(),
+      null,
+      null,
+    );
+  }
+
+  kurentoExitVideo() {
+    window.kurentoExitVideo();
+  }
+
+  kurentoShareScreen() {
+    window.kurentoShareScreen(
+      'screenshareVideo',
+      BridgeService.getConferenceBridge(),
+      getUsername(),
+      getMeetingId(),
+      null,
+      CHROME_EXTENSION_KEY,
+    );
+  }
+
+  kurentoExitScreenShare() {
+    window.kurentoExitScreenShare();
+  }
+}
diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js
index 92b24864c11fe4e244538a58fc0f594d61cdcbe8..aacc7d1eeab570c5cbd5169f0105164bccd2c99d 100644
--- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js
+++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js
@@ -1,7 +1,7 @@
 import { check } from 'meteor/check';
 import addScreenshare from '../modifiers/addScreenshare';
 
-export default function handleBroadcastStartedVoice({ body }, meetingId) {
+export default function handleScreenshareStarted({ body }, meetingId) {
   check(meetingId, String);
   check(body, Object);
 
diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js
index d0308ab5a312c3de18cc23175ce23e9237a92372..11e2871f0c7fb7ab0ad494fd8bb5336774b4fd55 100644
--- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js
+++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js
@@ -1,7 +1,7 @@
 import { check } from 'meteor/check';
 import clearScreenshare from '../modifiers/clearScreenshare';
 
-export default function handleBroadcastStartedVoice({ body }, meetingId) {
+export default function handleScreenshareStopped({ body }, meetingId) {
   const { screenshareConf } = body;
 
   check(meetingId, String);
diff --git a/bigbluebutton-html5/imports/api/video/server/eventHandlers.js b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e773b08eafe12ba8138ab8523b8a4c2e5196a82
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js
@@ -0,0 +1,6 @@
+import RedisPubSub from '/imports/startup/server/redis';
+import handleUserSharedHtml5Webcam from './handlers/userSharedHtml5Webcam';
+import handleUserUnsharedHtml5Webcam from './handlers/userUnsharedHtml5Webcam';
+
+RedisPubSub.on('UserBroadcastCamStartedEvtMsg', handleUserSharedHtml5Webcam);
+RedisPubSub.on('UserBroadcastCamStoppedEvtMsg', handleUserUnsharedHtml5Webcam);
diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..abf5466ffea602df604717d33c941c39c0b3316b
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js
@@ -0,0 +1,11 @@
+import sharedWebcam from '../modifiers/sharedWebcam';
+import {check} from 'meteor/check';
+
+export default function handleUserSharedHtml5Webcam({ header }, meetingId ) {
+  check(header, Object);
+  const { userId } = header;
+  check(meetingId, String);
+  check(userId, String);
+
+  return sharedWebcam(meetingId, userId);
+}
diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf6ba596e487375e198c671f8fe154f4e4da7c18
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js
@@ -0,0 +1,11 @@
+import unsharedWebcam from '../modifiers/unsharedWebcam';
+import { check } from 'meteor/check';
+
+export default function handleUserUnsharedHtml5Webcam({ header }, meetingId) {
+  check(header, Object);
+  const { userId } = header;
+  check(meetingId, String);
+  check(userId, String);
+
+  return unsharedWebcam(meetingId, userId);
+}
diff --git a/bigbluebutton-html5/imports/api/video/server/index.js b/bigbluebutton-html5/imports/api/video/server/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..9a7510e23ef5b6306a53e4b5743c7810976a3c18
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/index.js
@@ -0,0 +1,2 @@
+import './eventHandlers';
+import './methods';
diff --git a/bigbluebutton-html5/imports/api/video/server/methods.js b/bigbluebutton-html5/imports/api/video/server/methods.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f1dd46f1096c1479436e90cf776da4d57c1ac2f
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/methods.js
@@ -0,0 +1,7 @@
+import { Meteor } from 'meteor/meteor';
+import userShareWebcam from './methods/userShareWebcam';
+import userUnshareWebcam from './methods/userUnshareWebcam';
+
+Meteor.methods({
+  userShareWebcam, userUnshareWebcam,
+});
diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..9dbcabf1dde5f3dfb3833ae8d9244cca34b9c012
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import Logger from '/imports/startup/server/logger';
+import RedisPubSub from '/imports/startup/server/redis';
+
+export default function userShareWebcam(credentials, message) {
+  const REDIS_CONFIG = Meteor.settings.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'UserBroadcastCamStartMsg';
+
+  const { meetingId, requesterUserId, requesterToken } = credentials;
+
+  Logger.info(' user sharing webcam: ', credentials);
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(requesterToken, String);
+  // check(message, Object);
+
+  // const actionName = 'joinVideo';
+  /* TODO throw an error if user has no permission to share webcam
+  if (!isAllowedTo(actionName, credentials)) {
+    throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`);
+  } */
+
+  const payload = {
+    stream: message,
+    isHtml5Client: true,
+  };
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebec447808fa5c3e44ea168649cfdb74a0e4d8d4
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js
@@ -0,0 +1,32 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import Logger from '/imports/startup/server/logger';
+import RedisPubSub from '/imports/startup/server/redis';
+
+export default function userUnshareWebcam(credentials, message) {
+  const REDIS_CONFIG = Meteor.settings.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'UserBroadcastCamStopMsg';
+
+  const { meetingId, requesterUserId, requesterToken } = credentials;
+
+  Logger.info(' user unsharing webcam: ', credentials);
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(requesterToken, String);
+  // check(message, Object);
+
+  // const actionName = 'joinVideo';
+  /* TODO throw an error if user has no permission to share webcam
+  if (!isAllowedTo(actionName, credentials)) {
+    throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`);
+  } */
+
+  const payload = {
+    stream: message,
+    isHtml5Client: true,
+  };
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..749407ffc44860feb53b2389a2f68ce6d2e60bf5
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js
@@ -0,0 +1,33 @@
+import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
+import { check }  from 'meteor/check';
+
+export default function sharedWebcam(meetingId, userId) {
+  check(meetingId, String);
+  check(userId, String);
+
+  const selector = {
+    meetingId,
+    userId,
+  };
+
+  const modifier = {
+    $set: {
+      meetingId,
+      userId,
+      has_stream: true,
+    },
+  };
+
+  const cb = (err, numChanged) => {
+    if (err) {
+      return Logger.error(`Adding user to collection: ${err}`);
+    }
+
+    if (numChanged) {
+      return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`);
+    }
+  };
+
+  return Users.upsert(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ef8f0be36d552cd1610f739b30515f75308fac2
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js
@@ -0,0 +1,33 @@
+import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
+import { check } from 'meteor/check';
+
+export default function unsharedWebcam(meetingId, userId) {
+  check(meetingId, String);
+  check(userId, String);
+
+  const selector = {
+    meetingId,
+    userId,
+  };
+
+  const modifier = {
+    $set: {
+      meetingId,
+      userId,
+      has_stream: false,
+    },
+  };
+
+  const cb = (err, numChanged) => {
+    if (err) {
+      return Logger.error(`Adding user to collection: ${err}`);
+    }
+
+    if (numChanged) {
+      return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`);
+    }
+  };
+
+  return Users.upsert(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index d825b5dc704208a5eea0c51fc5d469ee351e829d..9970b8e74ed1e5815653a22f11cea19b57b16740 100644
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -83,7 +83,7 @@ Base.defaultProps = defaultProps;
 
 const SUBSCRIPTIONS_NAME = [
   'users', 'chat', 'cursor', 'meetings', 'polls', 'presentations', 'annotations',
-  'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user',
+  'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', 'screenshare',
 ];
 
 const BaseContainer = createContainer(({ params }) => {
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
index 97a7174b025cf7defe285639e811044edf78298e..8e15d2021a9bcffbfcc3d123d977f9b184c190a9 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
@@ -32,6 +32,22 @@ const intlMessages = defineMessages({
     id: 'app.actionsBar.actionsDropdown.presentationDesc',
     description: 'adds context to upload presentation option',
   },
+  desktopShareLabel: {
+    id: 'app.actionsBar.actionsDropdown.desktopShareLabel',
+    description: 'Desktop Share option label',
+  },
+  stopDesktopShareLabel: {
+    id: 'app.actionsBar.actionsDropdown.stopDesktopShareLabel',
+    description: 'Stop Desktop Share option label',
+  },
+  desktopShareDesc: {
+    id: 'app.actionsBar.actionsDropdown.desktopShareDesc',
+    description: 'adds context to desktop share option',
+  },
+  stopDesktopShareDesc: {
+    id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc',
+    description: 'adds context to stop desktop share option',
+  },
 });
 
 class ActionsDropdown extends Component {
@@ -53,7 +69,13 @@ class ActionsDropdown extends Component {
   }
 
   render() {
-    const { intl, isUserPresenter } = this.props;
+    const {
+      intl,
+      isUserPresenter,
+      handleShareScreen,
+      handleUnshareScreen,
+      isVideoBroadcasting,
+    } = this.props;
 
     if (!isUserPresenter) return null;
 
@@ -80,6 +102,18 @@ class ActionsDropdown extends Component {
               description={intl.formatMessage(intlMessages.presentationDesc)}
               onClick={this.handlePresentationClick}
             />
+           <DropdownListItem
+              icon="desktop"
+              label={intl.formatMessage(intlMessages.desktopShareLabel)}
+              description={intl.formatMessage(intlMessages.desktopShareDesc)}
+              onClick={handleShareScreen}
+            />
+            <DropdownListItem
+              icon="desktop"
+              label={intl.formatMessage(intlMessages.stopDesktopShareLabel)}
+              description={intl.formatMessage(intlMessages.stopDesktopShareDesc)}
+              onClick={handleUnshareScreen}
+            />
           </DropdownList>
         </DropdownContent>
       </Dropdown>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 74482af8407f3fe4d6d71c3eb91d014b6a897779..f671845580c4b104a657ac7703250d265a14d81b 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -3,20 +3,31 @@ import styles from './styles.scss';
 import EmojiSelect from './emoji-select/component';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
+import JoinVideoOptionsContainer from '../video-dock/video-menu/container';
 
 const ActionsBar = ({
   isUserPresenter,
+  handleExitVideo,
+  handleJoinVideo,
+  handleShareScreen,
+  handleUnshareScreen,
+  isVideoBroadcasting,
   emojiList,
   emojiSelected,
   handleEmojiChange,
 }) => (
   <div className={styles.actionsbar}>
     <div className={styles.left}>
-      <ActionsDropdown {...{ isUserPresenter }} />
+      <ActionsDropdown {...{ isUserPresenter, handleShareScreen, handleUnshareScreen, isVideoBroadcasting}} />
     </div>
     <div className={styles.center}>
       <AudioControlsContainer />
-      {/* <JoinVideo /> */}
+      {Meteor.settings.public.kurento.enableVideo ?
+        <JoinVideoOptionsContainer
+          handleJoinVideo={handleJoinVideo}
+          handleCloseVideo={handleExitVideo}
+        />
+      : null}
       <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} />
     </div>
   </div>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index 943d138689e941a85ebf25c44ffb0dffbb5f31a6..7465303e68711b11b76a5b611aac41c4a70260d3 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -2,6 +2,8 @@ import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
 import ActionsBar from './component';
 import Service from './service';
+import VideoService from '../video-dock/service';
+import ScreenshareService from '../screenshare/service';
 
 const ActionsBarContainer = props => <ActionsBar {...props} />;
 
@@ -10,4 +12,10 @@ export default createContainer(() => ({
   emojiList: Service.getEmojiList(),
   emojiSelected: Service.getEmoji(),
   handleEmojiChange: Service.setEmoji,
+  handleExitVideo: () => VideoService.exitVideo(),
+  handleJoinVideo: () => VideoService.joinVideo(),
+  handleShareScreen: () =>  ScreenshareService.shareScreen(),
+  handleUnshareScreen: () => ScreenshareService.unshareScreen(),
+  isVideoBroadcasting: () => ScreenshareService.isVideoBroadcasting(),
+
 }), ActionsBarContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx
index de9221e18afa228ef46256da32d02c13a5e6e2f3..119567f8a137dc3093fd980b0a9d0859398276b5 100644
--- a/bigbluebutton-html5/imports/ui/components/media/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx
@@ -8,7 +8,7 @@ import ScreenshareContainer from '../screenshare/container';
 import DefaultContent from '../presentation/default-content/component';
 
 const defaultProps = {
-  overlay: null, // <VideoDockContainer/>,
+  overlay: <VideoDockContainer />,
   content: <PresentationAreaContainer />,
   defaultContent: <DefaultContent />,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/media/service.js b/bigbluebutton-html5/imports/ui/components/media/service.js
index d583c089f19be3a4c79ea3e9d21b9f3f1164ad9b..9523f64779f3dc4eee14ac7f0f138856d0384a1d 100644
--- a/bigbluebutton-html5/imports/ui/components/media/service.js
+++ b/bigbluebutton-html5/imports/ui/components/media/service.js
@@ -17,11 +17,11 @@ function shouldShowWhiteboard() {
 }
 
 function shouldShowScreenshare() {
-  return isVideoBroadcasting();
+  return isVideoBroadcasting() && Meteor.settings.public.kurento.enableScreensharing;
 }
 
 function shouldShowOverlay() {
-  return false;
+  return Meteor.settings.public.kurento.enableVideo;
 }
 
 export default {
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
index 093de370e6ef54ab0be0c1ed601359aa9f67c039..c381280101b799860a9807c0cabff1e0c31199fe 100644
--- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
@@ -7,7 +7,7 @@ export default class ScreenshareComponent extends React.Component {
 
   render() {
     return (
-      <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} />
+      <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} autoPlay playsInline />
     );
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
index cdb7ecc5ef638abb9c47709b390b6e2b68bab139..33470b2ecd571fe97d01092772178b9240ecaf88 100644
--- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
@@ -1,33 +1,46 @@
 import Screenshare from '/imports/api/screenshare';
 import VertoBridge from '/imports/api/screenshare/client/bridge';
+import KurentoBridge from '/imports/api/screenshare/client/bridge';
 import PresentationService from '/imports/ui/components/presentation/service';
 
 // when the meeting information has been updated check to see if it was
 // screensharing. If it has changed either trigger a call to receive video
 // and display it, or end the call and hide the video
-function isVideoBroadcasting() {
+const isVideoBroadcasting = () => {
   const ds = Screenshare.findOne({});
 
   if (!ds) {
     return false;
   }
-  return ds.screenshare.stream && !PresentationService.isPresenter();
+
+  // TODO commented out isPresenter to enable screen viewing to the presenter
+  return ds.screenshare.stream; // && !PresentationService.isPresenter();
 }
 
 // if remote screenshare has been ended disconnect and hide the video stream
-function presenterScreenshareHasEnded() {
-  // references a function in the global namespace inside verto_extension.js
+const presenterScreenshareHasEnded = () => {
+  // references a function in the global namespace inside kurento-extension.js
   // that we load dynamically
-  VertoBridge.vertoExitVideo();
+  KurentoBridge.kurentoExitVideo();
 }
 
 // if remote screenshare has been started connect and display the video stream
-function presenterScreenshareHasStarted() {
-  // references a function in the global namespace inside verto_extension.js
+const presenterScreenshareHasStarted = () => {
+  // references a function in the global namespace inside kurento-extension.js
   // that we load dynamically
-  VertoBridge.vertoWatchVideo();
+  //VertoBridge.vertoWatchVideo();
+  KurentoBridge.kurentoWatchVideo();
+}
+
+const shareScreen = () => {
+  KurentoBridge.kurentoShareScreen();
+}
+
+const unshareScreen = () => {
+  console.log("Exiting screenshare");
+  KurentoBridge.kurentoExitScreenShare();
 }
 
 export {
-  isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted,
+  isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted, shareScreen, unshareScreen,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
index fcd9e12dbd8bff5bd7965e6b0cc626e36b7bd76c..107fc4785b2ed9dff874629097297eaf3d094887 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
@@ -1,11 +1,504 @@
-import React from 'react';
+import React, { Component } from 'react';
 import ScreenshareContainer from '/imports/ui/components/screenshare/container';
 import styles from './styles';
+import { log } from '/imports/ui/services/api';
 
-const VideoDock = () => (
-  <div className={styles.videoDock}>
-    <ScreenshareContainer />
-  </div>
-);
 
-export default VideoDock;
+class VideoElement extends Component {
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    return <video id={`video-elem-${this.props.videoId}`} width={320} height={240} autoPlay={true} playsInline={true} />;
+  }
+
+  componentDidMount() {
+    this.props.onMount(this.props.videoId, false);
+  }
+}
+
+export default class VideoDock extends Component {
+  constructor(props) {
+    super(props);
+
+    // Set a valid bbb-webrtc-sfu application server socket in the settings
+    this.ws = new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl);
+    this.wsQueue = [];
+    this.webRtcPeers = {};
+    this.reconnectWebcam = false;
+    this.reconnectList = false;
+    this.sharedCameraTimeout = null;
+    this.subscribedCamerasTimeouts = [];
+
+    this.state = {
+      videos: {},
+      sharedWebcam : false,
+    };
+
+    this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this);
+    this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this);
+
+    this.unshareWebcam = this.unshareWebcam.bind(this);
+    this.shareWebcam = this.shareWebcam.bind(this);
+
+    this.onWsOpen = this.onWsOpen.bind(this);
+    this.onWsClose = this.onWsClose.bind(this);
+    this.onWsMessage = this.onWsMessage.bind(this);
+  }
+
+  setupReconnectVideos() {
+    for (id in this.webRtcPeers) {
+      this.disconnected(id);
+      this.stop(id);
+    }
+  }
+
+  reconnectVideos() {
+    for (i in this.reconnectList) {
+      const id = this.reconnectList[i];
+
+      // TODO: base this on BBB API users instead of using memory
+      if (id != this.myId) {
+        setTimeout(() => {
+          log('debug', ` [camera] Trying to reconnect camera ${id}`);
+          this.start(id, false);
+        }, 5000);
+      }
+    }
+
+    if (this.reconnectWebcam) {
+      log('debug', ` [camera] Trying to re-share ${this.myId} after reconnect.`);
+      this.start(this.myId, true);
+    }
+
+    this.reconnectWebcam = false;
+    this.reconnectList = [];
+  }
+
+  componentDidMount() {
+    const ws = this.ws;
+    const { users } = this.props;
+    const id = users[0].userId;
+
+    for (let i = 0; i < users.length; i++) {
+      if (users[i].has_stream && users[i].userId !== id) {
+        this.start(users[i].userId, false);
+      }
+    }
+
+    document.addEventListener('joinVideo', this.shareWebcam.bind(this));// TODO find a better way to do this
+    document.addEventListener('exitVideo', this.unshareWebcam.bind(this));
+
+    window.addEventListener('resize', this.adjustVideos);
+
+    ws.addEventListener('message', this.onWsMessage);
+  }
+
+  componentWillMount () {
+    this.ws.addEventListener('open', this.onWsOpen);
+    this.ws.addEventListener('close', this.onWsClose);
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('joinVideo', this.shareWebcam);
+    document.removeEventListener('exitVideo', this.shareWebcam);
+    window.removeEventListener('resize', this.adjustVideos);
+
+    this.ws.removeEventListener('message', this.onWsMessage);
+    this.ws.removeEventListener('open', this.onWsOpen);
+    this.ws.removeEventListener('close', this.onWsClose);
+    // Close websocket connection to prevent multiple reconnects from happening
+    this.ws.close();
+  }
+
+  adjustVideos () {
+    window.adjustVideos('webcamArea', true);
+  }
+
+  onWsOpen () {
+    log('debug', '------ Websocket connection opened.');
+
+    // -- Resend queued messages that happened when socket was not connected
+    while (this.wsQueue.length > 0) {
+      this.sendMessage(this.wsQueue.pop());
+    }
+
+    this.reconnectVideos();
+  }
+
+  onWsClose (error) {
+    log('debug', '------ Websocket connection closed.');
+
+    this.setupReconnectVideos();
+  }
+
+  onWsMessage (msg) {
+    const parsedMessage = JSON.parse(msg.data);
+
+    console.log('Received message new ws message: ');
+    console.log(parsedMessage);
+
+    switch (parsedMessage.id) {
+
+      case 'startResponse':
+        this.startResponse(parsedMessage);
+        break;
+
+      case 'error':
+        this.handleError(parsedMessage);
+        break;
+
+      case 'playStart':
+        this.handlePlayStart(parsedMessage);
+        break;
+
+      case 'playStop':
+        this.handlePlayStop(parsedMessage);
+
+        break;
+
+      case 'iceCandidate':
+
+        const webRtcPeer = this.webRtcPeers[parsedMessage.cameraId];
+
+        if (webRtcPeer !== null) {
+          if (webRtcPeer.didSDPAnswered) {
+            webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => {
+              if (err) {
+                return log('error', `Error adding candidate: ${err}`);
+              }
+            });
+          } else {
+            webRtcPeer.iceQueue.push(parsedMessage.candidate);
+          }
+        } else {
+          log('error', ' [ICE] Message arrived before webRtcPeer?');
+        }
+        break;
+    }
+  };
+
+  start(id, shareWebcam) {
+    const that = this;
+
+    console.log(`Starting video call for video: ${id} with ${shareWebcam}`);
+
+    if (shareWebcam) {
+      this.setState({sharedWebcam: true});
+      this.initWebRTC(id, true);
+    } else {
+      // initWebRTC with shareWebcam false will be called after react mounts the element
+      this.createVideoTag(id);
+    }
+  }
+
+  initWebRTC(id, shareWebcam) {
+    let that = this;
+
+    const onIceCandidate = function (candidate) {
+      const message = {
+        type: 'video',
+        role: shareWebcam ? 'share' : 'viewer',
+        id: 'onIceCandidate',
+        candidate,
+        cameraId: id,
+      };
+      that.sendMessage(message);
+    };
+
+    let videoConstraints = {};
+    if (!!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) { // Custom constraints for Safari
+      videoConstraints = {
+        width: {min:320, max:640},
+        height: {min:240, max:480}
+      }
+    } else {
+      videoConstraints = {
+        width: {min: 320, ideal: 320},
+        height: {min: 240, ideal:240},
+        frameRate: {min: 5, ideal: 10}
+      };
+    }
+
+    let options = {
+      mediaConstraints: {
+        audio: false,
+        video: videoConstraints
+      },
+      onicecandidate: onIceCandidate,
+    };
+
+    let peerObj;
+    if (shareWebcam) {
+      options.localVideo = this.refs.videoInput;
+      peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
+    } else {
+      peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
+      options.remoteVideo = document.getElementById(`video-elem-${id}`);
+    }
+
+    let webRtcPeer = new peerObj(options, function (error) {
+      if (error) {
+        log('error', ' WebRTC peerObj create error');
+
+        that.destroyWebRTCPeer(id);
+        that.destroyVideoTag(id);
+
+        return log('error', error);
+      }
+
+      this.didSDPAnswered = false;
+      this.iceQueue = [];
+
+      that.webRtcPeers[id] = webRtcPeer;
+      if (shareWebcam) {
+        that.sharedWebcam = webRtcPeer;
+        that.myId = id;
+      }
+
+      this.generateOffer((error, offerSdp) => {
+        if (error) {
+          log('error', ' WebRtc generate offer error');
+
+          that.destroyWebRTCPeer(id);
+          that.destroyVideoTag(id);
+
+          return log('error', error);
+        }
+
+        console.log(`Invoking SDP offer callback function ${location.host}`);
+        const message = {
+          type: 'video',
+          role: shareWebcam ? 'share' : 'viewer',
+          id: 'start',
+          sdpOffer: offerSdp,
+          cameraId: id,
+        };
+        that.sendMessage(message);
+      });
+      while (this.iceQueue.length) {
+        let candidate = this.iceQueue.shift();
+        this.addIceCandidate(candidate, (err) => {
+          if (err) {
+            return console.error(`Error adding candidate: ${err}`);
+          }
+        });
+      }
+      this.didSDPAnswered = true;
+    });
+  }
+
+  disconnected(id) {
+    if (this.sharedWebcam) {
+      log('debug', ' [camera] Webcam disconnected, will try re-share webcam later.');
+      this.reconnectWebcam = true;
+    } else {
+      this.reconnectList.push(id);
+
+      log('debug', ` [camera] ${id} disconnected, will try re-subscribe later.`);
+    }
+  }
+
+  stop(id) {
+    const { users } = this.props;
+    this.sendMessage({
+      type: 'video',
+      role: id == users[0].userId ? 'share' : 'viewer',
+      id: 'stop',
+      cameraId: id,
+    });
+
+    this.destroyWebRTCPeer(id);
+    this.destroyVideoTag(id);
+  }
+
+  createVideoTag(id) {
+    let videos = this.state.videos;
+
+    videos[id] = true;
+    this.setState({videos: videos})
+  }
+
+  destroyVideoTag(id) {
+    let videos = this.state.videos;
+
+    delete videos[id];
+    this.setState({videos: videos});
+
+    if (id == this.myId) {
+      this.setState({sharedWebcam: false});
+    }
+  }
+
+  destroyWebRTCPeer(id) {
+    const webRtcPeer = this.webRtcPeers[id];
+
+    if (webRtcPeer) {
+      log('info', 'Stopping WebRTC peer');
+
+      if (id == this.myId && this.sharedWebcam) {
+        this.sharedWebcam.dispose();
+        this.sharedWebcam = null;
+      }
+
+      webRtcPeer.dispose();
+      delete this.webRtcPeers[id];
+    } else {
+      log('info', 'No WebRTC peer to stop (not an error)');
+    }
+  }
+
+  shareWebcam() {
+    const { users } = this.props;
+    const id = users[0].userId;
+
+    if (this.connectedToMediaServer()) {
+      this.start(id, true);
+    } else {
+      log("error", "Not connected to media server BRA");
+    }
+  }
+
+  unshareWebcam() {
+    log('info', 'Unsharing webcam');
+    const { users } = this.props;
+    const id = users[0].userId;
+    this.sendUserUnshareWebcam(id);
+  }
+
+  startResponse(message) {
+    const id = message.cameraId;
+    const webRtcPeer = this.webRtcPeers[id];
+
+    if (message.sdpAnswer == null) {
+      return log('debug', 'Null sdp answer. Camera unplugged?');
+    }
+
+    if (webRtcPeer == null) {
+      return log('debug', 'Null webrtc peer ????');
+    }
+
+    log('info', 'SDP answer received from server. Processing ...');
+
+    webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
+      if (error) {
+        return log('error', error);
+      }
+    });
+
+    this.sendUserShareWebcam(id);
+  }
+
+  sendMessage(message) {
+    const ws = this.ws;
+
+    if (this.connectedToMediaServer()) {
+      const jsonMessage = JSON.stringify(message);
+      console.log(`Sending message: ${jsonMessage}`);
+      ws.send(jsonMessage, (error) => {
+        if (error) {
+          console.error(`client: Websocket error "${error}" on message "${jsonMessage.id}"`);
+        }
+      });
+    } else {
+      // No need to queue video stop messages
+      if (message.id != 'stop') {
+        this.wsQueue.push(message);
+      }
+    }
+  }
+
+  connectedToMediaServer() {
+    return this.ws.readyState === WebSocket.OPEN;
+  }
+
+  connectionStatus() {
+    return this.ws.readyState;
+  }
+
+  handlePlayStop(message) {
+    log('info', 'Handle play stop <--------------------');
+    log('error', message);
+
+    const { users } = this.props;
+
+    if (message.cameraId == users[0].userId) {
+      this.unshareWebcam();
+    } else {
+      this.stop(message.cameraId);
+    }
+  }
+
+  handlePlayStart(message) {
+    log('info', 'Handle play start <===================');
+  }
+
+  handleError(message) {
+    console.error(' Handle error --------------------->');
+    log('debug', message.message);
+  }
+
+  componentDidUpdate() {
+    this.adjustVideos();
+  }
+
+  render() {
+    let cssClass;
+    if (this.state.sharedWebcam) {
+      cssClass = styles.sharedWebcamVideoLocal;
+    }
+    else {
+      cssClass = styles.sharedWebcamVideo;
+    }
+
+    return (
+
+      <div className={styles.videoDock}>
+        <div id="webcamArea">
+          {Object.keys(this.state.videos).map((id) => {
+            return (<VideoElement videoId={id} key={id} onMount={this.initWebRTC.bind(this)} />);
+          })}
+          <video autoPlay={true} playsInline={true} muted={true} id="shareWebcamVideo" className={cssClass} ref="videoInput" />
+        </div>
+      </div>
+    );
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    const { users } = this.props;
+    const nextUsers = nextProps.users;
+    const id = users[0].userId;
+
+    if (users) {
+      let suc = false;
+
+      for (let i = 0; i < users.length; i++) {
+        if (users && users[i] &&
+              nextUsers && nextUsers[i]) {
+          if (users[i].has_stream !== nextUsers[i].has_stream) {
+            console.log(`User ${nextUsers[i].has_stream ? '' : 'un'}shared webcam ${users[i].userId}`);
+
+            if (nextUsers[i].has_stream) {
+              if (id !== users[i].userId) {
+                this.start(users[i].userId, false);
+              }
+            } else {
+              this.stop(users[i].userId);
+            }
+
+            if (!nextUsers[i].has_stream) {
+              this.destroyVideoTag(users[i].userId);
+            }
+
+            suc = suc || true;
+          }
+        }
+      }
+
+      return true;
+    }
+
+    return false;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
index ad18e501e980831cb554c5070b75b55c51a12f16..a92bbb3d0989192cca4b2840832df304611843d5 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
@@ -1,15 +1,25 @@
-import React from 'react';
+import React, { Component } from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
 
 import VideoDock from './component';
+import VideoService from './service';
 
-const VideoDockContainer = props => (
-  <VideoDock>
-    {props.children}
-  </VideoDock>
-);
+class VideoDockContainer extends Component {
+  constructor(props) {
+    super(props);
+  }
 
-export default createContainer(() => {
-  const data = {};
-  return data;
-}, VideoDockContainer);
+  render() {
+    return (
+      <VideoDock {...this.props}>
+        {this.props.children}
+      </VideoDock>
+    );
+  }
+}
+
+export default createContainer(() => ({
+  sendUserShareWebcam: VideoService.sendUserShareWebcam,
+  sendUserUnshareWebcam: VideoService.sendUserUnshareWebcam,
+  users: VideoService.getAllUsers(),
+}), VideoDockContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/service.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d778fca8f2385b81697876dc7773327f94bcbaa
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/service.js
@@ -0,0 +1,29 @@
+import { makeCall } from '/imports/ui/services/api';
+import Users from '/imports/api/users';
+import { createContainer } from 'meteor/react-meteor-data';
+
+const joinVideo = () => {
+  var joinVideoEvent = new Event('joinVideo');
+  document.dispatchEvent(joinVideoEvent);
+}
+
+const exitVideo = () => {
+  var exitVideoEvent = new Event('exitVideo');
+  document.dispatchEvent(exitVideoEvent);
+}
+
+const sendUserShareWebcam = (stream) => {
+  makeCall('userShareWebcam', stream);
+};
+
+const sendUserUnshareWebcam = (stream) => {
+  makeCall('userUnshareWebcam', stream);
+};
+
+const getAllUsers = () => {
+  return Users.find().fetch();
+}
+
+export default {
+  sendUserShareWebcam, sendUserUnshareWebcam, joinVideo, exitVideo, getAllUsers,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
index a37e967d4ce74c1e91f784b5d0fac052d3fd6009..2dce62f218a11fd0b3ffbc6fc7e9c561bb4a6c37 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
@@ -7,9 +7,16 @@
   bottom: 0;
   left: 0;
 
-  background-image: url(https://avatars.slack-edge.com/2016-01-04/17715243383_99a961f4cb2bf2cde5c4_512.jpg);
   background-size: cover;
   background-position: center;
   box-shadow: 0 0 5px rgba(0, 0, 0, .5);
   border-radius: .2rem;
 }
+
+.sharedWebcamVideo {
+  display: none;
+}
+
+.sharedWebcamVideoLocal {
+  display: normal;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..7d08e37f35c430269cb66b01fad4bf237c458ca6
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const intlMessages = defineMessages({
+  joinVideo: {
+    id: 'app.video.joinVideo',
+    description: 'Join video button label',
+  },
+  leaveVideo: {
+    id: 'app.video.leaveVideo',
+    description: 'Leave video button label',
+  },
+});
+
+class JoinVideoOptions extends React.Component {
+  render() {
+    const {
+      intl,
+      isSharingVideo,
+      handleJoinVideo,
+      handleCloseVideo,
+    } = this.props;
+
+    if (isSharingVideo) {
+        return (
+          <Button
+            onClick={handleCloseVideo}
+            label={intl.formatMessage(intlMessages.leaveVideo)}
+            hideLabel
+            aria-label={intl.formatMessage(intlMessages.leaveVideo)}
+            color={'danger'}
+            icon={'video'}
+            size={'lg'}
+            circle
+          />
+        );
+    }
+
+    return (
+      <Button
+        onClick={handleJoinVideo}
+        label={intl.formatMessage(intlMessages.joinVideo)}
+        hideLabel
+        aria-label={intl.formatMessage(intlMessages.joinVideo)}
+        color={'primary'}
+        icon={'video_off'}
+        size={'lg'}
+        circle
+      />
+    );
+  }
+}
+
+export default injectIntl(JoinVideoOptions);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..da7d5517f5147408c91fba55207818fc666b7423
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import JoinVideoOptions from './component';
+import VideoMenuService from './service';
+
+const JoinVideoOptionsContainer = props => (<JoinVideoOptions {...props} />);
+
+export default createContainer((params) => {
+  const isSharingVideo = VideoMenuService.isSharingVideo();
+  return {
+    isSharingVideo,
+    handleJoinVideo: params.handleJoinVideo,
+    handleCloseVideo: params.handleCloseVideo,
+  };
+}, JoinVideoOptionsContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js
new file mode 100644
index 0000000000000000000000000000000000000000..92a58cd21afc50dac0196a3d19d325fea668e125
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js
@@ -0,0 +1,12 @@
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth/index';
+
+const isSharingVideo = () => {
+  const userId = Auth.userID;
+  const user = Users.findOne({ userId: userId });
+  return user.has_stream ? true : false;
+};
+
+export default {
+  isSharingVideo
+};
diff --git a/bigbluebutton-html5/private/config/development/public/kurento.yaml b/bigbluebutton-html5/private/config/development/public/kurento.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3a2e239be80123772738cea177f5bda6b7fd79e7
--- /dev/null
+++ b/bigbluebutton-html5/private/config/development/public/kurento.yaml
@@ -0,0 +1,6 @@
+kurento:
+  wsUrl: 'HOST'
+  chromeExtensionKey: 'KEY'
+  chromeExtensionLink: 'LINK'
+  enableScreensharing: false
+  enableVideo: false
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 073bc37727d99671911557d1a7b06431c26d0242..728ec78ae34f2731b4b648c3b788838be27be730 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -173,9 +173,11 @@
     "app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation",
     "app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll",
     "app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen",
+    "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Stop sharing your screen",
     "app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation",
     "app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll",
     "app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others",
+    "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with",
     "app.actionsBar.emojiMenu.statusTriggerLabel": "Status",
     "app.actionsBar.emojiMenu.awayLabel": "Away",
     "app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
@@ -244,6 +246,8 @@
     "app.audioManager.mediaError": "Error: There was an issue getting your media devices",
     "app.audio.joinAudio": "Join Audio",
     "app.audio.leaveAudio": "Leave Audio",
+    "app.video.joinVideo": "Share Webcam",
+    "app.video.leaveVideo": "Un-share Webcam",
     "app.audio.enterSessionLabel": "Enter Session",
     "app.audio.playSoundLabel": "Play Sound",
     "app.audio.backLabel": "Back",
@@ -269,5 +273,7 @@
     "app.toast.chat.singular":"you have {0} new message in {1}",
     "app.toast.chat.plural":"you have {0} new messages in {1}",
     "app.notification.recordingStart": "This session is now being recorded",
-    "app.notification.recordingStop": "This session is not being recorded anymore"
+    "app.notification.recordingStop": "This session is not being recorded anymore",
+    "app.video.joinVideo": "Share webcam",
+    "app.video.leaveVideo": "Unshare webcam"
 }
diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json
index 0cb562cf2bfb75eacb809fea852dedb35f4a4d0a..00caa32a14f37e6fdedab57ad5ee5e5c0e5e5618 100644
--- a/bigbluebutton-html5/private/locales/pt_BR.json
+++ b/bigbluebutton-html5/private/locales/pt_BR.json
@@ -204,6 +204,8 @@
     "app.audioModal.closeLabel": "Fechar",
     "app.audio.joinAudio": "Ativar áudio",
     "app.audio.leaveAudio": "Desativar áudio",
+    "app.video.joinVideo": "Compartilhar câmera",
+    "app.video.leaveVideo": "Des-compartilhar câmera",
     "app.audio.enterSessionLabel": "Entrar na reunião",
     "app.audio.playSoundLabel": "Tocar som de teste",
     "app.audio.backLabel": "Voltar",
diff --git a/bigbluebutton-html5/public/js/adapter.js b/bigbluebutton-html5/public/js/adapter.js
new file mode 100644
index 0000000000000000000000000000000000000000..fc1d8c8ad9bf5df2141e314a6fcc09f01b627db4
--- /dev/null
+++ b/bigbluebutton-html5/public/js/adapter.js
@@ -0,0 +1,4450 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+
+function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : dtlsRole || 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+}
+
+// Edge does not like
+// 1) stun: filtered after 14393 unless ?transport=udp is present
+// 2) turn: that does not have all of turn:host:port?transport=udp
+// 3) turn: with ipv6 addresses
+// 4) turn: occurring muliple times
+function filterIceServers(iceServers, edgeVersion) {
+  var hasTurn = false;
+  iceServers = JSON.parse(JSON.stringify(iceServers));
+  return iceServers.filter(function(server) {
+    if (server && (server.urls || server.url)) {
+      var urls = server.urls || server.url;
+      if (server.url && !server.urls) {
+        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+      }
+      var isString = typeof urls === 'string';
+      if (isString) {
+        urls = [urls];
+      }
+      urls = urls.filter(function(url) {
+        var validTurn = url.indexOf('turn:') === 0 &&
+            url.indexOf('transport=udp') !== -1 &&
+            url.indexOf('turn:[') === -1 &&
+            !hasTurn;
+
+        if (validTurn) {
+          hasTurn = true;
+          return true;
+        }
+        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
+            url.indexOf('?transport=udp') === -1;
+      });
+
+      delete server.url;
+      server.urls = isString ? urls[0] : urls;
+      return !!urls.length;
+    }
+    return false;
+  });
+}
+
+// Determines the intersection of local and remote capabilities.
+function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+  var commonCapabilities = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: []
+  };
+
+  var findCodecByPayloadType = function(pt, codecs) {
+    pt = parseInt(pt, 10);
+    for (var i = 0; i < codecs.length; i++) {
+      if (codecs[i].payloadType === pt ||
+          codecs[i].preferredPayloadType === pt) {
+        return codecs[i];
+      }
+    }
+  };
+
+  var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+    var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+    var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+    return lCodec && rCodec &&
+        lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+  };
+
+  localCapabilities.codecs.forEach(function(lCodec) {
+    for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+      var rCodec = remoteCapabilities.codecs[i];
+      if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+          lCodec.clockRate === rCodec.clockRate) {
+        if (lCodec.name.toLowerCase() === 'rtx' &&
+            lCodec.parameters && rCodec.parameters.apt) {
+          // for RTX we need to find the local rtx that has a apt
+          // which points to the same local codec as the remote one.
+          if (!rtxCapabilityMatches(lCodec, rCodec,
+              localCapabilities.codecs, remoteCapabilities.codecs)) {
+            continue;
+          }
+        }
+        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+        // number of channels is the highest common number of channels
+        rCodec.numChannels = Math.min(lCodec.numChannels,
+            rCodec.numChannels);
+        // push rCodec so we reply with offerer payload type
+        commonCapabilities.codecs.push(rCodec);
+
+        // determine common feedback mechanisms
+        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+          for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+            if (lCodec.rtcpFeedback[j].type === fb.type &&
+                lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+              return true;
+            }
+          }
+          return false;
+        });
+        // FIXME: also need to determine .parameters
+        //  see https://github.com/openpeer/ortc/issues/569
+        break;
+      }
+    }
+  });
+
+  localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+    for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+         i++) {
+      var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+      if (lHeaderExtension.uri === rHeaderExtension.uri) {
+        commonCapabilities.headerExtensions.push(rHeaderExtension);
+        break;
+      }
+    }
+  });
+
+  // FIXME: fecMechanisms
+  return commonCapabilities;
+}
+
+// is action=setLocalDescription with type allowed in signalingState
+function isActionAllowedInSignalingState(action, type, signalingState) {
+  return {
+    offer: {
+      setLocalDescription: ['stable', 'have-local-offer'],
+      setRemoteDescription: ['stable', 'have-remote-offer']
+    },
+    answer: {
+      setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+      setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+    }
+  }[type][action].indexOf(signalingState) !== -1;
+}
+
+function maybeAddCandidate(iceTransport, candidate) {
+  // Edge's internal representation adds some fields therefore
+  // not all fieldѕ are taken into account.
+  var alreadyAdded = iceTransport.getRemoteCandidates()
+      .find(function(remoteCandidate) {
+        return candidate.foundation === remoteCandidate.foundation &&
+            candidate.ip === remoteCandidate.ip &&
+            candidate.port === remoteCandidate.port &&
+            candidate.priority === remoteCandidate.priority &&
+            candidate.protocol === remoteCandidate.protocol &&
+            candidate.type === remoteCandidate.type;
+      });
+  if (!alreadyAdded) {
+    iceTransport.addRemoteCandidate(candidate);
+  }
+  return !alreadyAdded;
+}
+
+module.exports = function(window, edgeVersion) {
+  var RTCPeerConnection = function(config) {
+    var self = this;
+
+    var _eventTarget = document.createDocumentFragment();
+    ['addEventListener', 'removeEventListener', 'dispatchEvent']
+        .forEach(function(method) {
+          self[method] = _eventTarget[method].bind(_eventTarget);
+        });
+
+    this.onicecandidate = null;
+    this.onaddstream = null;
+    this.ontrack = null;
+    this.onremovestream = null;
+    this.onsignalingstatechange = null;
+    this.oniceconnectionstatechange = null;
+    this.onicegatheringstatechange = null;
+    this.onnegotiationneeded = null;
+    this.ondatachannel = null;
+    this.canTrickleIceCandidates = null;
+
+    this.needNegotiation = false;
+
+    this.localStreams = [];
+    this.remoteStreams = [];
+
+    this.localDescription = null;
+    this.remoteDescription = null;
+
+    this.signalingState = 'stable';
+    this.iceConnectionState = 'new';
+    this.iceGatheringState = 'new';
+
+    config = JSON.parse(JSON.stringify(config || {}));
+
+    this.usingBundle = config.bundlePolicy === 'max-bundle';
+    if (config.rtcpMuxPolicy === 'negotiate') {
+      var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported');
+      e.name = 'NotSupportedError';
+      throw(e);
+    } else if (!config.rtcpMuxPolicy) {
+      config.rtcpMuxPolicy = 'require';
+    }
+
+    switch (config.iceTransportPolicy) {
+      case 'all':
+      case 'relay':
+        break;
+      default:
+        config.iceTransportPolicy = 'all';
+        break;
+    }
+
+    switch (config.bundlePolicy) {
+      case 'balanced':
+      case 'max-compat':
+      case 'max-bundle':
+        break;
+      default:
+        config.bundlePolicy = 'balanced';
+        break;
+    }
+
+    config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
+
+    this._iceGatherers = [];
+    if (config.iceCandidatePoolSize) {
+      for (var i = config.iceCandidatePoolSize; i > 0; i--) {
+        this._iceGatherers = new window.RTCIceGatherer({
+          iceServers: config.iceServers,
+          gatherPolicy: config.iceTransportPolicy
+        });
+      }
+    } else {
+      config.iceCandidatePoolSize = 0;
+    }
+
+    this._config = config;
+
+    // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
+    // everything that is needed to describe a SDP m-line.
+    this.transceivers = [];
+
+    this._sdpSessionId = SDPUtils.generateSessionId();
+    this._sdpSessionVersion = 0;
+
+    this._dtlsRole = undefined; // role for a=setup to use in answers.
+  };
+
+  RTCPeerConnection.prototype._emitGatheringStateChange = function() {
+    var event = new Event('icegatheringstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onicegatheringstatechange === 'function') {
+      this.onicegatheringstatechange(event);
+    }
+  };
+
+  RTCPeerConnection.prototype.getConfiguration = function() {
+    return this._config;
+  };
+
+  RTCPeerConnection.prototype.getLocalStreams = function() {
+    return this.localStreams;
+  };
+
+  RTCPeerConnection.prototype.getRemoteStreams = function() {
+    return this.remoteStreams;
+  };
+
+  // internal helper to create a transceiver object.
+  // (whih is not yet the same as the WebRTC 1.0 transceiver)
+  RTCPeerConnection.prototype._createTransceiver = function(kind) {
+    var hasBundleTransport = this.transceivers.length > 0;
+    var transceiver = {
+      track: null,
+      iceGatherer: null,
+      iceTransport: null,
+      dtlsTransport: null,
+      localCapabilities: null,
+      remoteCapabilities: null,
+      rtpSender: null,
+      rtpReceiver: null,
+      kind: kind,
+      mid: null,
+      sendEncodingParameters: null,
+      recvEncodingParameters: null,
+      stream: null,
+      wantReceive: true
+    };
+    if (this.usingBundle && hasBundleTransport) {
+      transceiver.iceTransport = this.transceivers[0].iceTransport;
+      transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
+    } else {
+      var transports = this._createIceAndDtlsTransports();
+      transceiver.iceTransport = transports.iceTransport;
+      transceiver.dtlsTransport = transports.dtlsTransport;
+    }
+    this.transceivers.push(transceiver);
+    return transceiver;
+  };
+
+  RTCPeerConnection.prototype.addTrack = function(track, stream) {
+    var transceiver;
+    for (var i = 0; i < this.transceivers.length; i++) {
+      if (!this.transceivers[i].track &&
+          this.transceivers[i].kind === track.kind) {
+        transceiver = this.transceivers[i];
+      }
+    }
+    if (!transceiver) {
+      transceiver = this._createTransceiver(track.kind);
+    }
+
+    this._maybeFireNegotiationNeeded();
+
+    if (this.localStreams.indexOf(stream) === -1) {
+      this.localStreams.push(stream);
+    }
+
+    transceiver.track = track;
+    transceiver.stream = stream;
+    transceiver.rtpSender = new window.RTCRtpSender(track,
+        transceiver.dtlsTransport);
+    return transceiver.rtpSender;
+  };
+
+  RTCPeerConnection.prototype.addStream = function(stream) {
+    var self = this;
+    if (edgeVersion >= 15025) {
+      stream.getTracks().forEach(function(track) {
+        self.addTrack(track, stream);
+      });
+    } else {
+      // Clone is necessary for local demos mostly, attaching directly
+      // to two different senders does not work (build 10547).
+      // Fixed in 15025 (or earlier)
+      var clonedStream = stream.clone();
+      stream.getTracks().forEach(function(track, idx) {
+        var clonedTrack = clonedStream.getTracks()[idx];
+        track.addEventListener('enabled', function(event) {
+          clonedTrack.enabled = event.enabled;
+        });
+      });
+      clonedStream.getTracks().forEach(function(track) {
+        self.addTrack(track, clonedStream);
+      });
+    }
+  };
+
+  RTCPeerConnection.prototype.removeStream = function(stream) {
+    var idx = this.localStreams.indexOf(stream);
+    if (idx > -1) {
+      this.localStreams.splice(idx, 1);
+      this._maybeFireNegotiationNeeded();
+    }
+  };
+
+  RTCPeerConnection.prototype.getSenders = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpSender;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpSender;
+    });
+  };
+
+  RTCPeerConnection.prototype.getReceivers = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpReceiver;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpReceiver;
+    });
+  };
+
+
+  RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
+      usingBundle) {
+    var self = this;
+    if (usingBundle && sdpMLineIndex > 0) {
+      return this.transceivers[0].iceGatherer;
+    } else if (this._iceGatherers.length) {
+      return this._iceGatherers.shift();
+    }
+    var iceGatherer = new window.RTCIceGatherer({
+      iceServers: this._config.iceServers,
+      gatherPolicy: this._config.iceTransportPolicy
+    });
+    Object.defineProperty(iceGatherer, 'state',
+        {value: 'new', writable: true}
+    );
+
+    this.transceivers[sdpMLineIndex].candidates = [];
+    this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
+      var end = !event.candidate || Object.keys(event.candidate).length === 0;
+      // polyfill since RTCIceGatherer.state is not implemented in
+      // Edge 10547 yet.
+      iceGatherer.state = end ? 'completed' : 'gathering';
+      if (self.transceivers[sdpMLineIndex].candidates !== null) {
+        self.transceivers[sdpMLineIndex].candidates.push(event.candidate);
+      }
+    };
+    iceGatherer.addEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    return iceGatherer;
+  };
+
+  // start gathering from an RTCIceGatherer.
+  RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
+    var self = this;
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer.onlocalcandidate) {
+      return;
+    }
+    var candidates = this.transceivers[sdpMLineIndex].candidates;
+    this.transceivers[sdpMLineIndex].candidates = null;
+    iceGatherer.removeEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    iceGatherer.onlocalcandidate = function(evt) {
+      if (self.usingBundle && sdpMLineIndex > 0) {
+        // if we know that we use bundle we can drop candidates with
+        // ѕdpMLineIndex > 0. If we don't do this then our state gets
+        // confused since we dispose the extra ice gatherer.
+        return;
+      }
+      var event = new Event('icecandidate');
+      event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+      var cand = evt.candidate;
+      // Edge emits an empty object for RTCIceCandidateComplete‥
+      var end = !cand || Object.keys(cand).length === 0;
+      if (end) {
+        // polyfill since RTCIceGatherer.state is not implemented in
+        // Edge 10547 yet.
+        if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
+          iceGatherer.state = 'completed';
+        }
+      } else {
+        if (iceGatherer.state === 'new') {
+          iceGatherer.state = 'gathering';
+        }
+        // RTCIceCandidate doesn't have a component, needs to be added
+        cand.component = 1;
+        event.candidate.candidate = SDPUtils.writeCandidate(cand);
+      }
+
+      // update local description.
+      var sections = SDPUtils.splitSections(self.localDescription.sdp);
+      if (!end) {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=' + event.candidate.candidate + '\r\n';
+      } else {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=end-of-candidates\r\n';
+      }
+      self.localDescription.sdp = sections.join('');
+      var complete = self.transceivers.every(function(transceiver) {
+        return transceiver.iceGatherer &&
+            transceiver.iceGatherer.state === 'completed';
+      });
+
+      if (self.iceGatheringState !== 'gathering') {
+        self.iceGatheringState = 'gathering';
+        self._emitGatheringStateChange();
+      }
+
+      // Emit candidate. Also emit null candidate when all gatherers are
+      // complete.
+      if (!end) {
+        self.dispatchEvent(event);
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(event);
+        }
+      }
+      if (complete) {
+        self.dispatchEvent(new Event('icecandidate'));
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(new Event('icecandidate'));
+        }
+        self.iceGatheringState = 'complete';
+        self._emitGatheringStateChange();
+      }
+    };
+
+    // emit already gathered candidates.
+    window.setTimeout(function() {
+      candidates.forEach(function(candidate) {
+        var e = new Event('RTCIceGatherEvent');
+        e.candidate = candidate;
+        iceGatherer.onlocalcandidate(e);
+      });
+    }, 0);
+  };
+
+  // Create ICE transport and DTLS transport.
+  RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
+    var self = this;
+    var iceTransport = new window.RTCIceTransport(null);
+    iceTransport.onicestatechange = function() {
+      self._updateConnectionState();
+    };
+
+    var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
+    dtlsTransport.ondtlsstatechange = function() {
+      self._updateConnectionState();
+    };
+    dtlsTransport.onerror = function() {
+      // onerror does not set state to failed by itself.
+      Object.defineProperty(dtlsTransport, 'state',
+          {value: 'failed', writable: true});
+      self._updateConnectionState();
+    };
+
+    return {
+      iceTransport: iceTransport,
+      dtlsTransport: dtlsTransport
+    };
+  };
+
+  // Destroy ICE gatherer, ICE transport and DTLS transport.
+  // Without triggering the callbacks.
+  RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
+      sdpMLineIndex) {
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer) {
+      delete iceGatherer.onlocalcandidate;
+      delete this.transceivers[sdpMLineIndex].iceGatherer;
+    }
+    var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
+    if (iceTransport) {
+      delete iceTransport.onicestatechange;
+      delete this.transceivers[sdpMLineIndex].iceTransport;
+    }
+    var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
+    if (dtlsTransport) {
+      delete dtlsTransport.ondtlsstatechange;
+      delete dtlsTransport.onerror;
+      delete this.transceivers[sdpMLineIndex].dtlsTransport;
+    }
+  };
+
+  // Start the RTP Sender and Receiver for a transceiver.
+  RTCPeerConnection.prototype._transceive = function(transceiver,
+      send, recv) {
+    var params = getCommonCapabilities(transceiver.localCapabilities,
+        transceiver.remoteCapabilities);
+    if (send && transceiver.rtpSender) {
+      params.encodings = transceiver.sendEncodingParameters;
+      params.rtcp = {
+        cname: SDPUtils.localCName,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.recvEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpSender.send(params);
+    }
+    if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
+      // remove RTX field in Edge 14942
+      if (transceiver.kind === 'video'
+          && transceiver.recvEncodingParameters
+          && edgeVersion < 15019) {
+        transceiver.recvEncodingParameters.forEach(function(p) {
+          delete p.rtx;
+        });
+      }
+      params.encodings = transceiver.recvEncodingParameters;
+      params.rtcp = {
+        cname: transceiver.rtcpParameters.cname,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.sendEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpReceiver.receive(params);
+    }
+  };
+
+  RTCPeerConnection.prototype.setLocalDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setLocalDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set local ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var sections;
+    var sessionpart;
+    if (description.type === 'offer') {
+      // VERY limited support for SDP munging. Limited to:
+      // * changing the order of codecs
+      sections = SDPUtils.splitSections(description.sdp);
+      sessionpart = sections.shift();
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var caps = SDPUtils.parseRtpParameters(mediaSection);
+        self.transceivers[sdpMLineIndex].localCapabilities = caps;
+      });
+
+      this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+        self._gather(transceiver.mid, sdpMLineIndex);
+      });
+    } else if (description.type === 'answer') {
+      sections = SDPUtils.splitSections(self.remoteDescription.sdp);
+      sessionpart = sections.shift();
+      var isIceLite = SDPUtils.matchPrefix(sessionpart,
+          'a=ice-lite').length > 0;
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var transceiver = self.transceivers[sdpMLineIndex];
+        var iceGatherer = transceiver.iceGatherer;
+        var iceTransport = transceiver.iceTransport;
+        var dtlsTransport = transceiver.dtlsTransport;
+        var localCapabilities = transceiver.localCapabilities;
+        var remoteCapabilities = transceiver.remoteCapabilities;
+
+        // treat bundle-only as not-rejected.
+        var rejected = SDPUtils.isRejected(mediaSection) &&
+            !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+
+        if (!rejected && !transceiver.isDatachannel) {
+          var remoteIceParameters = SDPUtils.getIceParameters(
+              mediaSection, sessionpart);
+          var remoteDtlsParameters = SDPUtils.getDtlsParameters(
+              mediaSection, sessionpart);
+          if (isIceLite) {
+            remoteDtlsParameters.role = 'server';
+          }
+
+          if (!self.usingBundle || sdpMLineIndex === 0) {
+            self._gather(transceiver.mid, sdpMLineIndex);
+            if (iceTransport.state === 'new') {
+              iceTransport.start(iceGatherer, remoteIceParameters,
+                  isIceLite ? 'controlling' : 'controlled');
+            }
+            if (dtlsTransport.state === 'new') {
+              dtlsTransport.start(remoteDtlsParameters);
+            }
+          }
+
+          // Calculate intersection of capabilities.
+          var params = getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          // Start the RTCRtpSender. The RTCRtpReceiver for this
+          // transceiver has already been started in setRemoteDescription.
+          self._transceive(transceiver,
+              params.codecs.length > 0,
+              false);
+        }
+      });
+    }
+
+    this.localDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+
+    // If a success callback was provided, emit ICE candidates after it
+    // has been executed. Otherwise, emit callback after the Promise is
+    // resolved.
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    return new Promise(function(resolve) {
+      if (cb) {
+        cb.apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.setRemoteDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setRemoteDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set remote ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var streams = {};
+    this.remoteStreams.forEach(function(stream) {
+      streams[stream.id] = stream;
+    });
+    var receiverList = [];
+    var sections = SDPUtils.splitSections(description.sdp);
+    var sessionpart = sections.shift();
+    var isIceLite = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-lite').length > 0;
+    var usingBundle = SDPUtils.matchPrefix(sessionpart,
+        'a=group:BUNDLE ').length > 0;
+    this.usingBundle = usingBundle;
+    var iceOptions = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-options:')[0];
+    if (iceOptions) {
+      this.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
+          .indexOf('trickle') >= 0;
+    } else {
+      this.canTrickleIceCandidates = false;
+    }
+
+    sections.forEach(function(mediaSection, sdpMLineIndex) {
+      var lines = SDPUtils.splitLines(mediaSection);
+      var kind = SDPUtils.getKind(mediaSection);
+      // treat bundle-only as not-rejected.
+      var rejected = SDPUtils.isRejected(mediaSection) &&
+          !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+      var protocol = lines[0].substr(2).split(' ')[2];
+
+      var direction = SDPUtils.getDirection(mediaSection, sessionpart);
+      var remoteMsid = SDPUtils.parseMsid(mediaSection);
+
+      var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();
+
+      // Reject datachannels which are not implemented yet.
+      if (kind === 'application' && protocol === 'DTLS/SCTP') {
+        self.transceivers[sdpMLineIndex] = {
+          mid: mid,
+          isDatachannel: true
+        };
+        return;
+      }
+
+      var transceiver;
+      var iceGatherer;
+      var iceTransport;
+      var dtlsTransport;
+      var rtpReceiver;
+      var sendEncodingParameters;
+      var recvEncodingParameters;
+      var localCapabilities;
+
+      var track;
+      // FIXME: ensure the mediaSection has rtcp-mux set.
+      var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
+      var remoteIceParameters;
+      var remoteDtlsParameters;
+      if (!rejected) {
+        remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters.role = 'client';
+      }
+      recvEncodingParameters =
+          SDPUtils.parseRtpEncodingParameters(mediaSection);
+
+      var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);
+
+      var isComplete = SDPUtils.matchPrefix(mediaSection,
+          'a=end-of-candidates', sessionpart).length > 0;
+      var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
+          .map(function(cand) {
+            return SDPUtils.parseCandidate(cand);
+          })
+          .filter(function(cand) {
+            return cand.component === 1;
+          });
+
+      // Check if we can use BUNDLE and dispose transports.
+      if ((description.type === 'offer' || description.type === 'answer') &&
+          !rejected && usingBundle && sdpMLineIndex > 0 &&
+          self.transceivers[sdpMLineIndex]) {
+        self._disposeIceAndDtlsTransports(sdpMLineIndex);
+        self.transceivers[sdpMLineIndex].iceGatherer =
+            self.transceivers[0].iceGatherer;
+        self.transceivers[sdpMLineIndex].iceTransport =
+            self.transceivers[0].iceTransport;
+        self.transceivers[sdpMLineIndex].dtlsTransport =
+            self.transceivers[0].dtlsTransport;
+        if (self.transceivers[sdpMLineIndex].rtpSender) {
+          self.transceivers[sdpMLineIndex].rtpSender.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+        if (self.transceivers[sdpMLineIndex].rtpReceiver) {
+          self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+      }
+      if (description.type === 'offer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex] ||
+            self._createTransceiver(kind);
+        transceiver.mid = mid;
+
+        if (!transceiver.iceGatherer) {
+          transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+              usingBundle);
+        }
+
+        if (cands.length && transceiver.iceTransport.state === 'new') {
+          if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
+            transceiver.iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);
+
+        // filter RTX until additional stuff needed for RTX is implemented
+        // in adapter.js
+        if (edgeVersion < 15019) {
+          localCapabilities.codecs = localCapabilities.codecs.filter(
+              function(codec) {
+                return codec.name !== 'rtx';
+              });
+        }
+
+        sendEncodingParameters = transceiver.sendEncodingParameters || [{
+          ssrc: (2 * sdpMLineIndex + 2) * 1001
+        }];
+
+        var isNewTrack = false;
+        if (direction === 'sendrecv' || direction === 'sendonly') {
+          isNewTrack = !transceiver.rtpReceiver;
+          rtpReceiver = transceiver.rtpReceiver ||
+              new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);
+
+          if (isNewTrack) {
+            var stream;
+            track = rtpReceiver.track;
+            // FIXME: does not work with Plan B.
+            if (remoteMsid) {
+              if (!streams[remoteMsid.stream]) {
+                streams[remoteMsid.stream] = new window.MediaStream();
+                Object.defineProperty(streams[remoteMsid.stream], 'id', {
+                  get: function() {
+                    return remoteMsid.stream;
+                  }
+                });
+              }
+              Object.defineProperty(track, 'id', {
+                get: function() {
+                  return remoteMsid.track;
+                }
+              });
+              stream = streams[remoteMsid.stream];
+            } else {
+              if (!streams.default) {
+                streams.default = new window.MediaStream();
+              }
+              stream = streams.default;
+            }
+            stream.addTrack(track);
+            receiverList.push([track, rtpReceiver, stream]);
+          }
+        }
+
+        transceiver.localCapabilities = localCapabilities;
+        transceiver.remoteCapabilities = remoteCapabilities;
+        transceiver.rtpReceiver = rtpReceiver;
+        transceiver.rtcpParameters = rtcpParameters;
+        transceiver.sendEncodingParameters = sendEncodingParameters;
+        transceiver.recvEncodingParameters = recvEncodingParameters;
+
+        // Start the RTCRtpReceiver now. The RTPSender is started in
+        // setLocalDescription.
+        self._transceive(self.transceivers[sdpMLineIndex],
+            false,
+            isNewTrack);
+      } else if (description.type === 'answer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex];
+        iceGatherer = transceiver.iceGatherer;
+        iceTransport = transceiver.iceTransport;
+        dtlsTransport = transceiver.dtlsTransport;
+        rtpReceiver = transceiver.rtpReceiver;
+        sendEncodingParameters = transceiver.sendEncodingParameters;
+        localCapabilities = transceiver.localCapabilities;
+
+        self.transceivers[sdpMLineIndex].recvEncodingParameters =
+            recvEncodingParameters;
+        self.transceivers[sdpMLineIndex].remoteCapabilities =
+            remoteCapabilities;
+        self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;
+
+        if (cands.length && iceTransport.state === 'new') {
+          if ((isIceLite || isComplete) &&
+              (!usingBundle || sdpMLineIndex === 0)) {
+            iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        if (!usingBundle || sdpMLineIndex === 0) {
+          if (iceTransport.state === 'new') {
+            iceTransport.start(iceGatherer, remoteIceParameters,
+                'controlling');
+          }
+          if (dtlsTransport.state === 'new') {
+            dtlsTransport.start(remoteDtlsParameters);
+          }
+        }
+
+        self._transceive(transceiver,
+            direction === 'sendrecv' || direction === 'recvonly',
+            direction === 'sendrecv' || direction === 'sendonly');
+
+        if (rtpReceiver &&
+            (direction === 'sendrecv' || direction === 'sendonly')) {
+          track = rtpReceiver.track;
+          if (remoteMsid) {
+            if (!streams[remoteMsid.stream]) {
+              streams[remoteMsid.stream] = new window.MediaStream();
+            }
+            streams[remoteMsid.stream].addTrack(track);
+            receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
+          } else {
+            if (!streams.default) {
+              streams.default = new window.MediaStream();
+            }
+            streams.default.addTrack(track);
+            receiverList.push([track, rtpReceiver, streams.default]);
+          }
+        } else {
+          // FIXME: actually the receiver should be created later.
+          delete transceiver.rtpReceiver;
+        }
+      }
+    });
+
+    if (this._dtlsRole === undefined) {
+      this._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
+    }
+
+    this.remoteDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+    Object.keys(streams).forEach(function(sid) {
+      var stream = streams[sid];
+      if (stream.getTracks().length) {
+        if (self.remoteStreams.indexOf(stream) === -1) {
+          self.remoteStreams.push(stream);
+          var event = new Event('addstream');
+          event.stream = stream;
+          window.setTimeout(function() {
+            self.dispatchEvent(event);
+            if (typeof self.onaddstream === 'function') {
+              self.onaddstream(event);
+            }
+          });
+        }
+
+        receiverList.forEach(function(item) {
+          var track = item[0];
+          var receiver = item[1];
+          if (stream.id !== item[2].id) {
+            return;
+          }
+          var trackEvent = new Event('track');
+          trackEvent.track = track;
+          trackEvent.receiver = receiver;
+          trackEvent.transceiver = {receiver: receiver};
+          trackEvent.streams = [stream];
+          window.setTimeout(function() {
+            self.dispatchEvent(trackEvent);
+            if (typeof self.ontrack === 'function') {
+              self.ontrack(trackEvent);
+            }
+          });
+        });
+      }
+    });
+
+    // check whether addIceCandidate({}) was called within four seconds after
+    // setRemoteDescription.
+    window.setTimeout(function() {
+      if (!(self && self.transceivers)) {
+        return;
+      }
+      self.transceivers.forEach(function(transceiver) {
+        if (transceiver.iceTransport &&
+            transceiver.iceTransport.state === 'new' &&
+            transceiver.iceTransport.getRemoteCandidates().length > 0) {
+          console.warn('Timeout for addRemoteCandidate. Consider sending ' +
+              'an end-of-candidates notification');
+          transceiver.iceTransport.addRemoteCandidate({});
+        }
+      });
+    }, 4000);
+
+    return new Promise(function(resolve) {
+      if (args.length > 1 && typeof args[1] === 'function') {
+        args[1].apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.close = function() {
+    this.transceivers.forEach(function(transceiver) {
+      /* not yet
+      if (transceiver.iceGatherer) {
+        transceiver.iceGatherer.close();
+      }
+      */
+      if (transceiver.iceTransport) {
+        transceiver.iceTransport.stop();
+      }
+      if (transceiver.dtlsTransport) {
+        transceiver.dtlsTransport.stop();
+      }
+      if (transceiver.rtpSender) {
+        transceiver.rtpSender.stop();
+      }
+      if (transceiver.rtpReceiver) {
+        transceiver.rtpReceiver.stop();
+      }
+    });
+    // FIXME: clean up tracks, local streams, remote streams, etc
+    this._updateSignalingState('closed');
+  };
+
+  // Update the signaling state.
+  RTCPeerConnection.prototype._updateSignalingState = function(newState) {
+    this.signalingState = newState;
+    var event = new Event('signalingstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onsignalingstatechange === 'function') {
+      this.onsignalingstatechange(event);
+    }
+  };
+
+  // Determine whether to fire the negotiationneeded event.
+  RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
+    var self = this;
+    if (this.signalingState !== 'stable' || this.needNegotiation === true) {
+      return;
+    }
+    this.needNegotiation = true;
+    window.setTimeout(function() {
+      if (self.needNegotiation === false) {
+        return;
+      }
+      self.needNegotiation = false;
+      var event = new Event('negotiationneeded');
+      self.dispatchEvent(event);
+      if (typeof self.onnegotiationneeded === 'function') {
+        self.onnegotiationneeded(event);
+      }
+    }, 0);
+  };
+
+  // Update the connection state.
+  RTCPeerConnection.prototype._updateConnectionState = function() {
+    var newState;
+    var states = {
+      'new': 0,
+      closed: 0,
+      connecting: 0,
+      checking: 0,
+      connected: 0,
+      completed: 0,
+      disconnected: 0,
+      failed: 0
+    };
+    this.transceivers.forEach(function(transceiver) {
+      states[transceiver.iceTransport.state]++;
+      states[transceiver.dtlsTransport.state]++;
+    });
+    // ICETransport.completed and connected are the same for this purpose.
+    states.connected += states.completed;
+
+    newState = 'new';
+    if (states.failed > 0) {
+      newState = 'failed';
+    } else if (states.connecting > 0 || states.checking > 0) {
+      newState = 'connecting';
+    } else if (states.disconnected > 0) {
+      newState = 'disconnected';
+    } else if (states.new > 0) {
+      newState = 'new';
+    } else if (states.connected > 0 || states.completed > 0) {
+      newState = 'connected';
+    }
+
+    if (newState !== this.iceConnectionState) {
+      this.iceConnectionState = newState;
+      var event = new Event('iceconnectionstatechange');
+      this.dispatchEvent(event);
+      if (typeof this.oniceconnectionstatechange === 'function') {
+        this.oniceconnectionstatechange(event);
+      }
+    }
+  };
+
+  RTCPeerConnection.prototype.createOffer = function() {
+    var self = this;
+    var args = arguments;
+
+    var offerOptions;
+    if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+      offerOptions = arguments[0];
+    } else if (arguments.length === 3) {
+      offerOptions = arguments[2];
+    }
+
+    var numAudioTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'audio';
+    }).length;
+    var numVideoTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'video';
+    }).length;
+
+    // Determine number of audio and video tracks we need to send/recv.
+    if (offerOptions) {
+      // Reject Chrome legacy constraints.
+      if (offerOptions.mandatory || offerOptions.optional) {
+        throw new TypeError(
+            'Legacy mandatory/optional constraints not supported.');
+      }
+      if (offerOptions.offerToReceiveAudio !== undefined) {
+        if (offerOptions.offerToReceiveAudio === true) {
+          numAudioTracks = 1;
+        } else if (offerOptions.offerToReceiveAudio === false) {
+          numAudioTracks = 0;
+        } else {
+          numAudioTracks = offerOptions.offerToReceiveAudio;
+        }
+      }
+      if (offerOptions.offerToReceiveVideo !== undefined) {
+        if (offerOptions.offerToReceiveVideo === true) {
+          numVideoTracks = 1;
+        } else if (offerOptions.offerToReceiveVideo === false) {
+          numVideoTracks = 0;
+        } else {
+          numVideoTracks = offerOptions.offerToReceiveVideo;
+        }
+      }
+    }
+
+    this.transceivers.forEach(function(transceiver) {
+      if (transceiver.kind === 'audio') {
+        numAudioTracks--;
+        if (numAudioTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      } else if (transceiver.kind === 'video') {
+        numVideoTracks--;
+        if (numVideoTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      }
+    });
+
+    // Create M-lines for recvonly streams.
+    while (numAudioTracks > 0 || numVideoTracks > 0) {
+      if (numAudioTracks > 0) {
+        this._createTransceiver('audio');
+        numAudioTracks--;
+      }
+      if (numVideoTracks > 0) {
+        this._createTransceiver('video');
+        numVideoTracks--;
+      }
+    }
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      // For each track, create an ice gatherer, ice transport,
+      // dtls transport, potentially rtpsender and rtpreceiver.
+      var track = transceiver.track;
+      var kind = transceiver.kind;
+      var mid = SDPUtils.generateIdentifier();
+      transceiver.mid = mid;
+
+      if (!transceiver.iceGatherer) {
+        transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+            self.usingBundle);
+      }
+
+      var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
+      // filter RTX until additional stuff needed for RTX is implemented
+      // in adapter.js
+      if (edgeVersion < 15019) {
+        localCapabilities.codecs = localCapabilities.codecs.filter(
+            function(codec) {
+              return codec.name !== 'rtx';
+            });
+      }
+      localCapabilities.codecs.forEach(function(codec) {
+        // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
+        // by adding level-asymmetry-allowed=1
+        if (codec.name === 'H264' &&
+            codec.parameters['level-asymmetry-allowed'] === undefined) {
+          codec.parameters['level-asymmetry-allowed'] = '1';
+        }
+      });
+
+      // generate an ssrc now, to be used later in rtpSender.send
+      var sendEncodingParameters = transceiver.sendEncodingParameters || [{
+        ssrc: (2 * sdpMLineIndex + 1) * 1001
+      }];
+      if (track) {
+        // add RTX
+        if (edgeVersion >= 15019 && kind === 'video' &&
+            !sendEncodingParameters[0].rtx) {
+          sendEncodingParameters[0].rtx = {
+            ssrc: sendEncodingParameters[0].ssrc + 1
+          };
+        }
+      }
+
+      if (transceiver.wantReceive) {
+        transceiver.rtpReceiver = new window.RTCRtpReceiver(
+            transceiver.dtlsTransport, kind);
+      }
+
+      transceiver.localCapabilities = localCapabilities;
+      transceiver.sendEncodingParameters = sendEncodingParameters;
+    });
+
+    // always offer BUNDLE and dispose on return if not supported.
+    if (this._config.bundlePolicy !== 'max-compat') {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    sdp += 'a=ice-options:trickle\r\n';
+
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
+          'offer', transceiver.stream, self._dtlsRole);
+      sdp += 'a=rtcp-rsize\r\n';
+
+      if (transceiver.iceGatherer && self.iceGatheringState !== 'new' &&
+          (sdpMLineIndex === 0 || !self.usingBundle)) {
+        transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) {
+          cand.component = 1;
+          sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
+        });
+
+        if (transceiver.iceGatherer.state === 'completed') {
+          sdp += 'a=end-of-candidates\r\n';
+        }
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'offer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.createAnswer = function() {
+    var self = this;
+    var args = arguments;
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    if (this.usingBundle) {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    var mediaSectionsInOffer = SDPUtils.splitSections(
+        this.remoteDescription.sdp).length - 1;
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
+        return;
+      }
+      if (transceiver.isDatachannel) {
+        sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
+            'c=IN IP4 0.0.0.0\r\n' +
+            'a=mid:' + transceiver.mid + '\r\n';
+        return;
+      }
+
+      // FIXME: look at direction.
+      if (transceiver.stream) {
+        var localTrack;
+        if (transceiver.kind === 'audio') {
+          localTrack = transceiver.stream.getAudioTracks()[0];
+        } else if (transceiver.kind === 'video') {
+          localTrack = transceiver.stream.getVideoTracks()[0];
+        }
+        if (localTrack) {
+          // add RTX
+          if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
+              !transceiver.sendEncodingParameters[0].rtx) {
+            transceiver.sendEncodingParameters[0].rtx = {
+              ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
+            };
+          }
+        }
+      }
+
+      // Calculate intersection of capabilities.
+      var commonCapabilities = getCommonCapabilities(
+          transceiver.localCapabilities,
+          transceiver.remoteCapabilities);
+
+      var hasRtx = commonCapabilities.codecs.filter(function(c) {
+        return c.name.toLowerCase() === 'rtx';
+      }).length;
+      if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
+        delete transceiver.sendEncodingParameters[0].rtx;
+      }
+
+      sdp += writeMediaSection(transceiver, commonCapabilities,
+          'answer', transceiver.stream, self._dtlsRole);
+      if (transceiver.rtcpParameters &&
+          transceiver.rtcpParameters.reducedSize) {
+        sdp += 'a=rtcp-rsize\r\n';
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'answer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+    var err;
+    var sections;
+    if (!candidate || candidate.candidate === '') {
+      for (var j = 0; j < this.transceivers.length; j++) {
+        if (this.transceivers[j].isDatachannel) {
+          continue;
+        }
+        this.transceivers[j].iceTransport.addRemoteCandidate({});
+        sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+        sections[j + 1] += 'a=end-of-candidates\r\n';
+        this.remoteDescription.sdp = sections.join('');
+        if (this.usingBundle) {
+          break;
+        }
+      }
+    } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) {
+      throw new TypeError('sdpMLineIndex or sdpMid required');
+    } else if (!this.remoteDescription) {
+      err = new Error('Can not add ICE candidate without ' +
+          'a remote description');
+      err.name = 'InvalidStateError';
+    } else {
+      var sdpMLineIndex = candidate.sdpMLineIndex;
+      if (candidate.sdpMid) {
+        for (var i = 0; i < this.transceivers.length; i++) {
+          if (this.transceivers[i].mid === candidate.sdpMid) {
+            sdpMLineIndex = i;
+            break;
+          }
+        }
+      }
+      var transceiver = this.transceivers[sdpMLineIndex];
+      if (transceiver) {
+        if (transceiver.isDatachannel) {
+          return Promise.resolve();
+        }
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            SDPUtils.parseCandidate(candidate.candidate) : {};
+        // Ignore Chrome's invalid candidates since Edge does not like them.
+        if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
+          return Promise.resolve();
+        }
+        // Ignore RTCP candidates, we assume RTCP-MUX.
+        if (cand.component && cand.component !== 1) {
+          return Promise.resolve();
+        }
+        // when using bundle, avoid adding candidates to the wrong
+        // ice transport. And avoid adding candidates added in the SDP.
+        if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
+            transceiver.iceTransport !== this.transceivers[0].iceTransport)) {
+          if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
+            err = new Error('Can not add ICE candidate');
+            err.name = 'OperationError';
+          }
+        }
+
+        if (!err) {
+          // update the remoteDescription.
+          var candidateString = candidate.candidate.trim();
+          if (candidateString.indexOf('a=') === 0) {
+            candidateString = candidateString.substr(2);
+          }
+          sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+          sections[sdpMLineIndex + 1] += 'a=' +
+              (cand.type ? candidateString : 'end-of-candidates')
+              + '\r\n';
+          this.remoteDescription.sdp = sections.join('');
+        }
+      } else {
+        err = new Error('Can not add ICE candidate');
+        err.name = 'OperationError';
+      }
+    }
+    var args = arguments;
+    return new Promise(function(resolve, reject) {
+      if (err) {
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [err]);
+        }
+        reject(err);
+      } else {
+        if (args.length > 1 && typeof args[1] === 'function') {
+          args[1].apply(null);
+        }
+        resolve();
+      }
+    });
+  };
+
+  RTCPeerConnection.prototype.getStats = function() {
+    var promises = [];
+    this.transceivers.forEach(function(transceiver) {
+      ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+          'dtlsTransport'].forEach(function(method) {
+            if (transceiver[method]) {
+              promises.push(transceiver[method].getStats());
+            }
+          });
+    });
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    var fixStatsType = function(stat) {
+      return {
+        inboundrtp: 'inbound-rtp',
+        outboundrtp: 'outbound-rtp',
+        candidatepair: 'candidate-pair',
+        localcandidate: 'local-candidate',
+        remotecandidate: 'remote-candidate'
+      }[stat.type] || stat.type;
+    };
+    return new Promise(function(resolve) {
+      // shim getStats with maplike support
+      var results = new Map();
+      Promise.all(promises).then(function(res) {
+        res.forEach(function(result) {
+          Object.keys(result).forEach(function(id) {
+            result[id].type = fixStatsType(result[id]);
+            results.set(id, result[id]);
+          });
+        });
+        if (cb) {
+          cb.apply(null, results);
+        }
+        resolve(results);
+      });
+    });
+  };
+  return RTCPeerConnection;
+};
+
+},{"sdp":2}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrÑ–bute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 63) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":13}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":13,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var shimRTCPeerConnection = require('rtcpeerconnection-shim');
+
+module.exports = {
+  shimGetUserMedia: require('./getusermedia'),
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (window.RTCIceGatherer) {
+      // ORTC defines an RTCIceCandidate object but no constructor.
+      // Not implemented in Edge.
+      if (!window.RTCIceCandidate) {
+        window.RTCIceCandidate = function(args) {
+          return args;
+        };
+      }
+      // ORTC does not have a session description object but
+      // other browsers (i.e. Chrome) that will support both PC and ORTC
+      // in the future might have this defined already.
+      if (!window.RTCSessionDescription) {
+        window.RTCSessionDescription = function(args) {
+          return args;
+        };
+      }
+      // this adds an additional event listener to MediaStrackTrack that signals
+      // when a tracks enabled property was changed. Workaround for a bug in
+      // addStream, see below. No longer required in 15025+
+      if (browserDetails.version < 15025) {
+        var origMSTEnabled = Object.getOwnPropertyDescriptor(
+            window.MediaStreamTrack.prototype, 'enabled');
+        Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
+          set: function(value) {
+            origMSTEnabled.set.call(this, value);
+            var ev = new Event('enabled');
+            ev.enabled = value;
+            this.dispatchEvent(ev);
+          }
+        });
+      }
+    }
+
+    // ORTC defines the DTMF sender a bit different.
+    // https://github.com/w3c/ortc/issues/714
+    if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = new window.RTCDtmfSender(this);
+            } else if (this.track.kind === 'video') {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+
+    window.RTCPeerConnection =
+        shimRTCPeerConnection(window, browserDetails.version);
+  },
+  shimReplaceTrack: function(window) {
+    // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
+    if (window.RTCRtpSender &&
+        !('replaceTrack' in window.RTCRtpSender.prototype)) {
+      window.RTCRtpSender.prototype.replaceTrack =
+          window.RTCRtpSender.prototype.setTrack;
+    }
+  }
+};
+
+},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+// Expose public methods.
+module.exports = function(window) {
+  var navigator = window && window.navigator;
+
+  var shimError_ = function(e) {
+    return {
+      name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name;
+      }
+    };
+  };
+
+  // getUserMedia error shim.
+  var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+      bind(navigator.mediaDevices);
+  navigator.mediaDevices.getUserMedia = function(c) {
+    return origGetUserMedia(c).catch(function(e) {
+      return Promise.reject(shimError_(e));
+    });
+  };
+};
+
+},{}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":13}],12:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":13}],13:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3])(3)
+});
\ No newline at end of file
diff --git a/bigbluebutton-html5/public/js/adjust-videos.js b/bigbluebutton-html5/public/js/adjust-videos.js
new file mode 100644
index 0000000000000000000000000000000000000000..bdacb7f984e4311ff039eefd18e3eb72d0bb2836
--- /dev/null
+++ b/bigbluebutton-html5/public/js/adjust-videos.js
@@ -0,0 +1,89 @@
+
+(function() {
+  function adjustVideos(tagId, centerVideos) {
+    const _minContentAspectRatio = 4 / 3.0;
+
+    function calculateOccupiedArea(canvasWidth, canvasHeight, numColumns, numRows, numChildren) {
+      const obj = calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows);
+      obj.occupiedArea = obj.width * obj.height * numChildren;
+      obj.numColumns = numColumns;
+      obj.numRows = numRows;
+      obj.cellAspectRatio = _minContentAspectRatio;
+      return obj;
+    }
+
+    function calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows) {
+      const obj = {
+        width: Math.floor(canvasWidth / numColumns),
+        height: Math.floor(canvasHeight / numRows),
+      };
+
+      if (obj.width / obj.height > _minContentAspectRatio) {
+        obj.width = Math.min(Math.floor(obj.height * _minContentAspectRatio), Math.floor(canvasWidth / numColumns));
+      } else {
+        obj.height = Math.min(Math.floor(obj.width / _minContentAspectRatio), Math.floor(canvasHeight / numRows));
+      }
+      return obj;
+    }
+
+    function findBestConfiguration(canvasWidth, canvasHeight, numChildrenInCanvas) {
+      let bestConfiguration = {
+        occupiedArea: 0,
+      };
+
+      for (let cols = 1; cols <= numChildrenInCanvas; cols++) {
+        let rows = Math.floor(numChildrenInCanvas / cols);
+
+        // That's a small HACK, different from the original algorithm
+        // Sometimes numChildren will be bigger than cols*rows, this means that this configuration
+        // can't show all the videos and shouldn't be considered. So we just increment the number of rows
+        // and get a configuration which shows all the videos albeit with a few missing slots in the end.
+        //   For example: with numChildren == 8 the loop will generate cols == 3 and rows == 2
+        //   cols * rows is 6 so we bump rows to 3 and then cols*rows is 9 which is bigger than 8
+        if (numChildrenInCanvas > cols * rows) {
+          rows += 1;
+        }
+
+        const currentConfiguration = calculateOccupiedArea(canvasWidth, canvasHeight, cols, rows, numChildrenInCanvas);
+
+        if (currentConfiguration.occupiedArea > bestConfiguration.occupiedArea) {
+          bestConfiguration = currentConfiguration;
+        }
+      }
+
+      return bestConfiguration;
+    }
+
+    // http://stackoverflow.com/a/3437825/414642
+    const e = $("#" + tagId).parent();
+    const x = e.outerWidth() - 1;
+    const y = e.outerHeight() - 1;
+
+    const videos = $("#" + tagId + " video:visible");
+
+    const best = findBestConfiguration(x, y, videos.length);
+
+    videos.each(function (i) {
+      const row = Math.floor(i / best.numColumns);
+      const col = Math.floor(i % best.numColumns);
+
+      // Free width space remaining to the right and below of the videos
+      const remX = (x - best.width * best.numColumns);
+      const remY = (y - best.height * best.numRows);
+
+      // Center videos
+      const top = Math.floor(((best.height) * row) + remY / 2);
+      const left = Math.floor(((best.width) * col) + remX / 2);
+
+      const videoTop = `top: ${top}px;`;
+      const videoLeft = `left: ${left}px;`;
+
+      $(this).attr('style', videoTop + videoLeft);
+    });
+
+    videos.attr('width', best.width);
+    videos.attr('height', best.height);
+  }
+
+  window.adjustVideos = adjustVideos;
+})();
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index 1dab09e2879dc5ec2278e623e6703d710a03efe3..ac582ccc4631a68deb3ddab454687fd7155d3a1e 100644
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -14,6 +14,7 @@ import '/imports/api/chat/server';
 import '/imports/api/screenshare/server';
 import '/imports/api/voice-users/server';
 import '/imports/api/whiteboard-multi-user/server';
+import '/imports/api/video/server';
 
 // Commons
 import '/imports/api/log-client/server';
diff --git a/labs/kurento-screenshare/README.md b/labs/bbb-webrtc-sfu/README.md
similarity index 100%
rename from labs/kurento-screenshare/README.md
rename to labs/bbb-webrtc-sfu/README.md
diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3b9bbe8a29fa7cbbb171da6fcd190eeb7d7e5760
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/config/default.example.yml
@@ -0,0 +1,15 @@
+kurentoUrl: "ws://HOST/kurento"
+kurentoIp: ""
+localIpAddress: ""
+acceptSelfSignedCertificate: false
+redisHost : "127.0.0.1"
+redisPort : "6379"
+clientPort : "3008"
+minVideoPort: 30000
+maxVideoPort: 33000
+from-screenshare: "from-screenshare-sfu"
+to-screenshare: "to-screenshare-sfu"
+from-video: "from-video-sfu"
+to-video: "to-video-sfu"
+from-audio: "from-audio-sfu"
+to-audio: "to-audio-sfu"
diff --git a/labs/kurento-screenshare/debug-start.sh b/labs/bbb-webrtc-sfu/debug-start.sh
similarity index 100%
rename from labs/kurento-screenshare/debug-start.sh
rename to labs/bbb-webrtc-sfu/debug-start.sh
diff --git a/labs/kurento-screenshare/lib/bbb/messages/Constants.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js
similarity index 88%
rename from labs/kurento-screenshare/lib/bbb/messages/Constants.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js
index 6cccf19f93cadf15684b9a09c2f2ddde37909dbd..3b2a77052aefe4154435efb76a485935e4ee9e43 100644
--- a/labs/kurento-screenshare/lib/bbb/messages/Constants.js
+++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js
@@ -1,4 +1,6 @@
 "use strict";
+
+const config = require('config');
 /**
  * @classdesc
  * Message constants for the communication with BigBlueButton
@@ -17,9 +19,17 @@
         FROM_BBB_TRANSCODE_SYSTEM_CHAN : "bigbluebutton:from-bbb-transcode:system",
         FROM_VOICE_CONF_SYSTEM_CHAN: "from-voice-conf-redis-channel",
         TO_BBB_TRANSCODE_SYSTEM_CHAN: "bigbluebutton:to-bbb-transcode:system",
+        FROM_SCREENSHARE: config.get('from-screenshare'),
+        TO_SCREENSHARE: config.get('to-screenshare'),
+        FROM_VIDEO: config.get('from-video'),
+        TO_VIDEO: config.get('to-video'),
+        FROM_AUDIO: config.get('from-audio'),
+        TO_AUDIO: config.get('to-audio'),
 
         // RedisWrapper events
         REDIS_MESSAGE : "redis_message",
+        WEBSOCKET_MESAGE: "ws_message",
+        GATEWAY_MESSAGE: "gateway_message",
 
         // Message identifiers 1x
         START_TRANSCODER_REQUEST: "start_transcoder_request_message",
diff --git a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js
similarity index 69%
rename from labs/kurento-screenshare/lib/bbb/messages/Messaging.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js
index 92bab79944122fb88e16bb7c30152da68a0f80b7..a352acf1e5f8cfd736e48011316209995bf2a87d 100644
--- a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js
+++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js
@@ -1,24 +1,24 @@
-var Constants = require('./Constants.js');
+const Constants = require('./Constants.js');
 
 // Messages
 
-var OutMessage = require('./OutMessage.js');
+let OutMessage = require('./OutMessage.js');
 
-var StartTranscoderRequestMessage =
+let StartTranscoderRequestMessage =
     require('./transcode/StartTranscoderRequestMessage.js')(Constants);
-var StopTranscoderRequestMessage =
+let StopTranscoderRequestMessage =
     require('./transcode/StopTranscoderRequestMessage.js')(Constants);
-var StartTranscoderSysReqMsg =
+let StartTranscoderSysReqMsg =
     require('./transcode/StartTranscoderSysReqMsg.js')();
-var StopTranscoderSysReqMsg =
+let StopTranscoderSysReqMsg =
     require('./transcode/StopTranscoderSysReqMsg.js')();
-var DeskShareRTMPBroadcastStartedEventMessage =
+let DeskShareRTMPBroadcastStartedEventMessage =
     require('./screenshare/DeskShareRTMPBroadcastStartedEventMessage.js')(Constants);
-var DeskShareRTMPBroadcastStoppedEventMessage =
+let DeskShareRTMPBroadcastStoppedEventMessage =
     require('./screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js')(Constants);
-var ScreenshareRTMPBroadcastStartedEventMessage2x =
+let ScreenshareRTMPBroadcastStartedEventMessage2x =
     require('./screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js')(Constants);
-var ScreenshareRTMPBroadcastStoppedEventMessage2x =
+let ScreenshareRTMPBroadcastStoppedEventMessage2x =
     require('./screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js')(Constants);
 
 
@@ -31,39 +31,38 @@ function Messaging() {}
 
 Messaging.prototype.generateStartTranscoderRequestMessage =
   function(meetingId, transcoderId, params) {
-  var statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params);
+  let statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params);
   return statrm.toJson();
 }
 
 Messaging.prototype.generateStopTranscoderRequestMessage =
   function(meetingId, transcoderId) {
-  var stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId);
+  let stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId);
   return stotrm.toJson();
 }
 
 Messaging.prototype.generateDeskShareRTMPBroadcastStartedEvent =
   function(conferenceName, streamUrl, vw, vh, timestamp) {
-  var stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp);
+  let stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp);
   return stadrbem.toJson();
 }
 
 Messaging.prototype.generateDeskShareRTMPBroadcastStoppedEvent =
   function(conferenceName, streamUrl, vw, vh, timestamp) {
-  var stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp);
+  let stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp);
   return stodrbem.toJson();
 }
 
 Messaging.prototype.generateScreenshareRTMPBroadcastStartedEvent2x =
   function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) {
-  var stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp);
+  let stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp);
   return stadrbem.toJson();
 }
 
 Messaging.prototype.generateScreenshareRTMPBroadcastStoppedEvent2x =
   function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) {
-  var stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp);
+  let stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp);
   return stodrbem.toJson();
 }
 
 module.exports = new Messaging();
-module.exports.Constants = Constants;
diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js
diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js
similarity index 100%
rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js
diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js
new file mode 100644
index 0000000000000000000000000000000000000000..94acc7db4eeea467c5133a3aaabbb92603f5373a
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js
@@ -0,0 +1,129 @@
+/**
+ * @classdesc
+ * Redis wrapper class for connecting to Redis channels
+ */
+
+'use strict';
+
+/* Modules */
+
+const redis = require('redis');
+const config = require('config');
+const Constants = require('../messages/Constants.js');
+const EventEmitter = require('events').EventEmitter;
+
+/* Public members */
+
+module.exports = class RedisWrapper extends EventEmitter {
+  constructor(subpattern) {
+    super();
+    // Redis PubSub client holders
+    this.redisCli = null;
+    this.redisPub = null;
+    // Pub and Sub channels/patterns
+    this.subpattern = subpattern;
+  }
+
+  static get _retryThreshold() {
+    return 1000 * 60 * 60;
+  }
+
+  static get _maxRetries() {
+    return 10;
+  }
+
+  startPublisher () {
+    var options = {
+      host : config.get('redisHost'),
+      port : config.get('redisPort'),
+      //password: config.get('redis.password')
+      retry_strategy: this._redisRetry
+    };
+
+    this.redisPub = redis.createClient(options);
+  }
+
+  startSubscriber () {
+    let self = this;
+    if (this.redisCli) {
+      console.log("  [RedisWrapper] Redis Client already exists");
+      return;
+    }
+
+    var options = {
+      host : config.get('redisHost'),
+      port : config.get('redisPort'),
+      //password: config.get('redis.password')
+      retry_strategy: this._redisRetry
+    };
+
+    this.redisCli = redis.createClient(options);
+
+    console.log("  [RedisWrapper] Trying to subscribe to redis channel");
+
+    this.redisCli.on("connect", () => {
+      // console.log(" [RedisWrapper] Connected to Redis Server.");
+      // DO SOMETHING
+    });
+
+    this.redisCli.on("error", (e) => {
+      console.error(" [RedisWrapper] " + e);
+    });
+
+    this.redisCli.on("reconnecting", (e) => {
+      // DO SOMETHING
+    });
+
+    this.redisCli.on("psubscribe", (channel, count) => {
+      console.log(" [RedisWrapper] Successfully subscribed to pattern [" + channel + "]");
+    });
+
+    this.redisCli.on("pmessage", this._onMessage.bind(this));
+
+    if (!this.subpattern) {
+      throw new Error("[RedisWrapper] No subscriber pattern");
+    }
+
+    this.redisCli.psubscribe(this.subpattern);
+
+    console.log("  [RedisWrapper] Started Redis client at " + options.host + ":" + options.port +
+        " for subscription pattern: " + this.subpattern);
+
+    return ;
+  }
+
+  stopRedis (callback) {
+    if (this.redisCli){
+      this.redisCli.quit();
+    }
+    callback(false);
+  }
+
+  publishToChannel (_message, channel) {
+    let message = _message;
+    if(this.redisPub) {
+      this.redisPub.publish(channel, message);
+    }
+  }
+
+  /* Private members */
+
+  _onMessage (pattern, channel, _message) {
+    let message = (typeof _message !== 'object')?JSON.parse(_message):_message;
+    // use event emitter to throw new message
+    this.emit(Constants.REDIS_MESSAGE, message);
+  }
+
+  static _redisRetry (options) {
+    // if (options.error && options.error.code === 'ECONNREFUSED') {
+    //   return new Error('The server refused the connection');
+    // }
+    if (options.total_retry_time > RedisWrapper._retryThreshold) {
+      return new Error('Retry time exhausted');
+    }
+    if (options.times_connected > RedisWrapper._maxRetries) {
+      return undefined;
+    }
+    return Math.max(options.attempt * 100, 3000);
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
new file mode 100644
index 0000000000000000000000000000000000000000..b99b9b855539ba1b7f3d600da522b7e89308f372
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
@@ -0,0 +1,117 @@
+/**
+ * @classdesc
+ * BigBlueButton redis gateway for bbb-screenshare node app
+ */
+
+'use strict';
+
+/* Modules */
+
+const C = require('../messages/Constants.js');
+const RedisWrapper = require('./RedisWrapper.js');
+const config = require('config');
+const util = require('util');
+const EventEmitter = require('events').EventEmitter;
+
+let instance = null;
+
+module.exports = class BigBlueButtonGW extends EventEmitter {
+  constructor() {
+    if(!instance){
+      super();
+      this.subscribers = {};
+      this.publisher = null;
+      instance = this;
+    }
+
+    return instance;
+  }
+
+  addSubscribeChannel (channel) {
+    if (this.subscribers[channel]) {
+      return this.subscribers[channel];
+    }
+
+    let wrobj = new RedisWrapper(channel);
+    this.subscribers[channel] = {};
+    this.subscribers[channel] = wrobj;
+    try {
+      wrobj.startSubscriber();
+      wrobj.on(C.REDIS_MESSAGE, this.incomingMessage.bind(this));
+      console.log("  [BigBlueButtonGW] Added redis client to this.subscribers[" + channel + "]");
+      return Promise.resolve(wrobj);
+    }
+    catch (error) {
+      return Promise.reject("  [BigBlueButtonGW] Could not start redis client for channel " + channel);
+    }
+  }
+
+  /**
+   * Capture messages from subscribed channels and emit an event with it's
+   * identifier and payload. Check Constants.js for the identifiers.
+   *
+   * @param {Object} message  Redis message
+   */
+  incomingMessage (message) {
+    let header;
+    let payload;
+    let msg = (typeof message !== 'object')?JSON.parse(message):message;
+
+    // Trying to parse both message types, 1x and 2x
+    if (msg.header) {
+      header = msg.header;
+      payload = msg.payload;
+    }
+    else if (msg.core) {
+      header = msg.core.header;
+      payload = msg.core.body;
+    }
+
+    if (header){
+      switch (header.name) {
+        // interoperability with 1.1
+        case C.START_TRANSCODER_REPLY:
+          this.emit(C.START_TRANSCODER_REPLY, payload);
+          break;
+        case C.STOP_TRANSCODER_REPLY:
+          this.emit(C.STOP_TRANSCODER_REPLY, payload);
+          break;
+          // 2x messages
+        case C.START_TRANSCODER_RESP_2x:
+          payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x];
+          this.emit(C.START_TRANSCODER_RESP_2x, payload);
+          break;
+        case C.STOP_TRANSCODER_RESP_2x:
+          payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x];
+          this.emit(C.STOP_TRANSCODER_RESP_2x, payload);
+          break;
+
+        default:
+          this.emit(C.GATEWAY_MESSAGE, msg);
+      }
+    }
+    else {
+      this.emit(C.GATEWAY_MESSAGE, msg);
+    }
+  }
+
+  publish (message, channel) {
+    if (!this.publisher) {
+      this.publisher = new RedisWrapper();
+      this.publisher.startPublisher();
+    }
+
+    if (typeof this.publisher.publishToChannel === 'function') {
+      this.publisher.publishToChannel(message, channel);
+    }
+  }
+
+  setEventEmitter (emitter) {
+    this.emitter = emitter;
+  }
+
+  _onServerResponse(data) {
+    // Here this is the 'ws' instance
+    this.sendMessage(data);
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..653aada87fa2f5483643880c0e3fc6226ccccf80
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js
@@ -0,0 +1,101 @@
+/*
+ * Lucas Fialho Zawacki
+ * Paulo Renato Lanzarin
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+'use strict';
+
+// const express = require('express');
+// const session = require('express-session')
+// const wsModule = require('./websocket');
+
+const http = require('http');
+const fs = require('fs');
+const EventEmitter = require('events');
+const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw');
+const C = require('../bbb/messages/Constants');
+
+// Global variables
+module.exports = class ConnectionManager {
+
+  constructor (settings, logger) {
+    this._logger = logger;
+    this._screenshareSessions = {};
+
+    this._setupBBB();
+
+    this._emitter = this._setupEventEmitter();
+    this._adapters = [];
+  }
+
+  setHttpServer(httpServer) {
+    this.httpServer = httpServer;
+  }
+
+  listen(callback) {
+    this.httpServer.listen(callback);
+  }
+
+  addAdapter(adapter) {
+    adapter.setEventEmitter(this._emitter);
+    this._adapters.push(adapter);
+  }
+
+  _setupEventEmitter() {
+    let self = this;
+    let emitter = new EventEmitter();
+
+    emitter.on(C.WEBSOCKET_MESSAGE, (data) => {
+      switch (data.type) {
+        case "screenshare":
+          self._bbbGW.publish(JSON.stringify(data), C.TO_SCREENSHARE);
+          break;
+
+        case "video":
+          self._bbbGW.publish(JSON.stringify(data), C.TO_VIDEO);
+          break;
+
+        case "audio":
+          self._bbbGW.publish(JSON.stringify(data), C.TO_AUDIO);
+          break;
+
+        case "default":
+          // TODO handle API error message;
+      }
+    });
+
+    return emitter;
+  }
+
+  async _setupBBB() {
+    this._bbbGW = new BigBlueButtonGW();
+
+    try {
+      const screenshare = await this._bbbGW.addSubscribeChannel(C.FROM_SCREENSHARE);
+      const video = await this._bbbGW.addSubscribeChannel(C.FROM_VIDEO);
+      const audio = await this._bbbGW.addSubscribeChannel(C.FROM_AUDIO);
+
+      screenshare.on(C.REDIS_MESSAGE, (data) => {
+        this._emitter.emit('response', data);
+      });
+
+      video.on(C.REDIS_MESSAGE, (data) => {
+        this._emitter.emit('response', data);
+      });
+
+      console.log('  [ConnectionManager] Successfully subscribed to processes redis channels');
+    }
+    catch (err) {
+      console.log('  [ConnectionManager] ' + err);
+      this._stopAll;
+    }
+  }
+
+  _stopSession(sessionId) {
+  }
+
+  _stopAll() {
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ec5a30fac3abeb8eda002809665d17662757929
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const http = require("http");
+const fs = require("fs");
+const config = require('config');
+
+module.exports = class HttpServer {
+
+  constructor() {
+    //const privateKey  = fs.readFileSync('sslcert/server.key', 'utf8');
+    //const certificate = fs.readFileSync('sslcert/server.crt', 'utf8');
+    //const credentials = {key: privateKey, cert: certificate};
+
+    this.port = config.get('clientPort');
+
+    this.server = http.createServer((req,res) => {
+      //
+    });
+  }
+
+  getServerObject() {
+    return this.server;
+  }
+
+  listen(callback) {
+    console.log(' [HttpServer] Listening in port ' + this.port);
+    this.server.listen(this.port, callback);
+  }
+
+}
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js
new file mode 100644
index 0000000000000000000000000000000000000000..84022e268838c571893d3bf0c2b6f5d092584dbf
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js
@@ -0,0 +1,81 @@
+const Joi = require('joi');
+
+let instance = null;
+
+module.exports = class MessageParser {
+  constructor() {
+    if(!instance){
+      instance = this;
+    }
+    return instance;
+  }
+
+  static const schema {
+    startScreenshare: Joi.object().keys({
+      sdpOffer : Joi.string().required(),
+      vh: Joi.number().required(),
+      vw: Joi.number().required()
+    }),
+
+    startVideo: Joi.object().keys({
+      internalMeetingId: joi.string().required(),
+      callerName : Joi.string().required(),
+    }),
+
+    startAudio: Joi.object().keys({
+      internalMeetingId: joi.string().required(),
+      callerName : Joi.string().required(),
+    }),
+
+    playStart: Joi.object().keys({
+    }),
+
+    playStop: Joi.object().keys.({
+    }),
+
+    stop: Joi.object().keys({
+    }),
+
+    onIceCandidate: Joi.object().keys({
+      internalMeetingId: joi.string().required(),
+      candidate: Joi.object().required(),
+    }),
+  }
+
+  static const messageTemplate Joi.object().keys({
+    id: Joi.string().required(),
+    type: joi.string().required(),
+    role: joi.string().required(),
+  })
+
+  static const validateMessage (msg) {
+    let res = Joi.validate(msg, messageTemplate, {allowUnknown: true});
+
+    if (!res.error) {
+      res = Joi.validate(msg, schema[msg.id]);
+    }
+
+    return res;
+  }
+
+  _parse (message) {
+    let parsed = { id: '' };
+
+    try {
+      parsed = JSON.parse(message);
+    } catch (e) {
+      console.error(e);
+    }
+
+    let res = validateMessage(parsed);
+
+    if (res.error) {
+      parsed.validMessage = false;
+      parsed.errors = res.error;
+    } else {
+      parsed.validMessage = true;
+    }
+
+    return parsed;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..6c109baf75ac71b9c54a12f1ccca3f988a46ff90
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js
@@ -0,0 +1,34 @@
+'use strict';
+
+// incomplete
+
+module.exports = class RedisConnectionManager {
+
+  constructor(options) {
+
+    this._client = redis.createClient({options});
+    this._pubchannel = options.pubchannel;
+    this._subchannel = optiosn.subchannel;
+
+    if (options.pubchannel) {
+      this._client.on()
+    }
+
+    if (options.subchannel) {
+      this._client.on()
+    }
+
+    this._client.on()
+    // pub
+
+  }
+
+  setEventEmitter(emitter) {
+    this.emitter = emitter;
+  }
+
+  _onMessage() {
+
+  }
+
+}
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..17ea6c3a1d0b672b48600fec053361eeb056bdc7
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
@@ -0,0 +1,170 @@
+'use strict';
+
+const ws = require('ws');
+const C = require('../bbb/messages/Constants');
+
+// initialization
+let connectionIDCounter = 0;
+
+// when handling a new connection
+
+
+ws.prototype.setErrorCallback = function(callback) {
+
+  this._errorCallback = callback;
+};
+
+ws.prototype.sendMessage = function(json) {
+
+  let websocket = this;
+
+  if (this._closeCode === 1000) {
+    console.log("  [WebsocketConnectionManager] Websocket closed, not sending");
+    this._errorCallback("Error: not opened");
+  }
+
+  return this.send(JSON.stringify(json), function(error) {
+    if(error) {
+      console.log('  [WebsocketConnectionManager] server: Websocket error "' + error + '" on message "' + json.id + '"');
+
+      websocket._errorCallback(error);
+    }
+  });
+
+};
+
+module.exports = class WebsocketConnectionManager {
+  constructor (server, path) {
+    this.wss = new ws.Server({
+      server,
+      path
+    });
+
+    this.wss.on ('connection', (ws) => {
+      let self = this;
+
+      ws.id = connectionIDCounter++;
+
+      console.log(" [WebsocketConnectionManager] New connection with id [ " + ws.id + " ]");
+
+      ws.on('message', (data) => {
+        let message = {};
+
+        try {
+          message = JSON.parse(data);
+          message.connectionId = ws.id;
+
+          if (!ws.sessionId) {
+            ws.sessionId = message.voiceBridge;
+          }
+
+          if (!ws.route) {
+            ws.route = message.type;
+          }
+
+          if (!ws.role) {
+            ws.role = message.role;
+          }
+        } catch(e) {
+          console.error("  [WebsocketConnectionManager] JSON message parse error " + e);
+          message = {};
+        }
+
+        // Test for empty or invalid JSON
+        if (Object.getOwnPropertyNames(message).length !== 0) {
+          this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+        }
+      });
+
+      //ws.on('message', this._onMessage.bind(this));
+      ws.setErrorCallback(this._onError.bind(this));
+
+      ws.on('close', (ev) => {
+        console.log('  [WebsocketConnectionManager] Closed connection on [' + ws.id + ']');
+        let message = {
+          id: 'close',
+          type: ws.route,
+          role: ws.role,
+          voiceBridge: ws.sessionId,
+          connectionId: ws.id
+        }
+
+        this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+
+        ws = null;
+      });
+
+      ws.on('error', (err) => {
+        console.log('  [WebsocketConnectionManager] Connection error [' + ws.id + ']');
+        let message = {
+          id: 'error',
+          type: ws.route,
+          role: ws.role,
+          voiceBridge: ws.sessionId,
+          connectionId: ws.id
+        }
+
+        this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+
+        ws = null;
+      });
+
+      // TODO: should we delete this listener after websocket dies?
+      this.emitter.on('response', (data) => {
+        if (ws && ws.id == data.connectionId) {
+          ws.sendMessage(data);
+        }
+      });
+    });
+  }
+
+  setEventEmitter (emitter) {
+    this.emitter = emitter;
+  }
+
+  _onServerResponse (data) {
+    // Here this is the 'ws' instance
+    this.sendMessage(data);
+  }
+
+  _onMessage (data) {
+
+    let message = {};
+
+    try {
+      message = JSON.parse(data);
+    } catch(e) {
+      console.error("  [WebsocketConnectionManager] JSON message parse error " + e);
+      message = {};
+    }
+
+    // Test for empty or invalid JSON
+    if (Object.getOwnPropertyNames(message).length !== 0) {
+      this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+    }
+  }
+
+  _onError (err) {
+    console.log('  [WebsocketConnectionManager] Connection error');
+    let message = {
+      id: 'error',
+      voiceBridge: ws.sessionId,
+      connectionId: ws.id
+    }
+    this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+  }
+
+  _onClose (err) {
+    console.log('  [WebsocketConnectionManager] Closed connection [' + this.id + ']');
+    let message = {
+      id: 'close',
+      voiceBridge: this.sessionId,
+      connectionId: this.id
+    }
+
+    this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+  }
+
+  _stop () {
+  }
+}
diff --git a/labs/kurento-screenshare/lib/h264-sdp.js b/labs/bbb-webrtc-sfu/lib/h264-sdp.js
similarity index 100%
rename from labs/kurento-screenshare/lib/h264-sdp.js
rename to labs/bbb-webrtc-sfu/lib/h264-sdp.js
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..ee03958e42a0d3d54b74e7c76a2a1fef19234b7a
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js
@@ -0,0 +1,12 @@
+const MCSApiStub = require('./media/MCSApiStub');
+
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+});
+
+process.on('disconnect',function() {
+  console.log("Parent exited!");
+  process.kill();
+});
+
+core = new MCSApiStub();
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..94f7acc9dc9f89aa14094232af1bb300e47468e7
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js
@@ -0,0 +1,86 @@
+/*
+ * (C) Copyright 2016 Mconf Tecnologia (http://mconf.com/)
+ */
+
+/**
+ * @classdesc
+ * Message constants for the communication with BigBlueButton
+ * @constructor
+ */
+
+'use strict'
+
+exports.ALL = 'ALL'
+
+exports.LOG_LEVEL = {}
+exports.LOG_LEVEL.DEBUG = 0
+exports.LOG_LEVEL.INFO = 1
+exports.LOG_LEVEL.WARN = 2
+exports.LOG_LEVEL.ERROR = 3
+exports.LOG_LEVEL.OFF = 100
+
+exports.STATUS = {}
+exports.STATUS.STARTED = "STARTED"
+exports.STATUS.STOPPED = "STOPPED"
+exports.STATUS.RUNNING = "RUNNING'"
+exports.STATUS.STARTING = "STARTING"
+exports.STATUS.STOPPING = "STOPPING"
+exports.STATUS.RESTARTING = "RESTARTING"
+
+exports.USERS = {}
+exports.USERS.SFU = "SFU"
+exports.USERS.MCU = "MCU"
+
+exports.MEDIA_TYPE = {}
+exports.MEDIA_TYPE.WEBRTC = "WebRtcEndpoint"
+exports.MEDIA_TYPE.RTP= "RtpEndpoint"
+exports.MEDIA_TYPE.URI = "PlayerEndpoint"
+
+// Observer Constants
+exports.EVENT = {}
+exports.EVENT.DIAL_EVENT = "BRIDGE_DIAL"
+exports.EVENT.HANGUP_EVENT = "BRIDGE_HANGUP"
+exports.EVENT.SESSION_ID_EVENT = "SESSION_ID"
+exports.EVENT.AUDIO_SESSION_TERMINATED = "AUDIO_SESSION_TERMINATED"
+
+// Media server state changes 
+exports.EVENT.NEW_SESSION = "NewSession"
+exports.EVENT.MEDIA_STATE = {};
+exports.EVENT.MEDIA_STATE.MEDIA_EVENT = "MediaEvent"
+exports.EVENT.MEDIA_STATE.CHANGED = "MediaStateChanged"
+exports.EVENT.MEDIA_STATE.FLOW_OUT = "MediaFlowOutStateChange"
+exports.EVENT.MEDIA_STATE.FLOW_IN = "MediaFlowInStateChange"
+exports.EVENT.MEDIA_STATE.ENDOFSTREAM = "EndOfStream"
+exports.EVENT.MEDIA_STATE.ICE = "OnIceCandidate"
+
+
+
+// RTP params
+exports.SDP = {};
+exports.SDP.PARAMS = "params"
+exports.SDP.MEDIA_DESCRIPTION = "media_description"
+exports.SDP.LOCAL_IP_ADDRESS = "local_ip_address"
+exports.SDP.LOCAL_VIDEO_PORT = "local_video_port"
+exports.SDP.DESTINATION_IP_ADDRESS = "destination_ip_address"
+exports.SDP.DESTINATION_VIDEO_PORT = "destination_video_port"
+exports.SDP.REMOTE_VIDEO_PORT = "remote_video_port"
+exports.SDP.CODEC_NAME = "codec_name"
+exports.SDP.CODEC_ID = "codec_id"
+exports.SDP.CODEC_RATE = "codec_rate"
+exports.SDP.RTP_PROFILE = "rtp_profile"
+exports.SDP.SEND_RECEIVE = "send_receive"
+exports.SDP.FRAME_RATE = "frame_rate"
+
+// Strings
+exports.STRING = {}
+exports.STRING.ANONYMOUS = "ANONYMOUS"
+exports.STRING.FS_USER_AGENT_STRING = "Freeswitch_User_Agent"
+exports.STRING.XML_MEDIA_FAST_UPDATE = '<?xml version=\"1.0\" encoding=\"utf-8\" ?>' +
+                                          '<media_control>' +
+                                            '<vc_primitive>' +
+                                              '<to_encoder>' +
+                                                '<picture_fast_update>' +
+                                                '</picture_fast_update>' +
+                                              '</to_encoder>' +
+                                            '</vc_primitive>' +
+                                          '</media_control>'
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c42ec7efe33f34cbba1536c409fd0943408d4c3
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js
@@ -0,0 +1,143 @@
+'use strict'
+
+var config = require('config');
+var C = require('../constants/Constants');
+// EventEmitter
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+var MediaController = require('./MediaController.js');
+
+let instance = null;
+
+module.exports = class MCSApiStub extends EventEmitter{
+  constructor() {
+    if(!instance) {
+      super();
+      this.listener = new EventEmitter();
+      this._mediaController = new MediaController(this.listener);
+      instance = this;
+    }
+
+    return instance;
+  }
+
+  async join (room, type, params) {
+    try {
+      const answer = await this._mediaController.join(room, type, params);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      Promise.reject(err);
+    }
+  }
+
+  async leave (roomId, userId) {
+    try {
+      const answer = await this._mediaController.leave(roomId, userId);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async publishnsubscribe (user, sourceId, sdp, params) {
+    try {
+      const answer = await this._mediaController.publishnsubscribe(user, sourceId, sdp, params);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async publish (user, room,  type, params) {
+    try {
+      this.listener.once(C.EVENT.NEW_SESSION+user, (event) => {
+        let sessionId = event;
+        this.listener.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, (event) => {
+          this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, event);
+        });
+      });
+      const answer = await this._mediaController.publish(user, room, type, params);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+  
+  async unpublish (user, mediaId) {
+    try {
+      await this._mediaController.unpublish(mediaId);
+      return Promise.resolve();
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async subscribe (user, sourceId, type, params) {
+    try {
+      this.listener.once(C.EVENT.NEW_SESSION+user, (event) => {
+        let sessionId = event;
+        this.listener.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, (event) => {
+          this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, event);
+        });
+      });
+
+      const answer = await this._mediaController.subscribe(user, sourceId, type, params);
+
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async unsubscribe (user, mediaId) {
+    try {
+      await this._mediaController.unsubscribe(user, mediaId);
+      return Promise.resolve();
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async onEvent (eventName, mediaId) {
+    try {
+      const eventTag = this._mediaController.onEvent(eventName, mediaId);
+      this._mediaController.on(eventTag, (event) => {
+        this.emit(eventTag, event);
+      });
+
+      return Promise.resolve(eventTag);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject();
+    }
+  }
+
+  async addIceCandidate (mediaId, candidate) {
+    try {
+      const ack = await this._mediaController.addIceCandidate(mediaId, candidate);
+      return Promise.resolve(ack);
+    }
+    catch (err) {
+      console.log(err);
+      Promise.reject();
+    }
+  }
+  setStrategy (strategy) {
+    // TODO
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js
new file mode 100644
index 0000000000000000000000000000000000000000..b446aa2d8ebd1433568a10a934dad0f7988dc92f
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js
@@ -0,0 +1,360 @@
+'use strict'
+
+const config = require('config');
+const C = require('../constants/Constants');
+
+// Model
+const SfuUser = require('../model/SfuUser');
+const Room = require('../model/Room.js');
+
+const EventEmitter = require('events').EventEmitter;
+
+/* PRIVATE ELEMENTS */
+/**
+ * Deep copy a javascript Object
+ * @param  {Object} object The object to be copied
+ * @return {Object}        A deep copy of the given object
+ */
+function copy(object) {
+  return JSON.parse(JSON.stringify(object));
+}
+
+function getPort(min_port, max_port) {
+  return Math.floor((Math.random()*(max_port - min_port +1)+ min_port));
+}
+
+function getVideoPort() {
+  return getPort(config.get('sip.min_video_port'), config.get('sip.max_video_port'));
+}
+
+/* PUBLIC ELEMENTS */
+
+let instance = null;
+
+
+module.exports = class MediaController {
+  constructor(emitter) {
+    if (!instance) {
+      this.emitter = emitter;
+      this._rooms = {};
+      this._users = {};
+      this._mediaSessions = {};
+      instance = this;
+    }
+
+    return instance;
+  }
+
+  start (_kurentoClient, _kurentoToken, callback) {
+    var self = this;
+    return callback(null);
+  }
+
+  stop (callback) {
+    var self = this;
+    self.stopAllMedias(function (e) {
+      if (e) {
+        callback(e);
+      }
+      self._rooms = {};
+    });
+  }
+
+  getVideoPort () {
+    return getPort(config.get('sip.min_video_port'), config.get('sip.max_video_port'));
+  }
+
+  getRoom (roomId) {
+    return this._rooms[roomId];
+  }
+
+  async join (roomId, type, params) {
+    console.log("[mcs] Join room => " + roomId + ' as ' + type);
+    try {
+      let session;
+      const room = await this.createRoomMCS(roomId);
+      this._rooms[roomId] = room;
+      const user = await this.createUserMCS(roomId, type, params);
+      room.setUser(user.id);
+      this._users[user.id] = user;
+      if (params.sdp) {
+        session = user.addSdp(params.sdp);
+      }
+      if (params.uri) {
+        session = user.addUri(params.sdp);
+      }
+
+      console.log("[mcs] Resolving user " + user.id);
+      return Promise.resolve(user.id);
+    }
+    catch (err) {
+      console.log("[mcs] JOIN ERROR " + err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async leave (roomId, userId) {
+    try {
+      console.log("  [mcs] User => " + userId + " wants to leave ");
+      const room = this.getRoom(roomId);
+      const user = this.getUserMCS(userId);
+
+      if (!user || !room) {
+        return Promise.resolve();
+      }
+
+      const killedSessions = await user.leave();
+
+      for (var session in killedSessions) {
+        this._mediaSessions[killedSessions[session]] = null;
+      }
+
+      room.destroyUser(user.id);
+      this._users[user.id] = null;
+
+
+      return Promise.resolve();
+    }
+    catch (err) {
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async publishnsubscribe (userId, sourceId, sdp, params) {
+    console.log("[mcs] pns");
+    let type = params.type;
+    try {
+      user = this.getUserMCS(userId);
+      let userId = user.id;
+      let session = user.addSdp(sdp, type);
+      let sessionId = session.id;
+
+      if (typeof this._mediaSessions[session.id] == 'undefined' || 
+          !this._mediaSessions[session.id]) {
+        this._mediaSessions[session.id] = {};
+      }
+
+      this._mediaSessions[session.id] = session; 
+
+      const answer = await user.startSession(session.id);
+      await user.connect(sourceId, session.id);
+
+      console.log("[mcs] user with sdp session " + session.id);
+      return Promise.resolve({userId, sessionId});
+    }
+    catch (err) {
+      console.log("[mcs] PUBLISHNSUBSCRIBE ERROR " + err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async publish (userId, roomId, type, params) {
+    console.log("[mcs] publish");
+    let session;
+    // TODO handle mediaType
+    let mediaType = params.mediaType;
+    let answer;
+
+    try {
+      console.log("  [mcs] Fetching user => " + userId);
+
+      const user = await this.getUserMCS(userId);
+
+      console.log("  [mcs] Fetched user => " + user);
+
+      switch (type) {
+        case "RtpEndpoint":
+        case "WebRtcEndpoint":
+          session = user.addSdp(params.descriptor, type);
+          session.on('SESSION_STOPPED', (pubId) => {
+            console.log("  [mcs] SESSION ", session.id, " STOPPED ");
+            if(pubId === session.id) {
+              for (var sub in session.subscribedSessions) {
+                console.log("  [mcs] Unsubscribing session ", sub);
+                let subSession = this._mediaSessions[sub];
+                if (subSession) {
+                  subSession.stop();
+                  this._mediaSessions[sub] = null;
+                }
+              }
+            }
+          });
+
+          answer = await user.startSession(session.id);
+          break;
+        case "URI":
+          session = user.addUri(params.descriptor, type);
+
+          answer = await user.startSession(session.id);
+          break;
+
+        default: return Promise.reject(new Error("[mcs] Invalid media type"));
+      }
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+
+    if (typeof this._mediaSessions[session.id] == 'undefined' ||
+        !this._mediaSessions[session.id]) {
+      this._mediaSessions[session.id] = {};
+    }
+
+    this._mediaSessions[session.id] = session;
+    let sessionId = session.id;
+
+    return Promise.resolve({answer, sessionId});
+  }
+
+  async subscribe (userId, sourceId, type, params) {
+    console.log("  [mcs] subscribe");
+
+    let session;
+    // TODO handle mediaType
+    let mediaType = params.mediaType;
+    let answer;
+    let sourceSession = this._mediaSessions[sourceId];
+
+    if (typeof sourceSession === 'undefined') {
+      return Promise.reject(new Error("  [mcs] Media session " + sourceId + " was not found"));
+    }
+
+    try {
+      console.log("  [mcs] Fetching user => " + userId);
+
+      const user = await this.getUserMCS(userId);
+
+      console.log("  [mcs] Fetched user => " + user);
+
+      switch (type) {
+        case "RtpEndpoint":
+        case "WebRtcEndpoint":
+          session = user.addSdp(params.descriptor, type);
+
+          answer = await user.startSession(session.id);
+          await sourceSession.connect(session._mediaElement);
+          sourceSession.subscribedSessions.push(session.id);
+          console.log("  [mcs] ", sourceSession.id,  " subscribers list ", sourceSession.subscribedSessions);
+          break;
+        case "URI":
+          session = user.addUri(params.descriptor, type);
+          answer = await user.startSession(session.id);
+          await sourceSession.connect(session._mediaElement);
+
+          break;
+
+        default: return Promise.reject(new Error("[mcs] Invalid media type"));
+      }
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+
+    if (typeof this._mediaSessions[session.id] == 'undefined' ||
+        !this._mediaSessions[session.id]) {
+      this._mediaSessions[session.id] = {};
+    }
+
+    this._mediaSessions[session.id] = session;
+    let sessionId = session.id;
+
+    return Promise.resolve({answer, sessionId});
+  }
+
+  async unpublish (userId, mediaId) {
+    try {
+      const session = this._mediaSessions[mediaId];
+      const user = this.getUserMCS(userId);
+
+      if(typeof session === 'undefined' || !session) {
+        return Promise.resolve();
+      }
+
+
+      const answer = await user.unpublish(mediaId);
+      this._mediaSessions[mediaId] = null;
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async unsubscribe (userId, mediaId) {
+    try {
+      const user = this.getUserMCS(userId);
+      if (user) {
+        const answer = await user.unsubscribe(mediaId);
+        this._mediaSessions[mediaId] = null;
+      }
+      return Promise.resolve();
+    }
+    catch (err) {
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async addIceCandidate (mediaId, candidate) {
+    let session = this._mediaSessions[mediaId];
+    if (typeof session === 'undefined') {
+      return Promise.reject(new Error("  [mcs] Media session " + mediaId + " was not found"));
+    }
+    try {
+      const ack = await session.addIceCandidate(candidate);
+      return Promise.resolve(ack);
+    }
+    catch (err) {
+      console.log(err);
+      return Promise.reject(err);
+    }
+  }
+
+  /**
+   * Creates an empty {Room} room and indexes it
+   * @param {String} roomId
+   */
+  async createRoomMCS (roomId)  {
+    let self = this;
+
+    console.log("  [media] Creating new room with ID " + roomId);
+
+    if(!self._rooms[roomId]) {
+      self._rooms[roomId] = new Room(roomId);
+    }
+
+    return Promise.resolve(self._rooms[roomId]);
+  }
+
+  /**
+   * Creates an {User} of type @type
+   * @param {String} roomId
+   */
+  createUserMCS (roomId, type, params)  {
+    let self = this;
+    let user;
+    console.log("  [media] Creating a new user[" + type + "]");
+
+    switch (type) {
+      case C.USERS.SFU:
+        user  = new SfuUser(roomId, type, this.emitter, params.userAgentString, params.sdp);
+        break;
+      case C.USERS.MCU:
+        console.log("  [media] createUserMCS MCU TODO");
+        break;
+      default:
+        console.log("  [controller] Unrecognized user type");
+    }
+
+    if(!self._users[user.id]) {
+      self._users[user.id] = user;
+    }
+
+    return Promise.resolve(user);
+  }
+
+  getUserMCS (userId) {
+    return this._users[userId];
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js
new file mode 100644
index 0000000000000000000000000000000000000000..6693beb97e4df4f160bc1b3beee78edb17bf532c
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js
@@ -0,0 +1,272 @@
+'use strict'
+
+const C = require('../constants/Constants.js');
+const config = require('config');
+const mediaServerClient = require('kurento-client');
+const util = require('util');
+const EventEmitter = require('events').EventEmitter;
+
+let instance = null;
+
+/* Public members */
+module.exports = class MediaServer extends EventEmitter {
+  constructor(serverUri) {
+    if(!instance){
+      super();
+      this._serverUri = serverUri;
+      this._mediaPipelines = {};
+      this._mediaElements= {};
+      this._mediaServer;
+      instance = this;
+    }
+
+    return instance;
+  }
+
+  async init () {
+    if (typeof this._mediaServer === 'undefined' || !this._mediaServer) {
+      this._mediaServer = await this._getMediaServerClient(this._serverUri);
+    }
+  }
+
+  _getMediaServerClient (serverUri) {
+    return new Promise((resolve, reject) =>  {
+      mediaServerClient(serverUri, (error, client) => {
+        if (error) {
+          reject(error);
+        }
+        console.log("  [media] Retrieved media server client => " + client);
+        resolve(client);
+      });
+    });
+  }
+
+  _getMediaPipeline (conference) {
+    return new Promise((resolve, reject) => {
+      if (this._mediaPipelines[conference]) {
+        console.log(' [media] Pipeline already exists. ' + JSON.stringify(this._mediaPipelines, null, 2));
+        resolve(this._mediaPipelines[conference]);
+      }
+      else {
+        this._mediaServer.create('MediaPipeline', (error, pipeline) => {
+          if (error) {
+            console.log(error);
+            reject(error);
+          }
+          this._mediaPipelines[conference] = pipeline;
+          resolve(pipeline);
+        });
+      }
+    });
+  }
+
+  _releasePipeline (pipelineId) {
+    let mediaPipeline = this._mediaPipelines[pipelineId];
+
+    if (typeof mediaPipeline !== 'undefined' && typeof mediaPipeline.release === 'function') {
+      mediaElement.release();
+    }
+  }
+
+  _createElement (pipeline, type) {
+    return new Promise((resolve, reject) => {
+      pipeline.create(type, (error, mediaElement) => {
+        if (error) {
+          return reject(error);
+        }
+        console.log("  [MediaController] Created [" + type + "] media element: " + mediaElement.id);
+        this._mediaElements[mediaElement.id] = mediaElement;
+        return resolve(mediaElement);
+      });
+    });
+  }
+
+
+  async createMediaElement (conference, type) {
+    try {
+      const pipeline = await this._getMediaPipeline(conference);
+      const mediaElement = await this._createElement(pipeline, type);
+      return Promise.resolve(mediaElement.id);
+    }
+    catch (err) {
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async connect (sourceId, sinkId, type) {
+    let source = this._mediaElements[sourceId];
+    let sink = this._mediaElements[sinkId];
+
+    if (source && sink) {
+      return new Promise((resolve, reject) => {
+        switch (type) {
+          case 'ALL': 
+            source.connect(sink, (error) => {
+              if (error) {
+                return reject(error);
+              }
+              return resolve();
+            });
+            break;
+
+
+          case 'AUDIO':
+          case 'VIDEO':
+            source.connect(sink, (error) => {
+              if (error) {
+                return reject(error);
+              }
+              return resolve();
+            });
+            break;
+
+          default: return reject("  [media] Invalid connect type");
+        }
+      });
+    }
+    else {
+      return Promise.reject("  [media] Failed to connect " + type + ": " + sourceId + " to " + sinkId);
+    }
+  }
+
+  stop (elementId) {
+    let mediaElement = this._mediaElements[elementId];
+    if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') {
+      console.log("  [media] Releasing endpoint => " + elementId);
+      mediaElement.release();
+      this._mediaElements[elementId] = null;
+    }
+  }
+
+  
+  addIceCandidate (elementId, candidate) {
+    let mediaElement = this._mediaElements[elementId];
+    let kurentoCandidate = mediaServerClient.getComplexType('IceCandidate')(candidate);
+
+    if (typeof mediaElement !== 'undefined' && typeof mediaElement.addIceCandidate === 'function' &&
+        typeof candidate !== 'undefined') {
+      mediaElement.addIceCandidate(candidate);
+      console.log("  [media] Added ICE candidate for => " + elementId);
+      return Promise.resolve();
+    }
+    else {
+      return Promise.reject(new Error("Candidate could not be parsed or media element does not exist"));
+    }
+  }
+
+  gatherCandidates (elementId) {
+    console.log('  [media] Gathering ICE candidates for ' + elementId);
+    let mediaElement = this._mediaElements[elementId];
+
+    return new Promise((resolve, reject) => {
+      if (typeof mediaElement !== 'undefined' && typeof mediaElement.gatherCandidates === 'function') {
+        mediaElement.gatherCandidates((error) => {
+          if (error) {
+            return reject(new Error(error));
+          }
+          console.log('  [media] Triggered ICE gathering for ' + elementId);
+          return resolve(); 
+        });
+      }
+      else {
+        return reject("  [MediaController/gatherCandidates] There is no element " + elementId);
+      }
+    });
+  }
+
+  setInputBandwidth (elementId, min, max) {
+    let mediaElement = this._mediaElements[elementId];
+
+    if (typeof mediaElement !== 'undefined') {
+      endpoint.setMinVideoRecvBandwidth(min);
+      endpoint.setMaxVideoRecvBandwidth(max);
+    } else {
+      return (" [MediaController/setInputBandwidth] There is no element " + elementId);
+    }
+  }
+
+  setOutputBandwidth (endpoint, min, max) {
+    let mediaElement = this._mediaElements[elementId];
+
+    if (typeof mediaElement !== 'undefined') {
+      endpoint.setMinVideoSendBandwidth(min);
+      endpoint.setMaxVideoSendBandwidth(max);
+    } else {
+      return (" [MediaController/setOutputBandwidth] There is no element " + elementId );
+    }
+  }
+
+  setOutputBitrate (endpoint, min, max) {
+    let mediaElement = this._mediaElements[elementId];
+
+    if (typeof mediaElement !== 'undefined') {
+      endpoint.setMinOutputBitrate(min);
+      endpoint.setMaxOutputBitrate(max);
+    } else {
+      return (" [MediaController/setOutputBitrate] There is no element " + elementId);
+    }
+  }
+
+  processOffer (elementId, sdpOffer) {
+    let mediaElement = this._mediaElements[elementId];
+
+    return new Promise((resolve, reject) => {
+      if (typeof mediaElement !== 'undefined' && typeof mediaElement.processOffer === 'function') {
+        mediaElement.processOffer(sdpOffer, (error, answer) => {
+          if (error) {
+            return reject(error);
+          }
+          return resolve(answer);
+        });
+      }
+      else {
+        return reject("  [MediaController/processOffer] There is no element " + elementId);
+      }
+    });
+  }
+
+  trackMediaState (elementId, type) {
+    switch (type) {
+      case C.MEDIA_TYPE.URI:
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.ENDOFSTREAM, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId);
+        break;
+
+      case C.MEDIA_TYPE.WEBRTC:
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.ICE, elementId);
+        break;
+
+      case C.MEDIA_TYPE.RTP:
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId);
+        this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId);
+        break;
+
+      default: return;
+    }
+    return;
+  }
+
+  addMediaEventListener (eventTag, elementId) {
+    let mediaElement = this._mediaElements[elementId];
+    // TODO event type validator
+    if (typeof mediaElement !== 'undefined' && mediaElement) {
+      console.log('  [media] Adding media state listener [' + eventTag + '] for ' + elementId);
+      mediaElement.on(eventTag, (event) => {
+        if (eventTag === C.EVENT.MEDIA_STATE.ICE) {
+          event.candidate = mediaServerClient.getComplexType('IceCandidate')(event.candidate);
+        }
+        this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+elementId , {eventTag, event});
+      });
+    }
+  }
+
+  notifyMediaState (elementId, eventTag, event) {
+    this.emit(C.MEDIA_STATE.MEDIA_EVENT , {elementId, eventTag, event});
+  }
+};
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b9eb77cbea7e652595f8d8ff023f6b997ad5b1c
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js
@@ -0,0 +1,30 @@
+/**
+ * @classdesc
+ * Model class for rooms
+ */
+
+'use strict'
+
+module.exports = class Room {
+  constructor(id) {
+    this._id = id;
+    this._users = {};
+    this._mcuUsers = {};
+  }
+
+  getUser (id) {
+    return this._users[id];
+  }
+
+  setUser (user) {
+  if (typeof this._users[user.id] == 'undefined' ||
+        !this._users[user.id]) {
+      this._users[user.id] = {};
+    }
+    this._users[user.id] = user;
+  }
+
+  destroyUser(userId) {
+    this._users[userId] = null;;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
new file mode 100644
index 0000000000000000000000000000000000000000..e532a3d7089e2dfafe12f1842bd7d3da10e31a03
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
@@ -0,0 +1,119 @@
+/**
+ * @classdesc
+ * Model class for external devices
+ */
+
+'use strict'
+
+const C = require('../constants/Constants');
+const SdpWrapper = require('../utils/SdpWrapper');
+const rid = require('readable-id');
+const EventEmitter = require('events').EventEmitter;
+const MediaServer = require('../media/media-server');
+const config = require('config');
+const kurentoUrl = config.get('kurentoUrl');
+
+module.exports = class SdpSession extends EventEmitter {
+  constructor(emitter, sdp = null, room, type = 'WebRtcEndpoint') {
+    super();
+    this.id = rid();
+    this.room = room;
+    this.emitter = emitter;
+    this._status = C.STATUS.STOPPED;
+    this._type = type;
+    // {SdpWrapper} SdpWrapper
+    this._sdp;
+    if (sdp && type) {
+      this.setSdp(sdp, type);
+    }
+    this._MediaServer = new MediaServer(kurentoUrl);
+    this._mediaElement;
+    this.subscribedSessions = [];
+  }
+
+  async setSdp (sdp, type) {
+    this._sdp = new SdpWrapper(sdp, type);
+    await this._sdp.processSdp();
+  }
+
+  async start (sdpId) {
+    this._status = C.STATUS.STARTING;
+    try {
+      const client = await this._MediaServer.init();
+
+      console.log("[SdpSession] start/cme");
+      this._mediaElement = await this._MediaServer.createMediaElement(this.room, this._type);
+      console.log("[SdpSession] start/po " + this._mediaElement);
+
+      this._MediaServer.trackMediaState(this._mediaElement, this._type);
+      this._MediaServer.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+this._mediaElement, (event) => {
+        setTimeout(() => {
+          event.id = this.id;
+          this.emitter.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+this.id, event);
+        }, 50);
+      });
+
+      const answer = await this._MediaServer.processOffer(this._mediaElement, this._sdp.getPlainSdp()); 
+
+      if (this._type === 'WebRtcEndpoint') {
+        this._MediaServer.gatherCandidates(this._mediaElement);
+      }
+
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(err);
+    }
+  }
+
+  // TODO move to parent Session
+  async stop () {
+    this._status = C.STATUS.STOPPING;
+    try {
+      await this._MediaServer.stop(this._mediaElement);
+      this._status = C.STATUS.STOPPED;
+      console.log("  [SdpSession] Session ", this.id, " is going to stop...");
+      this.emit('SESSION_STOPPED', this.id);
+      Promise.resolve();
+    }
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(err);
+    }
+  }
+
+
+  // TODO move to parent Session
+  // TODO handle connection type
+  async connect (sinkId) {
+    try {
+      console.log("  [SdpSession] Connecting " + this._mediaElement + " => " + sinkId);
+      await this._MediaServer.connect(this._mediaElement, sinkId, 'ALL');
+      return Promise.resolve();
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(err);
+    }
+  }
+
+  async addIceCandidate (candidate) {
+    try {
+      await this._MediaServer.addIceCandidate(this._mediaElement, candidate);
+      Promise.resolve();
+    }
+    catch (err) {
+      Promise.reject(err);
+    }
+  }
+
+  addMediaEventListener (type, mediaId) {
+    this._MediaServer.addMediaEventListener (type, mediaId);
+  }
+
+  handleError (err) {
+    console.log(err);
+    this._status = C.STATUS.STOPPED;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js
new file mode 100644
index 0000000000000000000000000000000000000000..86cced861c78564e0ac7789395a7e599593a503d
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js
@@ -0,0 +1,187 @@
+/**
+ * @classdesc
+ * Model class for external devices
+ */
+
+'use strict'
+
+const User = require('./User');
+const C = require('../constants/Constants');
+const SdpWrapper = require('../utils/SdpWrapper');
+const SdpSession = require('../model/SdpSession');
+const UriSession = require('../model/UriSession');
+
+module.exports = class SfuUser extends User {
+  constructor(_roomId, type, emitter, userAgentString = C.STRING.ANONYMOUS, sdp = null, uri = null) {
+    super(_roomId);
+    // {SdpWrapper} SdpWrapper
+    this._sdp;
+    // {Object} hasAudio, hasVideo, hasContent
+    this._mediaSessions = {}
+    this.userAgentString;
+    this.emitter = emitter;
+    if (sdp) {
+      this.addSdp(sdp);
+    }
+    if (uri) {
+      this.addUri(uri);
+    }
+  }
+
+  async addUri (uri, type) {
+    // TODO switch from type to children UriSessions (RTSP|HTTP|etc)
+    let session = new UriSession(uri, type);
+
+    if (typeof this._mediaSessions[session.id] == 'undefined' || 
+        !this._mediaSessions[session.id]) {
+      this._mediaSessions[session.id] = {};
+    }
+    this._mediaSessions[session.id] = session; 
+    try {
+      await this.startSession(session.id);
+      Promise.resolve(session.id);
+    }
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  addSdp (sdp, type) {
+    // TODO switch from type to children SdpSessions (WebRTC|SDP)
+    let session = new SdpSession(this.emitter, sdp, this.roomId, type);
+    this.emitter.emit(C.EVENT.NEW_SESSION+this.id, session.id);
+    session.on("SESSION_STOPPED", (sessId) => {
+      console.log("  [SfuUser] Session ", sessId, "stopped, cleaning it...");
+      if (sessId === session.id) {
+        this._mediaSessions[sessId] = null;
+      }
+    });
+
+    if (typeof this._mediaSessions[session.id] == 'undefined' || 
+        !this._mediaSessions[session.id]) {
+      this._mediaSessions[session.id] = {};
+    }
+    this._mediaSessions[session.id] = session; 
+    console.log("[SfuUser] Added SDP " + session.id);
+
+    return session;
+  }
+
+  async startSession (sessionId) {
+    console.log("[SfuUser] starting session " + sessionId);
+    let session = this._mediaSessions[sessionId];
+  
+    try {
+      const answer = await session.start();
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async subscribe (sdp, type,  mediaId) {
+    try {
+      const session = await this.addSdp(sdp, type);
+      await this.startSession(session.id);
+      await this.connect(session.id, mediaId);
+      Promise.resolve(session);
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async publish (sdp, mediaId) {
+    let session = await this.addSdp(sdp);
+    try {
+      await this.startSession(session.id);
+      Promise.resolve();
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async unsubscribe (mediaId) {
+    try {
+      await this.stopSession(mediaId);
+      Promise.resolve();
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async unpublish (mediaId) {
+    try {
+      await this.stopSession(mediaId);
+      Promise.resolve();
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async stopSession (sessionId) {
+    console.log("  [SfuUser] Stopping session => " + sessionId);
+    let session = this._mediaSessions[sessionId];
+
+    try {
+      await session.stop();
+      this._mediaSessions[sessionId] = null;
+      return Promise.resolve();
+    }
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async connect (sourceId, sinkId) {
+    let session = this._mediaSessions[sourceId];
+    if(session) {
+      try {
+        console.log(" [SfuUser] Connecting sessions " + sourceId + "=>" + sinkId);
+        await session.connect(sinkId);
+        return Promise.resolve();
+      }
+      catch (err) {
+        this.handleError(err);
+        return Promise.reject(new Error(err));
+      }
+    }
+    else {
+      return Promise.reject(new Error("  [SfuUser] Source session " + sourceId + " not found"));
+    }
+  }
+
+  async leave () {
+    let sessions = Object.keys(this._mediaSessions);
+    console.log("  [SfuUser] User sessions will be killed");
+    console.log(sessions);
+
+    try {
+      for (var session in sessions) {
+        await this.stopSession(sessions[session]);
+      }
+
+      return Promise.resolve(sessions);
+    }
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  handleError (err) {
+    console.log(err);
+    this._status = C.STATUS.STOPPED;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js
new file mode 100644
index 0000000000000000000000000000000000000000..8193a56524e10a0958248e3abb95a0106e6312c2
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js
@@ -0,0 +1,73 @@
+/**
+ * @classdesc
+ * Model class for external devices
+ */
+
+'use strict'
+
+const C = require('../constants/Constants');
+const rid = require('readable-id');
+const EventEmitter = require('events').EventEmitter;
+const MediaServer = require('../media/media-server');
+
+module.exports = class UriSession extends EventEmitter {
+  constructor(uri = null) {
+    super();
+    this.id = rid();
+    this._status = C.STATUS.STOPPED;
+    this._uri;
+    if (uri) {
+      this.setUri(uri);
+    }
+  }
+
+  setUri (uri) {
+    this._uri = uri;
+  }
+
+  async start () {
+    this._status = C.STATUS.STARTING;
+    try {
+      const mediaElement = await MediaServer.createMediaElement(this.id, C.MEDIA_TYPE.URI);
+      console.log("start/cme");
+      await MediaServer.play(this.id);
+      this._status = C.STATUS.STARTED;
+      return Promise.resolve();
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  // TODO move to parent Session
+  async stop () {
+    this._status = C.STATUS.STOPPING;
+    try {
+      await MediaServer.stop(this.id);
+      this._status = C.STATUS.STOPPED;
+      return Promise.resolve();
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  // TODO move to parent Session
+  async connect (sinkId) {
+    try {
+      await MediaServer.connect(this.id, sinkId);
+      return Promise.resolve()
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  handleError (err) {
+    console.log(err);
+    this._status = C.STATUS.STOPPED;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js
new file mode 100644
index 0000000000000000000000000000000000000000..919a505189bcbe977d9e0801310ff6382a351bd5
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js
@@ -0,0 +1,18 @@
+/**
+ * @classdesc
+ * Model class for external devices
+ */
+
+'use strict'
+
+const rid = require('readable-id');
+const User = require('./User');
+const C = require('../constants/Constants.js');
+
+module.exports = class User {
+  constructor(roomId, type, userAgentString = C.STRING.ANONYMOUS) {
+    this.roomId = roomId;
+    this.id = rid();
+    this.userAgentString = userAgentString;
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
new file mode 100644
index 0000000000000000000000000000000000000000..eea0d58895e7424e145df30a93d7a20fe6db1a29
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
@@ -0,0 +1,256 @@
+/**
+ * @classdesc
+ * Utils class for manipulating SDP
+ */
+
+'use strict'
+
+var config = require('config');
+var transform = require('sdp-transform');
+
+module.exports = class SdpWrapper {
+  constructor(sdp) {
+    this._plainSdp = sdp;
+    this._jsonSdp = transform.parse(sdp);
+    this._mediaLines = {};
+    this._mediaCapabilities = {};
+    this._profileThreshold = "ffffff";
+  }
+
+  setSdp (sdp) {
+    this._plainSdp = sdp;
+    this._jsonSdp = transform.parse(sdp);
+  }
+
+  getPlainSdp () {
+    return this._plainSdp;
+  }
+  
+  getJsonSdp () {
+    return this._jsonSdp; 
+  }
+
+  removeFmtp () {
+    return this._plainSdp.replace(/(a=fmtp:).*/g, '');
+  }
+
+  replaceServerIpv4 (ipv4) {
+    return this._plainSdp.replace(/(IP4\s[0-9.]*)/g, 'IP4 ' + ipv4);
+  }
+
+  getCallId () {
+    return this._plainSdp.match(/(call-id|i):\s(.*)/i)[2];
+  }
+
+  /**
+   * Given a SDP, test if there is more than on video description
+   * @param  {string} sdp The Session Descriptor
+   * @return {boolean}    true if there is more than one video description, else false
+   */
+  hasAudio () {
+    return /(m=audio)/i.test(this._plainSdp);
+  }
+
+  /**
+   * Given a SDP, test if there is a video description in it 
+   * @param  {string} sdp The Session Descriptor
+   * @return {boolean}    true if there is a video description, else false
+   */
+  hasVideo (sdp) {
+    return /(m=video)/i.test(sdp);
+  }
+
+  /**
+   * Given a SDP, test if there is more than on video description
+   * @param  {string} sdp The Session Descriptor
+   * @return {boolean}    true if there is more than one video description, else false
+   */
+  hasMultipleVideo (sdp) {
+    return /(m=video)([\s\S]*\1){1,}/i.test(sdp);
+  }
+
+  /**
+   * Given a SDP, return its Session Description
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     Session description (SDP until the first media line)
+   */
+  getSessionDescription (sdp) {
+    return sdp.match(/[\s\S]+?(?=m=audio|m=video)/i);
+  }
+
+  removeSessionDescription (sdp) {
+    return sdp.match(/(?=[\s\S]+?)(m=audio[\s\S]+|m=video[\s\S]+)/i)[1];
+  }
+
+  getVideoParameters (sdp) {
+    var res = transform.parse(sdp);
+    console.log("  [sdp] getVideoParameters => " + JSON.stringify(res, null, 2));
+    var params = {};
+    params.fmtp = "";
+    params.codecId = 96;
+    var pt = 0;
+    for(var ml of res.media) {
+      if(ml.type == 'video') {
+        if (typeof ml.fmtp[0] != 'undefined' && ml.fmtp) {
+          params.codecId = ml.fmtp[0].payload;
+          params.fmtp = ml.fmtp[0].config;
+          console.log("  [sdp] getVideoParameters fmtp => " + JSON.stringify(params));
+          return params;
+        }
+      }
+    }
+    return params;
+  }
+
+  /**
+   * Given a SDP, return its Content Description
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     Content Description (SDP after first media description)
+   */
+  getContentDescription (sdp) {
+    var res = transform.parse(sdp);
+    res.media = res.media.filter(function (ml) { return ml.type == "video" });
+    var mangledSdp = transform.write(res);
+    if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") {
+      return mangledSdp;
+    }
+    else
+      return sdp;
+  }
+
+  /**
+   * Given a SDP, return its first Media Description
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     Content Description (SDP after first media description)
+   */
+  getAudioDescription (sdp) {
+    var res = transform.parse(sdp);
+    res.media = res.media.filter(function (ml) { return ml.type == "audio" });
+    // Hack: Some devices (Snom, Pexip) send crypto with RTP/AVP
+    // That is forbidden according to RFC3711 and FreeSWITCH rebukes it
+    res = this.removeTransformCrypto(res);
+    var mangledSdp = transform.write(res);
+    this.getSessionDescription(mangledSdp);
+    if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") {
+      return mangledSdp;
+    }
+    else {
+      return sdp;
+    }
+  }
+
+  /**
+   * Given a SDP, return its first Media Description
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     Content Description (SDP after first media description)
+   */
+  getMainDescription () {
+    var res = transform.parse(this._plainSdp);
+    // Filter should also carry && ml.invalid[0].value != 'content:slides';
+    // when content is enabled
+    res.media = res.media.filter(function (ml) { return ml.type == "video"}); //&& ml.invalid[0].value != 'content:slides'});
+    var mangledSdp = transform.write(res);
+    if (typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") {
+      console.log("  [sdp] MAIN VIDEO SDP => " + mangledSdp);
+      return mangledSdp;
+    }
+    else {
+      return sdp;
+    }
+  }
+
+  /**
+   * Given a JSON SDP, remove associated crypto 'a=' lines from media lines
+   * WARNING: HACK MADE FOR FreeSWITCH ~1.4 COMPATIBILITY
+   * @param  {Object} sdp The Session Descriptor JSON
+   * @return {Object}     JSON SDP without crypto lines
+   */
+  removeTransformCrypto (sdp) {
+    for(var ml of sdp.media) {
+      delete ml['crypto'];
+    }
+    return sdp;
+  }
+
+  removeHighQualityFmtps (sdp) {
+    let res = transform.parse(sdp);
+    let maxProfileLevel = config.get('kurento.maximum_profile_level_hex');
+    let pt = 0;
+    let idx = 0;
+    for(var ml of res.media) {
+      if(ml.type == 'video') {
+        for(var fmtp of ml.fmtp) {
+          let fmtpConfig = transform.parseParams(fmtp.config);
+          let profileId = fmtpConfig['profile-level-id'];
+          if(typeof profileId !== 'undefined' && parseInt(profileId, 16) > parseInt(maxProfileLevel, 16)) {
+            console.log("  [sdp] Filtering profile " + parseInt(profileId, 16) + ". Higher than max "+ parseInt(maxProfileLevel, 16));
+            pt = fmtp.payload;
+            delete ml.fmtp[idx];
+            ml.rtp = ml.rtp.filter((rtp) => { return rtp.payload != pt});
+          }
+          else {
+            // Remove fmtp further specifications
+            //let configProfile = "profile-level-id="+profileId;
+            //fmtp.config = configProfile;
+          }
+          idx++;
+        }
+      }
+    }
+    var mangledSdp = transform.write(res);
+    return mangledSdp;
+  }
+
+  async processSdp () {
+    let description = this._plainSdp;
+    //if(config.get('kurento.force_low_resolution'))  {
+    //  description = this.removeFmtp(description);
+    //}
+
+    description = description.toString().replace(/telephone-event/, "TELEPHONE-EVENT");
+
+    this._mediaCapabilities.hasVideo = this.hasVideo(description);
+    this._mediaCapabilities.hasAudio = this.hasAudio(description);
+    this._mediaCapabilities.hasContent = this.hasMultipleVideo(description);
+    this.sdpSessionDescription = this.getSessionDescription(description);
+    this.audioSdp =  this.getAudioDescription(description);
+    this.mainVideoSdp = this.getMainDescription(description);
+    //this.mainVideoSdp = this.removeHighQualityFmtps(this.mainVideoSdp);
+    this.contentVideoSdp = this.getContentDescription(description);
+
+    return;
+  }
+
+  /* DEVELOPMENT METHODS */
+  _disableMedia  (sdp) {
+    return sdp.replace(/(m=application\s)\d*/g, "$10");
+  };
+
+  /**
+   * Given a SDP, add Floor Control response
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     A new Session Descriptor with Floor Control
+   */
+  _addFloorControl (sdp) {
+    return sdp.replace(/a=inactive/i, 'a=sendrecv\r\na=floorctrl:c-only\r\na=setup:active\r\na=connection:new');
+  }
+
+  /**
+   * Given a SDP, add Floor Control response to reinvite
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     A new Session Descriptor with Floor Control Id
+   */
+  _addFloorId (sdp) {
+    sdp = sdp.replace(/(a=floorctrl:c-only)/i, '$1\r\na=floorid:1 m-stream:3');
+    return sdp.replace(/(m=video.*)([\s\S]*?m=video.*)([\s\S]*)/i, '$1\r\na=content:main\r\na=label:1$2\r\na=content:slides\r\na=label:3$3');
+  }
+
+  /**
+   * Given the string representation of a Session Descriptor, remove it's video
+   * @param  {string} sdp The Session Descriptor
+   * @return {string}     A new Session Descriptor without the video
+   */
+  _removeVideoSdp  (sdp) {
+    return sdp.replace(/(m=video[\s\S]+)/g,'');
+  };
+};
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..11b06b0bcf49d721be79203de85fe307962b74a7
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js
@@ -0,0 +1,37 @@
+/**
+ * @classdesc
+ * Utils class for SDP generation
+ */
+
+module.exports.generateSdp = function(remote_ip_address, remote_video_port) {
+  return "v=0\r\n"
+    + "o=- 0 0 IN IP4 " + remote_ip_address + "\r\n"
+    + "s=No Name\r\n"
+    + "c=IN IP4 " + remote_ip_address + "\r\n"
+    + "t=0 0\r\n"
+    + "m=video " + remote_video_port + " RTP/AVP 96\r\n"
+    + "a=rtpmap:96 H264/90000\r\n"
+    + "a=ftmp:96 packetization-mode=0\r\n";
+}
+
+/**
+ * Generates a video SDP given the media specs
+ * @param  {string} sourceIpAddress The source IP address of the media
+ * @param  {string} sourceVideoPort The source video port of the media
+ * @param  {string} codecId         The ID of the codec
+ * @param  {string} sendReceive     The SDP flag of the media flow
+ * direction, 'sendonly', 'recvonly' or 'sendrecv'
+ * @param {String} rtpProfile       The RTP profile of the RTP Endpoint
+ * @param {String} codecName        The name of the codec used for the RTP
+ * Endpoint
+ * @param {String} codecRate        The codec rate
+ * @return {string}                 The Session Descriptor for the media
+ */
+module.exports.generateVideoSdp = function (sourceIpAddress, sourceVideoPort, codecId, sendReceive, rtpProfile, codecName, codecRate, fmtp) {
+  return 'm=video ' + sourceVideoPort + ' ' + rtpProfile + ' ' + codecId + '\r\n'
+    + 'a=' + sendReceive + '\r\n'
+    + 'c=IN IP4 ' + sourceIpAddress + '\r\n'
+    + 'a=rtpmap:' + codecId + ' ' + codecName + '/' + codecRate + '\r\n'
+    + 'a=fmtp:' + codecId + ' ' + fmtp + '\r\n';
+};
+
diff --git a/labs/kurento-screenshare/lib/media-handler.js b/labs/bbb-webrtc-sfu/lib/media-handler.js
similarity index 69%
rename from labs/kurento-screenshare/lib/media-handler.js
rename to labs/bbb-webrtc-sfu/lib/media-handler.js
index 2d1ab3815cb3d583be7a115c12ab6a1c6626fa92..5c611ec395d7a994c162f5e1b0e11088f5e6b233 100644
--- a/labs/kurento-screenshare/lib/media-handler.js
+++ b/labs/bbb-webrtc-sfu/lib/media-handler.js
@@ -1,42 +1,6 @@
 var config = require('config');
-var kurento = require('kurento-client');
 var Constants = require('./bbb/messages/Constants');
 
-var kurentoClient = null;
-var mediaPipelines = {};
-
-module.exports.getKurentoClient = function(kurentoUrl, callback) {
-  if (kurentoClient !== null) {
-    return callback(null, kurentoClient);
-  }
-
-  kurento(kurentoUrl, function(error, _kurentoClient) {
-    if (error) {
-      console.log("Could not find media server at address " + kurentoUrl);
-      return callback("Could not find media server at address" + kurentoUrl + ". Exiting with error " + error);
-    }
-
-    console.log(" [MediaHandler] Initiating kurento client. Connecting to: " + kurentoUrl);
-
-    kurentoClient = _kurentoClient;
-    callback(null, kurentoClient);
-  });
-}
-
-module.exports.getMediaPipeline = function(id, callback) {
-  console.log(' [MediaHandler] Creating media pipeline for ' + id);
-
-  if (mediaPipelines[id]) {
-    console.log(' [media] Pipeline already exists.');
-    callback(null, mediaPipelines[id]);
-  } else {
-    kurentoClient.create('MediaPipeline', function(err, pipeline) {
-      mediaPipelines[id] = pipeline;
-      return callback(err, pipeline);
-    });
-  }
-}
-
 module.exports.generateSdp = function(remote_ip_address, remote_video_port) {
   return "v=0\r\n"
     + "o=- 0 0 IN IP4 " + remote_ip_address + "\r\n"
@@ -76,7 +40,7 @@ module.exports.generateStreamUrl = function (address, meeting, path) {
   return "rtmp://" + address + "/video-broadcast/" + meeting + "/" + path;
 }
 
-module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, recvPort, input, streamType, transcoderType, codec, callername) {
+module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, recvPort, input, streamType, transcoderType, codec, callername, voiceConf) {
   var rtpParams = {};
   rtpParams[Constants.LOCAL_IP_ADDRESS] = localIp;
   rtpParams[Constants.LOCAL_VIDEO_PORT] = sendPort;
@@ -87,6 +51,7 @@ module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, r
   rtpParams[Constants.TRANSCODER_TYPE] = transcoderType;
   rtpParams[Constants.TRANSCODER_CODEC] = codec;
   rtpParams[Constants.CALLERNAME] = callername;
+  rtpParams[Constants.VOICE_CONF] = voiceConf;
   return rtpParams;
 }
 
diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..024bb7bf4e9eaf5a1ff8dcf5a7a888bcf9f6e27b
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js
@@ -0,0 +1,190 @@
+/*
+ * Lucas Fialho Zawacki
+ * Paulo Renato Lanzarin
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+"use strict";
+
+const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw');
+const Screenshare = require('./screenshare');
+const C = require('../bbb/messages/Constants');
+// Global variables
+
+module.exports = class ScreenshareManager {
+  constructor (logger) {
+    this._logger = logger;
+    this._clientId = 0;
+
+    this._sessions = {};
+    this._screenshareSessions = {};
+
+    this._bbbGW = new BigBlueButtonGW("MANAGER");
+    this._redisGateway;
+  }
+
+  async start() {
+    try {
+      this._redisGateway = await this._bbbGW.addSubscribeChannel(C.TO_SCREENSHARE);
+      const transcode = await this._bbbGW.addSubscribeChannel(C.FROM_BBB_TRANSCODE_SYSTEM_CHAN);
+      this._redisGateway.on(C.REDIS_MESSAGE, this._onMessage.bind(this));
+      console.log('  [ScreenshareManager] Successfully subscribed to redis channel');
+    }
+    catch (error) {
+      console.log('  [ScreenshareManager] Could not connect to transcoder redis channel, finishing app...');
+      console.log(error);
+      this.stopAll();
+    }
+  }
+
+  _onMessage(_message) {
+    console.log('  [ScreenshareManager] Received message => ');
+    let session;
+    let message = _message;
+
+    let sessionId = message.voiceBridge;
+    let connectionId = message.connectionId;
+
+    if(this._screenshareSessions[sessionId]) {
+      session = this._screenshareSessions[sessionId];
+    }
+
+    switch (message.id) {
+
+      case 'presenter':
+
+        // Checking if there's already a Screenshare session started
+        // because we shouldn't overwrite it
+
+        if (!this._screenshareSessions[message.voiceBridge]) {
+          this._screenshareSessions[message.voiceBridge] = {}
+          this._screenshareSessions[message.voiceBridge] = session;
+        }
+
+        if(session) {
+          break;
+        }
+
+        session = new Screenshare(connectionId, this._bbbGW,
+            sessionId, connectionId, message.vh, message.vw,
+            message.internalMeetingId);
+
+        this._screenshareSessions[sessionId] = {}
+        this._screenshareSessions[sessionId] = session;
+
+        // starts presenter by sending sessionID, websocket and sdpoffer
+        session._startPresenter(sessionId, message.sdpOffer, (error, sdpAnswer) => {
+          console.log("  [ScreenshareManager] Started presenter " + sessionId);
+          if (error) {
+            this._bbbGW.publish(JSON.stringify({
+              connectionId: session._id,
+              id : 'presenterResponse',
+              response : 'rejected',
+              message : error
+            }), C.FROM_SCREENSHARE);
+            return error;
+          }
+
+          this._bbbGW.publish(JSON.stringify({
+            connectionId: session._id,
+            id : 'presenterResponse',
+            response : 'accepted',
+            sdpAnswer : sdpAnswer
+          }), C.FROM_SCREENSHARE);
+
+          console.log("  [ScreenshareManager]  [websocket] Sending presenterResponse \n" + sdpAnswer);
+        });
+        break;
+
+      case 'viewer':
+        console.log("  [ScreenshareManager][viewer] Session output \n " + session);
+        if (message.sdpOffer && message.voiceBridge) {
+          if (session) {
+            session._startViewer(message.connectionId, message.voiceBridge, message.sdpOffer, connectionId,
+                this._screenshareSessions[message.voiceBridge]._presenterEndpoint);
+          } else {
+            // TODO ERROR HANDLING
+          }
+        }
+        break;
+
+      case 'stop':
+        console.log('[' + message.id + '] connection ' + sessionId);
+
+        if (session) {
+          session._stop(sessionId);
+        } else {
+          console.log(" [stop] Why is there no session on STOP?");
+        }
+        break;
+
+      case 'onIceCandidate':
+        if (session) {
+          session.onIceCandidate(message.candidate);
+        } else {
+          console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?");
+        }
+        break;
+
+      case 'viewerIceCandidate':
+        console.log("[viewerIceCandidate] Session output => " + session);
+        if (session) {
+          session.onViewerIceCandidate(message.candidate, connectionId);
+        } else {
+          console.log("[iceCandidate] Why is there no session on ICE CANDIDATE?");
+        }
+        break;
+
+      case 'close':
+        console.log('  [ScreenshareManager] Connection ' + connectionId + ' closed');
+
+        if (message.role === 'presenter' && this._screenshareSessions[sessionId]) {
+          console.log("  [ScreenshareManager] Stopping presenter " + sessionId);
+          this._stopSession(sessionId);
+        }
+        if (message.role === 'viewer' && typeof session !== 'undefined') {
+          console.log("  [ScreenshareManager] Stopping viewer " + sessionId);
+          session.stopViewer(message.connectionId);
+        }
+        break;
+
+      default:
+        this._bbbGW.publish(JSON.stringify({
+          connectionId: session._id? session._id : 'none',
+          id : 'error',
+          message: 'Invald message ' + message
+        }), C.FROM_SCREENSHARE);
+        break;
+    }
+  }
+
+  _stopSession(sessionId) {
+    console.log(' [>] Stopping session ' + sessionId);
+
+    if (typeof this._screenshareSessions === 'undefined' || typeof sessionId === 'undefined') {
+      return;
+    }
+
+    let session = this._screenshareSessions[sessionId];
+    if(typeof session !== 'undefined' && typeof session._stop === 'function') {
+      session._stop();
+    }
+
+    delete this._screenshareSessions[sessionId];
+  }
+
+  stopAll() {
+    console.log('\n [x] Stopping everything! ');
+
+    if (typeof this._screenshareSessions === 'undefined') {
+      return;
+    }
+
+    let sessionIds = Object.keys(this._screenshareSessions);
+
+    for (let i = 0; i < sessionIds.length; i++) {
+      this._stopSession(sessionIds[i]);
+    }
+  }
+};
diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..223bde09dfb129381ca98bb591e1ecb83291af16
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js
@@ -0,0 +1,10 @@
+const ScreenshareManager = require('./ScreenshareManager');
+
+let c = new ScreenshareManager();
+c.start();
+
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+});
+
+process.on('disconnect', c.stopAll);
diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
new file mode 100644
index 0000000000000000000000000000000000000000..df3df06f1c5470a449e64c899ea9995f8e1d4ae8
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
@@ -0,0 +1,357 @@
+/*
+* Lucas Fialho Zawacki
+ * Paulo Renato Lanzarin
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+'use strict'
+
+// Imports
+const C = require('../bbb/messages/Constants');
+const MediaHandler = require('../media-handler');
+const Messaging = require('../bbb/messages/Messaging');
+const moment = require('moment');
+const h264_sdp = require('../h264-sdp');
+const now = moment();
+const MCSApi = require('../mcs-core/lib/media/MCSApiStub');
+const config = require('config');
+const kurentoIp = config.get('kurentoIp');
+const localIpAddress = config.get('localIpAddress');
+
+// Global stuff
+var sharedScreens = {};
+var rtpEndpoints = {};
+
+if (config.get('acceptSelfSignedCertificate')) {
+  process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
+}
+
+module.exports = class Screenshare {
+  constructor(id, bbbgw, voiceBridge, caller = 'caller', vh, vw, meetingId) {
+    this.mcs = new MCSApi();
+    this._id = id;
+    this._BigBlueButtonGW = bbbgw;
+    this._presenterEndpoint = null;
+    this._ffmpegEndpoint = null;
+    this._voiceBridge = voiceBridge;
+    this._meetingId = meetingId;
+    this._caller = caller;
+    this._streamUrl = "";
+    this._vw = vw;
+    this._vh = vh;
+    this._presenterCandidatesQueue = [];
+    this._viewersEndpoint = [];
+    this._viewersCandidatesQueue = [];
+  }
+
+  onIceCandidate (_candidate) {
+    if (this._presenterEndpoint) {
+      try {
+        this.flushCandidatesQueue(this._presenterEndpoint, this._presenterCandidatesQueue);
+        this.mcs.addIceCandidate(this._presenterEndpoint, _candidate);
+      }
+      catch (err)   {
+        console.log(err);
+      }
+    }
+    else {
+      this._presenterCandidatesQueue.push(_candidate);
+    }
+  };
+
+  flushCandidatesQueue (mediaId, queue) {
+    if (this.mediaId) {
+      try {
+        while(queue.length) {
+          let candidate = queue.shift();
+          this.mcs.addIceCandidate(mediaId, candidate);
+        }
+      }
+      catch (err) {
+        console.log(err);
+      }
+    }
+  }
+
+  mediaStateRtp (event) {
+    let msEvent = event.event;
+
+    console.log('  [screenshare] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this._id);
+
+    switch (event.eventTag) {
+      case "MediaStateChanged":
+        break;
+
+      case "MediaFlowOutStateChange":
+        break;
+
+      case "MediaFlowInStateChange":
+        if (msEvent.state === 'FLOWING') {
+          this._onRtpMediaFlowing();
+        }
+        else {
+          this._onRtpMediaNotFlowing();
+        }
+        break;
+
+      default: console.log("  [video] Unrecognized event");
+    }
+  }
+
+  mediaStateWebRtc (event, id) {
+    let msEvent = event.event;
+
+    console.log('  [screenshare] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this._id);
+
+    switch (event.eventTag) {
+      case "OnIceCandidate":
+        let candidate = msEvent.candidate;
+        this._BigBlueButtonGW.publish(JSON.stringify({
+          connectionId: id,
+          id : 'iceCandidate',
+          cameraId: this._id,
+          candidate : candidate
+        }), C.FROM_SCREENSHARE);
+
+        break;
+
+      case "MediaStateChanged":
+        break;
+
+      case "MediaFlowOutStateChange":
+        break;
+
+      case "MediaFlowInStateChange":
+        break;
+
+      default: console.log("  [video] Unrecognized event");
+    }
+  }
+
+  async _startPresenter(id, sdpOffer, callback) {
+    let presenterSdpAnswer, rtpSdpAnswer;
+    let _callback = callback;
+
+    // Force H264 on Firefox and Chrome
+    sdpOffer = h264_sdp.transform(sdpOffer);
+    console.log(" [screenshare] Starting presenter " + id + " at voiceBridge " + this._voiceBridge);
+
+    try {
+      this.userId = await this.mcs.join(this._meetingId, 'SFU', {});
+      console.log("  [video] Join returned => " + this.userId);
+    }
+    catch (err) {
+      console.log("  [video] MCS join returned error => " + err);
+      return callback(err);
+    }
+
+    try {
+      const retSource = await this.mcs.publish(this.userId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer});
+
+      this._presenterEndpoint = retSource.sessionId;
+      sharedScreens[id] = this._presenterEndpoint;
+      presenterSdpAnswer = retSource.answer;
+      this.flushCandidatesQueue(this._presenterEndpoint, this._presenterCandidatesQueue);
+
+      this.mcs.on('MediaEvent' + this._presenterEndpoint, (event) => {
+        this.mediaStateWebRtc(event, this._id)
+      });
+
+      console.log("  [video] Publish returned => " + this._presenterEndpoint);
+
+    }
+    catch (err) {
+      console.log("  [video] MCS publish returned error => " + err);
+      return callback(err);
+    }
+
+    try {
+      let sendVideoPort = MediaHandler.getVideoPort();
+      let rtpSdpOffer = MediaHandler.generateVideoSdp(localIpAddress, sendVideoPort);
+
+      const retRtp = await this.mcs.subscribe(this.userId, sharedScreens[id], 'RtpEndpoint', {descriptor: rtpSdpOffer});
+
+      this._ffmpegEndpoint = retRtp.sessionId;
+      rtpEndpoints[id] = this._ffmpegEndpoint;
+
+      let recvVideoPort = retRtp.answer.match(/m=video\s(\d*)/)[1];
+      this._rtpParams = MediaHandler.generateTranscoderParams(kurentoIp, localIpAddress,
+          sendVideoPort, recvVideoPort, this._meetingId, "stream_type_video", C.RTP_TO_RTMP, "copy", this._caller, this._voiceBridge);
+
+      this.mcs.on('MediaEvent' + this._ffmpegEndpoint, this.mediaStateRtp.bind(this));
+
+      console.log("  [video] Subscribe returned => " + this._ffmpegEndpoint);
+
+      return callback(null, presenterSdpAnswer);
+    }
+    catch (err) {
+      console.log("  [video] MCS subscribe returned error => " + err);
+      return callback(err);
+    }
+  }
+
+  onViewerIceCandidate(candidate, callerName) {
+    if (this._viewersEndpoint[callerName]) {
+      try {
+        this.flushCandidatesQueue(this._viewersEndpoint[callerName], this._viewersCandidatesQueue[callerName]);
+        this.mcs.addIceCandidate(this._viewersEndpoint[callerName], candidate);
+      }
+      catch (err)   {
+        console.log(err);
+      }
+    }
+    else {
+      if (!this._viewersCandidatesQueue[callerName]) {
+        this._viewersCandidatesQueue[callerName] = [];
+      }
+      this._viewersCandidatesQueue[callerName].push(candidate);
+    }
+  }
+
+  async _startViewer(connectionId, voiceBridge, sdp, callerName, presenterEndpoint, callback) {
+    let _callback = function(){};
+    let sdpAnswer, sdpOffer;
+    console.log("  [screenshare] Starting viewer " + callerName + " for voiceBridge " + this._voiceBridge);
+
+    sdpOffer = h264_sdp.transform(sdp);
+    sdpOffer = sdp;
+
+    this._viewersCandidatesQueue[callerName] = [];
+
+
+    try {
+      const retSource = await this.mcs.subscribe(this.userId, sharedScreens[voiceBridge], 'WebRtcEndpoint', {descriptor: sdpOffer});
+
+      this._viewersEndpoint[callerName] = retSource.sessionId;
+      sdpAnswer = retSource.answer;
+      this.flushCandidatesQueue(this._viewersEndpoint[callerName], this._viewersCandidatesQueue[callerName]);
+      this.mcs.on('MediaEvent' + this._viewersEndpoint[callerName], (event) => {
+        this.mediaStateWebRtc(event, connectionId);
+      });
+
+      this._BigBlueButtonGW.publish(JSON.stringify({
+        connectionId: connectionId,
+        id: "viewerResponse",
+        sdpAnswer: sdpAnswer,
+        response: "accepted"
+      }), C.FROM_SCREENSHARE);
+
+      console.log(" Sent sdp message to client with callerName:" + callerName);
+      console.log("  [screenshare] Subscribe returned => " + this._viewersEndpoint[callerName]);
+    }
+    catch (err) {
+      console.log("  [screenshare] MCS publish returned error => " + err);
+      return _callback(err);
+    }
+  }
+
+  async _stop() {
+    console.log(' [stop] Releasing endpoints for ' + this.userId);
+
+    this._stopScreensharing();
+
+    if (this._presenterEndpoint) {
+      try {
+        await this.mcs.leave(this._meetingId, this.userId);
+        sharedScreens[this._presenterEndpoint] = null;
+        this._candidatesQueue = null;
+        this._presenterEndpoint = null;
+        this._ffmpegEndpoint = null;
+        return;
+      }
+      catch (err) {
+        console.log(err);
+        return;
+      }
+    }
+    return;
+  }
+
+  _stopScreensharing() {
+    let strm = Messaging.generateStopTranscoderRequestMessage(this._meetingId, this._meetingId);
+
+    this._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {});
+
+    // Interoperability: capturing 1.1 stop_transcoder_reply messages
+    this._BigBlueButtonGW.once(C.STOP_TRANSCODER_REPLY, (payload) => {
+      let meetingId = payload[C.MEETING_ID];
+      this._stopRtmpBroadcast(meetingId);
+    });
+
+    // Capturing stop transcoder responses from the 2x model
+    this._BigBlueButtonGW.once(C.STOP_TRANSCODER_RESP_2x, (payload) => {
+      let meetingId = payload[C.MEETING_ID_2x];
+      this._stopRtmpBroadcast(meetingId);
+    });
+
+  }
+
+  _onRtpMediaFlowing() {
+    console.log("  [screenshare] Media FLOWING for meeting => " + this._meetingId);
+    let strm = Messaging.generateStartTranscoderRequestMessage(this._meetingId, this._meetingId, this._rtpParams);
+
+    // Interoperability: capturing 1.1 start_transcoder_reply messages
+    this._BigBlueButtonGW.once(C.START_TRANSCODER_REPLY, (payload) => {
+      let meetingId = payload[C.MEETING_ID];
+      let output = payload["params"].output;
+      this._startRtmpBroadcast(meetingId, output);
+    });
+
+    // Capturing stop transcoder responses from the 2x model
+    this._BigBlueButtonGW.once(C.START_TRANSCODER_RESP_2x, (payload) => {
+      let meetingId = payload[C.MEETING_ID_2x];
+      let output = payload["params"].output;
+      this._startRtmpBroadcast(meetingId, output);
+    });
+
+
+    this._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {});
+  };
+
+  _stopRtmpBroadcast (meetingId) {
+    console.log("  [screenshare] _stopRtmpBroadcast for meeting => " + meetingId);
+    if(this._meetingId === meetingId) {
+      // TODO correctly assemble this timestamp
+      let timestamp = now.format('hhmmss');
+      let dsrstom = Messaging.generateScreenshareRTMPBroadcastStoppedEvent2x(this._voiceBridge,
+          this._voiceBridge, this._streamUrl, this._vw, this._vh, timestamp);
+      this._BigBlueButtonGW.publish(dsrstom, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {});
+    }
+  }
+
+  _startRtmpBroadcast (meetingId, output) {
+    console.log("  [screenshare] _startRtmpBroadcast for meeting => " + meetingId);
+    if(this._meetingId === meetingId) {
+      // TODO correctly assemble this timestamp
+      let timestamp = now.format('hhmmss');
+      this._streamUrl = MediaHandler.generateStreamUrl(localIpAddress, meetingId, output);
+      let dsrbstam = Messaging.generateScreenshareRTMPBroadcastStartedEvent2x(this._voiceBridge,
+          this._voiceBridge, this._streamUrl, this._vw, this._vh, timestamp);
+
+      this._BigBlueButtonGW.publish(dsrbstam, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {});
+    }
+  }
+
+  _onRtpMediaNotFlowing() {
+    console.log("  [screenshare] TODO RTP NOT_FLOWING");
+  }
+
+  async stopViewer(id) {
+    let viewer = this._viewersEndpoint[id];
+    console.log(' [stop] Releasing endpoints for ' + viewer);
+
+    if (viewer) {
+      try {
+        await this.mcs.unsubscribe(this.userId, this.viewer);
+        this._viewersCandidatesQueue[id] = null;
+        this._viewersEndpoint[id] = null;
+        return;
+      }
+      catch (err) {
+        console.log(err);
+        return;
+      }
+    }
+  }
+};
diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoManager.js b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
new file mode 100755
index 0000000000000000000000000000000000000000..a42a3454e80b40c8909cc7342359207d54f6fef1
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
@@ -0,0 +1,214 @@
+/*
+ * Lucas Fialho Zawacki
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+'use strict';
+
+const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw');
+const Video = require('./video');
+const C = require('../bbb/messages/Constants');
+
+let sessions = {};
+
+var clientId = 0;
+
+let bbbGW = new BigBlueButtonGW("MANAGER");
+let redisGateway;
+
+bbbGW.addSubscribeChannel(C.TO_VIDEO).then((gw) => {
+  redisGateway = gw;
+  redisGateway.on(C.REDIS_MESSAGE, _onMessage);
+  console.log('  [VideoManager] Successfully subscribed to redis channel ' + C.TO_VIDEO);
+
+});
+
+var _onMessage = function (_message) {
+  let message = _message;
+  let sessionId = message.connectionId;
+  let video;
+  let role = message.role? message.role : 'any';
+  let cameraId = message.cameraId;
+  let shared = false;
+  let iceQueue = {};
+
+  if (message.role == 'share') {
+    shared = true;
+  }
+
+  if (!sessions[sessionId]) {
+    sessions[sessionId] = {};
+  }
+
+  logAvailableSessions();
+
+  switch (role) {
+    case 'share':
+      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'shared']) {
+        video = sessions[sessionId][message.cameraId+'shared'];
+      }
+      break;
+    case 'viewer':
+      if (message.cameraId && sessions[sessionId][message.cameraId]) {
+        video = sessions[sessionId][message.cameraId];
+      }
+    case 'any':
+      if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' &&  sessions[sessionId][message.cameraId+'shared']) {
+        video = sessions[sessionId][message.cameraId+'shared'];
+      }
+      else if (message.cameraId && sessions[sessionId][message.cameraId]) {
+        video = sessions[sessionId][message.cameraId];
+      }
+
+      break;
+  }
+
+  switch (message.id) {
+    case 'start':
+      console.log('[' + message.id + '] connection ' + sessionId + " message => " + JSON.stringify(message, null, 2));
+
+      video = new Video(bbbGW, message.cameraId, shared, message.connectionId);
+
+      // Empty ice queue after starting video
+      if (iceQueue[message.cameraId]) {
+        let candidate;
+        while(candidate = iceQueue[message.cameraId].pop()) {
+          video.onIceCandidate(cand);
+        }
+      }
+
+      switch (role) {
+        case 'share':
+          sessions[sessionId][message.cameraId+'shared']= video;
+          break;
+        case 'viewer':
+          sessions[sessionId][message.cameraId] = video;
+          break;
+        default: console.log(" [VideoManager] Unknown role? ", role);
+      }
+
+      video.start(message.sdpOffer, (error, sdpAnswer) => {
+        if (error) {
+          return bbbGW.publish(JSON.stringify({
+            connectionId: sessionId,
+            type: 'video',
+            role: role,
+            id : 'error',
+            response : 'rejected',
+            message : error
+          }), C.FROM_VIDEO);
+        }
+
+        bbbGW.publish(JSON.stringify({
+          connectionId: sessionId,
+          type: 'video',
+          role: role,
+          id : 'startResponse',
+          cameraId: message.cameraId,
+          sdpAnswer : sdpAnswer
+        }), C.FROM_VIDEO);
+      });
+      break;
+
+    case 'stop':
+
+      console.log('[' + message.id + '] connection ' + sessionId + " with message => " + JSON.stringify(message, null, 2));
+
+      if (video) {
+        stopSession(sessionId, role, cameraId);
+      } else {
+        console.log(" [stop] Why is there no video on STOP?");
+      }
+      break;
+
+    case 'onIceCandidate':
+
+      if (video) {
+        video.onIceCandidate(message.candidate);
+      } else {
+        console.log(" [iceCandidate] Queueing ice candidate for later in video " + message.cameraId);
+
+        if (!iceQueue[message.cameraId]) {
+          iceQueue[message.cameraId] = [];
+        }
+        iceQueue[message.cameraId].push(message.candidate);
+      }
+      break;
+
+    case 'close':
+      console.log(" [vide] Closing session for sessionId: " + sessionId);
+
+      stopSession(sessionId);
+
+      break;
+
+    default:
+      bbbGW.publish(JSON.stringify({
+        connectionId: sessionId,
+        type: 'video',
+        id : 'error',
+        response : 'rejected',
+        message : 'Invalid message ' + JSON.stringify(message)
+      }), C.FROM_VIDEO);
+      break;
+  }
+};
+
+let stopSession = async function(sessionId, role, cameraId) {
+  console.log('  [VideoManager/x] Stopping session ' + sessionId + " with role " + role + " for camera " + cameraId);
+
+  let videoIds = Object.keys(sessions[sessionId]);
+
+  try {
+    if (role === 'share') {
+      var sharedVideo = sessions[sessionId][cameraId+'shared'];
+      await sharedVideo.stop();
+      delete sessions[sessionId][cameraId+'shared'];
+      console.log('  [VideoManager] Stopping sharer [', sessionId, '][', cameraId,'] with IDs' , videoIds);
+    }
+    else if (role === 'viewer') {
+      var video = sessions[sessionId][cameraId];
+      await video.stop();
+      delete sessions[sessionId][cameraId];
+      console.log('  [VideoManager] Stopping viewer [', sessionId, '][', cameraId,'] with IDs ', sessions[sessionId][cameraId]);
+    }
+
+    logAvailableSessions();
+  }
+  catch (err) {
+    console.log("  [VideoManager] Stop error => ", err);
+  }
+}
+
+let stopAll = function() {
+  console.log('  [Video/x] Stopping everything! ');
+
+  if (sessions == null) {
+    return;
+  }
+
+  let sessionIds = Object.keys(sessions);
+
+  for (var i = 0; i < sessionIds.length; i++) {
+
+    stopSession(sessionIds[i]);
+  }
+
+  setTimeout(process.exit, 100);
+}
+
+let logAvailableSessions = function() {
+  if(typeof sessions !== 'undefined' && sessions) {
+    console.log("  [VideoManager] Available sessions are =>");
+    let sessionMainKeys = Object.keys(sessions);
+    for (var k in sessions) {
+      if(typeof sessions[k] !== 'undefined' && sessions[k]) {
+        console.log('  [VideoManager] Session[', k,'] => ', Object.keys(sessions[k]));
+      }
+    }
+  }
+}
+
+process.on('SIGTERM', stopAll);
+process.on('SIGINT', stopAll);
diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js
new file mode 100644
index 0000000000000000000000000000000000000000..b6add57d4fbd68c5ce2de4cf12a4fd710016be06
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js
@@ -0,0 +1,9 @@
+const VideoManager = require('./VideoManager');
+
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+});
+
+process.on('disconnect',function() {
+  console.log("Parent exited!");
+});
diff --git a/labs/bbb-webrtc-sfu/lib/video/video.js b/labs/bbb-webrtc-sfu/lib/video/video.js
new file mode 100644
index 0000000000000000000000000000000000000000..b99077321b18855d86db5ecc5301aa09af7cc744
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/video.js
@@ -0,0 +1,166 @@
+'use strict';
+// Global stuff
+var sharedWebcams = {};
+
+const kurento = require('kurento-client');
+const config = require('config');
+const kurentoUrl = config.get('kurentoUrl');
+const MCSApi = require('../mcs-core/lib/media/MCSApiStub');
+const C = require('../bbb/messages/Constants');
+
+if (config.get('acceptSelfSignedCertificate')) {
+  process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
+}
+
+module.exports = class Video {
+  constructor(_bbbGW, _id, _shared, _sessionId) {
+    this.mcs = new MCSApi();
+    this.bbbGW = _bbbGW;
+    this.id = _id;
+    this.sessionId = _sessionId;
+    this.meetingId = _id;
+    this.shared = _shared;
+    this.role = this.shared? 'share' : 'view'
+    this.webRtcEndpoint = null;
+    this.mediaId = null;
+
+    this.candidatesQueue = [];
+  }
+
+  onIceCandidate (_candidate) {
+    if (this.mediaId) {
+      try {
+        this.flushCandidatesQueue();
+        this.mcs.addIceCandidate(this.mediaId, _candidate);
+      }
+      catch (err)   {
+        console.log(err);
+      }
+    }
+    else {
+      this.candidatesQueue.push(_candidate);
+    }
+  };
+
+  flushCandidatesQueue () {
+    if (this.mediaId) {
+      try {
+        while(this.candidatesQueue.length) {
+          let candidate = this.candidatesQueue.shift();
+          this.mcs.addIceCandidate(this.mediaId, candidate);
+        }
+      }
+      catch (err) {
+        console.log(err);
+      }
+    }
+  }
+
+  mediaState (event) {
+    let msEvent = event.event;
+
+    switch (event.eventTag) {
+
+      case "OnIceCandidate":
+        //console.log("  [video] Sending ICE candidate to user => " + this.id);
+        let candidate = msEvent.candidate;
+        this.bbbGW.publish(JSON.stringify({
+          connectionId: this.sessionId,
+          type: 'video',
+          role: this.role,
+          id : 'iceCandidate',
+          cameraId: this.id,
+          candidate: candidate
+        }), C.FROM_VIDEO);
+        break;
+
+      case "MediaStateChanged":
+        break;
+
+      case "MediaFlowOutStateChange":
+      case "MediaFlowInStateChange":
+        console.log(' [video] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this.id);
+
+        if (msEvent.state === 'NOT_FLOWING') {
+          this.bbbGW.publish(JSON.stringify({
+            connectionId: this.sessionId,
+            type: 'video',
+            role: this.role,
+            id : 'playStop',
+            cameraId: this.id,
+          }), C.FROM_VIDEO);
+        }
+        else if (msEvent.state === 'FLOWING') {
+          this.bbbGW.publish(JSON.stringify({
+            connectionId: this.sessionId,
+            type: 'video',
+            role: this.role,
+            id : 'playStart',
+            cameraId: this.id,
+          }), C.FROM_VIDEO);
+        }
+
+        break;
+
+      default: console.log("  [video] Unrecognized event");
+    }
+  }
+
+  async start (sdpOffer, callback) {
+    console.log("  [video] start");
+    let sdpAnswer;
+
+    try {
+      this.userId = await this.mcs.join(this.meetingId, 'SFU', {});
+      console.log("  [video] Join returned => " + this.userId);
+
+      if (this.shared) {
+        const ret = await this.mcs.publish(this.userId, this.meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer});
+
+        this.mediaId = ret.sessionId;
+        sharedWebcams[this.id] = this.mediaId;
+        sdpAnswer = ret.answer;
+        this.flushCandidatesQueue();
+        this.mcs.on('MediaEvent' + this.mediaId, this.mediaState.bind(this));
+
+        console.log("  [video] Publish returned => " + this.mediaId);
+
+        return callback(null, sdpAnswer);
+      }
+      else {
+        const ret  = await this.mcs.subscribe(this.userId, sharedWebcams[this.id], 'WebRtcEndpoint', {descriptor: sdpOffer});
+
+        this.mediaId = ret.sessionId;
+        sdpAnswer = ret.answer;
+        this.flushCandidatesQueue();
+        this.mcs.on('MediaEvent' + this.mediaId, this.mediaState.bind(this));
+
+        console.log("  [video] Subscribe for user ", this.userId, " returned => " + this.mediaId);
+
+        return callback(null, sdpAnswer);
+      }
+    }
+    catch (err) {
+      console.log("  [video] MCS returned error => " + err);
+      return callback(err);
+    }
+  };
+
+  async stop () {
+    console.log(' [stop] Releasing endpoints for user ' + this.userId + ' at room ' + this.meetingId);
+
+    try {
+      await this.mcs.leave(this.meetingId, this.userId);
+      if (this.shared) {
+        sharedWebcams[this.id] = null;
+      }
+      this._candidatesQueue = null;
+      Promise.resolve();
+    }
+    catch (err) {
+      // TODO error handling
+      Promise.reject();
+    }
+    return;
+  };
+};
diff --git a/labs/kurento-screenshare/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/video/websocket.js
similarity index 100%
rename from labs/kurento-screenshare/lib/websocket.js
rename to labs/bbb-webrtc-sfu/lib/video/websocket.js
diff --git a/labs/bbb-webrtc-sfu/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/websocket.js
new file mode 100644
index 0000000000000000000000000000000000000000..c4fe9f6f18b220e8b4c43be58ec75004a6430124
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/websocket.js
@@ -0,0 +1,18 @@
+/*
+ * Simple wrapper around the ws library
+ *
+ */
+
+var ws = require('ws');
+
+ws.prototype.sendMessage = function(json) {
+
+  return this.send(JSON.stringify(json), function(error) {
+    if(error)
+      console.log(' [server] Websocket error "' + error + '" on message "' + json.id + '"');
+  });
+
+};
+
+
+module.exports = ws;
\ No newline at end of file
diff --git a/labs/bbb-webrtc-sfu/package-lock.json b/labs/bbb-webrtc-sfu/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..8fbfe4227510eb9e880d5bf83dc0de9fa24fb80e
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/package-lock.json
@@ -0,0 +1,371 @@
+{
+  "name": "bbb-webrtc-sfu",
+  "version": "0.0.2",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "argparse": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
+      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+      "requires": {
+        "sprintf-js": "1.0.3"
+      }
+    },
+    "asap": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+      "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+    },
+    "async": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz",
+      "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=",
+      "requires": {
+        "lodash": "4.17.4"
+      }
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
+    },
+    "backoff": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.3.0.tgz",
+      "integrity": "sha1-7nx+OAk/kuRyhZ22NedlJFT8Ieo="
+    },
+    "bindings": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
+      "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
+    },
+    "bufferutil": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz",
+      "integrity": "sha1-N75dNuHgZJIiHmjUdLGsWOUQy9c=",
+      "requires": {
+        "bindings": "1.2.1",
+        "nan": "2.7.0"
+      }
+    },
+    "commander": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz",
+      "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E="
+    },
+    "config": {
+      "version": "1.28.1",
+      "resolved": "https://registry.npmjs.org/config/-/config-1.28.1.tgz",
+      "integrity": "sha1-diXSoeTJDxMdinM0eYLZPDhzKC0=",
+      "requires": {
+        "json5": "0.4.0",
+        "os-homedir": "1.0.2"
+      }
+    },
+    "double-ended-queue": {
+      "version": "2.1.0-0",
+      "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
+      "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
+    },
+    "error-tojson": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/error-tojson/-/error-tojson-0.0.1.tgz",
+      "integrity": "sha1-p7GqlP/ADpB4wuuibiBL2Hzyy7k="
+    },
+    "es6-promise": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz",
+      "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng=="
+    },
+    "esprima": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+      "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
+    },
+    "extend": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+      "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
+    },
+    "hoek": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz",
+      "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ=="
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "isbuffer": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/isbuffer/-/isbuffer-0.0.0.tgz",
+      "integrity": "sha1-OMFG2d9Si4v5sHAcPUPPEt8/w5s="
+    },
+    "isemail": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.0.0.tgz",
+      "integrity": "sha512-rz0ng/c+fX+zACpLgDB8fnUQ845WSU06f4hlhk4K8TJxmR6f5hyvitu9a9JdMD7aq/P4E0XdG1uaab2OiXgHlA==",
+      "requires": {
+        "punycode": "2.1.0"
+      }
+    },
+    "joi": {
+      "version": "13.0.2",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-13.0.2.tgz",
+      "integrity": "sha512-kVka3LaHQyENvcMW4WJPSepGM43oCofcKxfs9HbbKd/FrwBAAt4lNNTPKOzSMmV53GIspmNO4U3O2TzoGvxxCA==",
+      "requires": {
+        "hoek": "5.0.2",
+        "isemail": "3.0.0",
+        "topo": "3.0.0"
+      }
+    },
+    "js-yaml": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
+      "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
+      "requires": {
+        "argparse": "1.0.9",
+        "esprima": "4.0.0"
+      }
+    },
+    "json5": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz",
+      "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0="
+    },
+    "kurento-client": {
+      "version": "git+https://github.com/Kurento/kurento-client-js.git#efb160e85a4b1f376307fe1979c9fbcb5f978393",
+      "requires": {
+        "async": "2.0.1",
+        "error-tojson": "0.0.1",
+        "es6-promise": "4.1.1",
+        "extend": "3.0.1",
+        "inherits": "2.0.3",
+        "kurento-client-core": "github:Kurento/kurento-client-core-js#2160f8e6938f138b52b72a5c5c354d1e5fce1ca0",
+        "kurento-client-elements": "github:Kurento/kurento-client-elements-js#cbd1ff67fbf0faddc9f6f266bb33e449bc9e1f81",
+        "kurento-client-filters": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0",
+        "kurento-jsonrpc": "github:Kurento/kurento-jsonrpc-js#827827bbeb557e1c1901f5a562c4c700b9a51401",
+        "minimist": "1.2.0",
+        "promise": "7.1.1",
+        "promisecallback": "0.0.4",
+        "reconnect-ws": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a"
+      },
+      "dependencies": {
+        "kurento-client-core": {
+          "version": "github:Kurento/kurento-client-core-js#2160f8e6938f138b52b72a5c5c354d1e5fce1ca0"
+        },
+        "kurento-client-elements": {
+          "version": "github:Kurento/kurento-client-elements-js#cbd1ff67fbf0faddc9f6f266bb33e449bc9e1f81"
+        },
+        "kurento-client-filters": {
+          "version": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0"
+        },
+        "kurento-jsonrpc": {
+          "version": "github:Kurento/kurento-jsonrpc-js#827827bbeb557e1c1901f5a562c4c700b9a51401",
+          "requires": {
+            "bufferutil": "1.2.1",
+            "inherits": "2.0.3",
+            "utf-8-validate": "1.2.2",
+            "ws": "1.1.5"
+          }
+        },
+        "reconnect-core": {
+          "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
+          "requires": {
+            "backoff": "2.3.0"
+          }
+        },
+        "reconnect-ws": {
+          "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a",
+          "requires": {
+            "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
+            "websocket-stream": "0.5.1"
+          }
+        },
+        "ws": {
+          "version": "1.1.5",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
+          "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==",
+          "requires": {
+            "options": "0.0.6",
+            "ultron": "1.0.2"
+          }
+        }
+      }
+    },
+    "lodash": {
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
+      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+    },
+    "minimist": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+    },
+    "moment": {
+      "version": "2.19.2",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.2.tgz",
+      "integrity": "sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA=="
+    },
+    "nan": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
+      "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY="
+    },
+    "options": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
+      "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+    },
+    "promise": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz",
+      "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=",
+      "requires": {
+        "asap": "2.0.6"
+      }
+    },
+    "promisecallback": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/promisecallback/-/promisecallback-0.0.4.tgz",
+      "integrity": "sha1-uTTxPATkQ2IrTWbeTkLqX2zmbnQ="
+    },
+    "punycode": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
+      "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
+    },
+    "redis": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
+      "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
+      "requires": {
+        "double-ended-queue": "2.1.0-0",
+        "redis-commands": "1.3.1",
+        "redis-parser": "2.6.0"
+      }
+    },
+    "redis-commands": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz",
+      "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs="
+    },
+    "redis-parser": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
+      "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
+    },
+    "safe-buffer": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
+    },
+    "sdp-transform": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
+      "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY="
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "tinycolor": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz",
+      "integrity": "sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ="
+    },
+    "topo": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz",
+      "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==",
+      "requires": {
+        "hoek": "5.0.2"
+      }
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po="
+    },
+    "utf-8-validate": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.2.tgz",
+      "integrity": "sha1-i7hxpHQeCFxwSHynrNvX1tNgKes=",
+      "requires": {
+        "bindings": "1.2.1",
+        "nan": "2.4.0"
+      },
+      "dependencies": {
+        "nan": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz",
+          "integrity": "sha1-+zxZ1F/k7/4hXwuJD4rfbrMtIjI="
+        }
+      }
+    },
+    "uuid": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
+      "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
+    },
+    "websocket-stream": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-0.5.1.tgz",
+      "integrity": "sha1-YizR8FZvuEzgpNb4VFJvPcTXDkg=",
+      "requires": {
+        "isbuffer": "0.0.0",
+        "through": "2.3.8",
+        "ws": "0.4.32"
+      },
+      "dependencies": {
+        "nan": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/nan/-/nan-1.0.0.tgz",
+          "integrity": "sha1-riT4hQgY1mL8q1rPfzuVv6oszzg="
+        },
+        "ws": {
+          "version": "0.4.32",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz",
+          "integrity": "sha1-eHphVEFPPJntg8V3IVOyD+sM7DI=",
+          "requires": {
+            "commander": "2.1.0",
+            "nan": "1.0.0",
+            "options": "0.0.6",
+            "tinycolor": "0.0.1"
+          }
+        }
+      }
+    },
+    "ws": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz",
+      "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==",
+      "requires": {
+        "async-limiter": "1.0.0",
+        "safe-buffer": "5.1.1",
+        "ultron": "1.1.1"
+      },
+      "dependencies": {
+        "ultron": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+          "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+        }
+      }
+    }
+  }
+}
diff --git a/labs/bbb-webrtc-sfu/package.json b/labs/bbb-webrtc-sfu/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..e7ed691d7824275af6e6c07153d9b7c8b5a00ba7
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/package.json
@@ -0,0 +1,23 @@
+{
+  "name": "bbb-webrtc-sfu",
+  "version": "0.0.2",
+  "private": true,
+  "scripts": {
+    "start": "node server.js"
+  },
+  "dependencies": {
+    "joi": "^13.0.2",
+    "kurento-client": "git+https://github.com/Kurento/kurento-client-js.git#master",
+    "moment": "^2.19.2",
+    "redis": "^2.8.0",
+    "sdp-transform": "^2.3.0",
+    "uuid": "^3.1.0",
+    "ws": "^3.3.2",
+    "config": "^1.26.1",
+    "js-yaml": "^3.8.3"
+  },
+  "devDependencies": {
+    "readable-id": "^1.0.0"
+  },
+  "optionalDependencies": {}
+}
diff --git a/labs/bbb-webrtc-sfu/server.js b/labs/bbb-webrtc-sfu/server.js
new file mode 100755
index 0000000000000000000000000000000000000000..3252a2b96febc93847754de12567273fafc6893f
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/server.js
@@ -0,0 +1,67 @@
+/*
+ * Lucas Fialho Zawacki
+ * Paulo Renato Lanzarin
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+const ConnectionManager = require('./lib/connection-manager/ConnectionManager');
+const HttpServer = require('./lib/connection-manager/HttpServer');
+const server = new HttpServer();
+const WebsocketConnectionManager = require('./lib/connection-manager/WebsocketConnectionManager');
+const cp = require('child_process');
+
+let screenshareProc = cp.fork('./lib/screenshare/ScreenshareProcess', {
+    // Pass over all of the environment.
+    env: process.ENV,
+    // Share stdout/stderr, so we can hear the inevitable errors.
+    silent: false
+});
+
+let videoProc = cp.fork('./lib/video/VideoProcess.js', {
+    // Pass over all of the environment.
+    env: process.ENV,
+    // Share stdout/stderr, so we can hear the inevitable errors.
+    silent: false
+});
+
+let onMessage = function (message) {
+  console.log('event','child message',this.pid,message);
+};
+
+let onError = function(e) {
+  console.log('event','child error',this.pid,e);
+};
+
+let onDisconnect = function(e) {
+  console.log(e);
+  console.log('event','child disconnect',this.pid,'killing...');
+  this.kill();
+};
+
+screenshareProc.on('message',onMessage);
+screenshareProc.on('error',onError);
+screenshareProc.on('disconnect',onDisconnect);
+
+videoProc.on('message',onMessage);
+videoProc.on('error',onError);
+videoProc.on('disconnect',onDisconnect);
+
+const CM = new ConnectionManager(screenshareProc, videoProc);
+
+let websocketManager = new WebsocketConnectionManager(server.getServerObject(), '/bbb-webrtc-sfu');
+
+process.on('SIGTERM', process.exit)
+process.on('SIGINT', process.exit)
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+  process.exit('1');
+});
+
+
+CM.setHttpServer(server);
+CM.addAdapter(websocketManager);
+
+CM.listen(() => {
+  console.log(" [SERVER] Server started");
+});
diff --git a/labs/kurento-screenshare/.gitignore b/labs/kurento-screenshare/.gitignore
deleted file mode 100644
index 40b878db5b1c97fc77049537a71bb2e249abe5dc..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules/
\ No newline at end of file
diff --git a/labs/kurento-screenshare/config/default.yml b/labs/kurento-screenshare/config/default.yml
deleted file mode 100644
index 46b6e2629ecf6e34603f5a34c1288266d0583918..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/config/default.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-kurentoUrl: "KURENTOURL"
-kurentoIp: "KURENTOIP"
-localIpAddress: "HOST"
-acceptSelfSignedCertificate: false
-redisHost : "127.0.0.1"
-redisPort : "6379"
-minVideoPort: 30000
-maxVideoPort: 33000
diff --git a/labs/kurento-screenshare/keys/README.md b/labs/kurento-screenshare/keys/README.md
deleted file mode 100644
index 5bc681a1c8d2ece88651b6ee63d410536eae50f6..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/keys/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-This folder contains a dummy self-signed certificate only for demo purposses,
-**DON'T USE IT IN PRODUCTION**.
diff --git a/labs/kurento-screenshare/keys/server.crt b/labs/kurento-screenshare/keys/server.crt
deleted file mode 100644
index 65e608dad5d9fb19f68ac486e6189dfc67dcd2ff..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/keys/server.crt
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDBjCCAe4CCQCuf5QfyX2oDDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
-VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
-cyBQdHkgTHRkMB4XDTE0MDkyOTA5NDczNVoXDTE1MDkyOTA5NDczNVowRTELMAkG
-A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
-IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-AMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5lFi3pBYWIY6kTN/iUaxJLROFo
-FhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP1UitWzVO6pVvBaIt5IKlhhfm
-YA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5VjKYc3OtEhcG8dgLAnOjbbk2Hr
-8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo9gs56urvVDWG4rhdGybj1uwU
-ZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0NjaU+MpSdEbB82z4b2NiN8Wq+
-rFA/JbvyeoWWHMoa7wkVs1MCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYLRwV9fo
-AOhJfeK199Tv6oXoNSSSe10pVLnYxPcczCVQ4b9SomKFJFbmwtPVGi6w3m+8mV7F
-9I2WKyeBHzmzfW2utZNupVybxgzEjuFLOVytSPdsB+DcJomOi8W/Cf2Vk8Wykb/t
-Ctr1gfOcI8rwEGKxm279spBs0u1snzoLyoimbMbiXbC82j1IiN3Jus08U07m/j7N
-hRBCpeHjUHT3CRpvYyTRnt+AyBd8BiyJB7nWmcNI1DksXPfehd62MAFS9e1ZE+dH
-Aavg/U8VpS7pcCQcPJvIJ2hehrt8L6kUk3YUYqZ0OeRZK27f2R5+wFlDF33esm3N
-dCSsLJlXyqAQFg==
------END CERTIFICATE-----
diff --git a/labs/kurento-screenshare/keys/server.csr b/labs/kurento-screenshare/keys/server.csr
deleted file mode 100644
index 6615b130471ce23cf8d980df5a308694ed06695b..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/keys/server.csr
+++ /dev/null
@@ -1,16 +0,0 @@
------BEGIN CERTIFICATE REQUEST-----
-MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
-ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
-AQEBBQADggEPADCCAQoCggEBAMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5l
-Fi3pBYWIY6kTN/iUaxJLROFoFhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP
-1UitWzVO6pVvBaIt5IKlhhfmYA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5Vj
-KYc3OtEhcG8dgLAnOjbbk2Hr8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo
-9gs56urvVDWG4rhdGybj1uwUZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0N
-jaU+MpSdEbB82z4b2NiN8Wq+rFA/JbvyeoWWHMoa7wkVs1MCAwEAAaAAMA0GCSqG
-SIb3DQEBCwUAA4IBAQBMszYHMpklgTF/3h1zAzKXUD9NrtZp8eWhL06nwVjQX8Ai
-EaCUiW0ypstokWcH9+30chd2OD++67NbxYUEucH8HrKpOoy6gs5L/mqgQ9Npz3OT
-TB1HI4kGtpVuUQ5D7L0596tKzMX/CgW/hRcHWl+PDkwGhQs1qZcJ8QN+YP6AkRrO
-5sDdDB/BLrB9PtBQbPrYIQcHQ7ooYWz/G+goqRxzZ6rt0aU2uAB6l7c82ADLAqFJ
-qlw+xqVzEETVfqM5TXKK/wV3hgm4oSX5Q4SHLKF94ODOkWcnV4nfIKz7y+5XcQ3p
-PrGimI1br07okC5rO9cgLCR0Ks20PPFcM0FvInW/
------END CERTIFICATE REQUEST-----
diff --git a/labs/kurento-screenshare/keys/server.key b/labs/kurento-screenshare/keys/server.key
deleted file mode 100644
index a69a0a279daf6a68b9eff057204cd05af1b27a5a..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/keys/server.key
+++ /dev/null
@@ -1,27 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIIEogIBAAKCAQEAwk7I4cn6slYkRDs/uQqhZrfV9/uEo1nEXqxgTmUWLekFhYhj
-qRM3+JRrEktE4WgWGgL8z9JNjvqsivKLHjvi//pxGgbw34ZAESfggA/VSK1bNU7q
-lW8Foi3kgqWGF+ZgDUgzB4J3Te8txodN102YUMFOSztAPB96dNpHlWMphzc60SFw
-bx2AsCc6NtuTYevzC16vYh42CSHJrfPKhedMtPybwgyDaZBomzrZeWj2Cznq6u9U
-NYbiuF0bJuPW7BRmINjI/gIUJQdLpRW3Xa8AM/y+NvCayzZJwawh/Q2NpT4ylJ0R
-sHzbPhvY2I3xar6sUD8lu/J6hZYcyhrvCRWzUwIDAQABAoIBACwt56TW3MZxqZtN
-8WYsUZheUispJ/ZQMcLo5JjOiSV1Jwk+gpJtyTse291z+bxagzP02/CQu4u32UVa
-cmE0cp+LHO4zB8964dREwdm8P91fdS6Au/uwG5LNZniCFCQZAFvkv52Ef4XbzQen
-uf4rKWerHBck6K0C5z/sZXxE6KtScE2ZLUmkhO0nkHM6MA6gFk2OMnB+oDTOWWPt
-1mlreQlzuMYG/D4axviRYrOSYCE5Qu1SOw/DEOLQqqeBjQrKtAyOlFHZsIR6lBfe
-KHMChPUcYIwaowt2DcqH/A+AFXRtaifa6DvH8Yul+2vAp47UEpaenVfM5bpN33XV
-EzerjtECgYEA+xiXzblek67iQgRpc9eHSoqs4iRLhae8s8kpAG51Jz46Je+Dmium
-XV769oiUGUxBeoUb7ryW+4MOzHJaA1BfGejQSvwLIB9e4cnikqnAArcqbcAcOCL1
-aYYDiSmSmN/AokNZlPKEBFXP9bzXrU9smQJWNTHlcRl7JXfnwF+jwNsCgYEAxhpE
-SBr9vlUVHNh/S6C5i80NIYg6jCy2FgsmuzEqmcqV0pTyzegmq8bru+QmuvoUj2o4
-nVv4J9d1fLF6ECUVk9aK8UdJOOB6hAfurOdJCArgrsY/9t4uDzXfbPCdfSNQITE0
-XgeNGQX1EzvwwkBmyZKk0kLIr3syP8ZCWfXDROkCgYBR+dF1pJMv++R6UR5sZ20P
-9P5ERj0xwXVl7MKqFWXCDhrFz9BTQPTrftrIKgbPy4mFCnf4FTHlov/t11dzxYWG
-2+9Ey8yGDDfZ1yNVZn39ZPdBJXsRCLi+XrZAzYXCyyoEz6ArdJGNKMbgH2r6dfeq
-bIzgiQ2zQvJlZSQQNiksCQKBgCgwzAmU8EXdHRttEOZXBU3HnBJhgP9PUuHGAWWY
-4/uvjhXbAiekIbRX9xt3fiQQ+HrgIfxK3F246K0TlKAR5f7IWAf7Xm+bmz+OHG4X
-vklTa6IJtpBvIwkS9PE1H75zm54gTW+GOKoK+12bm4zNZA0hIy9FPVHcvKUTpAJ8
-SdGBAoGAHLtJnB1NO4EgO6WtLQMXt7HrIbup8eZi8/82gC3422C+ooKIrYQ07qSw
-nBOO/G0OB4yd6vCE2x5+TWSSCYGgG5A8aIv5qP76RP4hovGHxG/y2tfotw5UuOrh
-nFWlTP4Urs8PeykvK9ao8r/T8BnPIC16U6ENYvAc0mRlFA2j1GA=
------END RSA PRIVATE KEY-----
diff --git a/labs/kurento-screenshare/lib/ConnectionManager.js b/labs/kurento-screenshare/lib/ConnectionManager.js
deleted file mode 100644
index 2dbef1e75d32f7759337ef0f56a3b11ced7d4ed6..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/lib/ConnectionManager.js
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Lucas Fialho Zawacki
- * Paulo Renato Lanzarin
- * (C) Copyright 2017 Bigbluebutton
- *
- */
-
-'use strict'
-
-const cookieParser = require('cookie-parser')
-const express = require('express');
-const session = require('express-session')
-const wsModule = require('./websocket');
-const http = require('http');
-const fs = require('fs');
-const BigBlueButtonGW = require('./bbb/pubsub/bbb-gw');
-var Screenshare = require('./screenshare');
-var C = require('./bbb/messages/Constants');
-
-// Global variables
-
-module.exports = class ConnectionManager {
-
-  constructor (settings, logger) {
-    this._logger = logger;
-    this._clientId = 0;
-    this._app = express();
-    this._screenshareSessions = {};
-
-    this._setupExpressSession();
-    this._setupHttpServer();
-  }
-
-  _setupExpressSession() {
-    this._app.use(cookieParser());
-
-    this._sessionHandler = session({
-      secret : 'Shawarma', rolling : true, resave : true, saveUninitialized : true
-    });
-
-    this._app.use(this._sessionHandler);
-  }
-
-  _setupHttpServer() {
-    let self = this;
-    /*
-     * Server startup
-     */
-    this._httpServer = http.createServer(this._app).listen(3008, function() {
-      console.log(' [*] Running node-apps connection manager.');
-    });
-
-    /*
-     * Management of sessions
-     */
-    this._wss = new wsModule.Server({
-      server : this._httpServer,
-      path : '/kurento-screenshare'
-    });
-
-
-    // TODO isolate this
-    this._bbbGW = new BigBlueButtonGW();
-
-    this._bbbGW.addSubscribeChannel(C.FROM_BBB_TRANSCODE_SYSTEM_CHAN, function(error, redisWrapper) {
-      if(error) {
-        console.log(' Could not connect to transcoder redis channel, finishing app...');
-        self._stopAll();
-      }
-      console.log('  [server] Successfully subscribed to redis channel');
-    });
-
-
-    this._wss.on('connection', self._onNewConnection.bind(self));
-  }
-
-  _onNewConnection(webSocket) {
-    let self = this;
-    let connectionId;
-    let request = webSocket.upgradeReq;
-    let sessionId;
-    let response = {
-      writeHead : {}
-    };
-
-    this._sessionHandler(request, response, function(err) {
-      connectionId = request.session.id + "_" + self._clientId++;
-      console.log('Connection received with connectionId ' + connectionId);
-    });
-
-    webSocket.on('error', function(error) {
-      console.log('Connection ' + connectionId + ' error');
-      self._stopSession(sessionId);
-    });
-
-    webSocket.on('close', function() {
-      console.log('Connection ' + connectionId + ' closed');
-      self._stopSession(sessionId);
-    });
-
-    webSocket.on('message', function(_message) {
-      let message = JSON.parse(_message);
-      let session;
-      // The sessionId is voiceBridge for screensharing sessions
-      sessionId = message.voiceBridge;
-
-      if(self._screenshareSessions[sessionId]) {
-        session = self._screenshareSessions[sessionId];
-      }
-
-      switch (message.id) {
-
-        case 'presenter':
-
-          // Checking if there's already a Screenshare session started
-          // because we shouldn't overwrite it
-
-          if(session) {
-            break;
-          }
-
-          session = new Screenshare(webSocket, connectionId, self._bbbGW,
-              sessionId, message.callerName, message.vh, message.vw,
-              message.internalMeetingId);
-
-          self._screenshareSessions[sessionId] = {}
-          self._screenshareSessions[sessionId] = session;
-
-          // starts presenter by sending sessionID, websocket and sdpoffer
-          session._startPresenter(connectionId, webSocket, message.sdpOffer, function(error, sdpAnswer) {
-            console.log(" Started presenter " + connectionId);
-            if (error) {
-              return webSocket.send(JSON.stringify({
-                id : 'presenterResponse',
-                response : 'rejected',
-                message : error
-              }));
-            }
-
-            webSocket.send(JSON.stringify({
-              id : 'presenterResponse',
-              response : 'accepted',
-              sdpAnswer : sdpAnswer
-            }));
-            console.log("  [websocket] Sending presenterResponse \n" + sdpAnswer);
-          });
-          break;
-
-        case 'viewer':
-          console.log('Viewer message => [' + message.id + '] connection [' + connectionId + '][' + message.presenterId + '][' + message.sessionId + '][' + message.callerName + ']');
-
-          break;
-        case 'stop':
-
-          console.log('[' + message.id + '] connection ' + connectionId);
-
-          if (session) {
-            session._stop(sessionId);
-          } else {
-            console.log(" [stop] Why is there no session on STOP?");
-          }
-          break;
-
-        case 'onIceCandidate':
-          if (session) {
-            session._onIceCandidate(message.candidate);
-          } else {
-            console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?");
-          }
-          break;
-
-        case 'ping':
-          webSocket.send(JSON.stringify({
-            id : 'pong',
-            response : 'accepted'
-          }));
-          break;
-
-        default:
-          webSocket.sendMessage({ id : 'error', message : 'Invalid message ' + message });
-          break;
-      }
-    });
-  }
-
-  _stopSession(sessionId) {
-    console.log(' [>] Stopping session ' + sessionId);
-    let session = this._screenshareSessions[sessionId];
-    if(typeof session !== 'undefined' && typeof session._stop === 'function') {
-      session._stop();
-    }
-
-    delete this._screenshareSessions[sessionId];
-  }
-
-  _stopAll() {
-    console.log('\n [x] Stopping everything! ');
-    let sessionIds = Object.keys(this._screenshareSessions);
-
-    for (let i = 0; i < sessionIds.length; i++) {
-      this._stopSession(sessionIds[i]);
-    }
-
-    setTimeout(process.exit, 1000);
-  }
-}
diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js b/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js
deleted file mode 100644
index da92167e4169e8a03ee3d3cb3e03b78405c1049f..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * @classdesc
- * Redis wrapper class for connecting to Redis channels
- */
-
-/* Modules */
-
-var redis = require('redis');
-var config = require('config');
-var Constants = require('../messages/Constants.js');
-var util = require('util');
-const EventEmitter = require('events').EventEmitter;
-const _retryThreshold = 1000 * 60 * 60;
-const _maxRetries = 10;
-
-
-/* Public members */
-
-var RedisWrapper = function(subpattern) {
-  // Redis PubSub client holders
-  this.redisCli = null;
-  this.redisPub = null;
-  // Pub and Sub channels/patterns
-  this.subpattern = subpattern;
-  EventEmitter.call(this);
-}
-
-util.inherits(RedisWrapper, EventEmitter);
-
-RedisWrapper.prototype.startRedis = function(callback) {
-  var self = this;
-  if (this.redisCli) {
-    console.log("  [RedisWrapper] Redis Client already exists");
-    callback(false, this);
-  }
-
-  var options = {
-    host : config.get('redisHost'),
-    port : config.get('redisPort'),
-    //password: config.get('redis.password')
-    retry_strategy: redisRetry
-  };
-
-  this.redisCli = redis.createClient(options);
-  this.redisPub = redis.createClient(options);
-
-  console.log("  [RedisWrapper] Trying to subscribe to redis channel");
-
-  this.redisCli.on("psubscribe", function (channel, count) {
-    console.log(" [RedisWrapper] Successfully subscribed to pattern [" + channel + "]");
-  });
-
-  this.redisCli.on("pmessage", self.onMessage.bind(self));
-  this.redisCli.psubscribe(this.subpattern);
-
-  console.log("  [RedisWrapper] Started Redis client at " + options.host + ":" + options.port +
-    " for subscription pattern: " + this.subpattern);
-
-  callback(false, this);
-};
-
-RedisWrapper.prototype.stopRedis = function(callback) {
-  if (this.redisCli){
-    this.redisCli.quit();
-  }
-  callback(false);
-};
-
-RedisWrapper.prototype.publishToChannel = function(message, channel) {
-  if(this.redisPub) {
-    console.log("  [RedisWrapper] Sending message to channel [" + channel + "]: " + message);
-    this.redisPub.publish(channel, message);
-  }
-};
-
-RedisWrapper.prototype.onMessage = function(pattern, channel, message) {
-  console.log(" [RedisWrapper] Message received from channel [" + channel +  "] : " + message);
-  // use event emitter to throw new message
-  this.emit(Constants.REDIS_MESSAGE, message);
-}
-
-/* Private members */
-
-function redisRetry(options) {
-  if (options.error && options.error.code === 'ECONNREFUSED') {
-    return new Error('The server refused the connection');
-  }
-  if (options.total_retry_time > _retryThreshold) {
-    return new Error('Retry time exhausted');
-  }
-  if (options.times_connected > _maxRetries) {
-    return undefined;
-  }
-  return Math.max(options.attempt * 100, 3000);
-};
-
-module.exports = RedisWrapper;
diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js b/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js
deleted file mode 100644
index 1abbb25aa010d9dc2a18d09edb2064c8516d9463..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * @classdesc
- * BigBlueButton redis gateway for bbb-screenshare node app
- */
-
-/* Modules */
-
-var C = require('../messages/Constants.js');
-var RedisWrapper = require('./RedisWrapper.js');
-var config = require('config');
-var util = require('util');
-var EventEmitter = require('events').EventEmitter;
-
-/* Public members */
-
-var BigBlueButtonGW = function () {
-  this.redisClients = null 
-  EventEmitter.call(this);
-};
-
-util.inherits(BigBlueButtonGW, EventEmitter);
-
-BigBlueButtonGW.prototype.addSubscribeChannel = function (channel, callback) {
-  var self = this;
-
-  if (this.redisClients === null) {
-    this.redisClients = {};
-  }
-
-  if (this.redisClients[channel]) {
-    return callback(null, this.redisClients[channel]);
-  }
-
-  var wrobj = new RedisWrapper(channel);
-  this.redisClients[channel] = {};
-  this.redisClients[channel] = wrobj;
-  wrobj.startRedis(function(error, redisCli) {
-    if(error) {
-      console.log("  [BigBlueButtonGW] Could not start redis client for channel " + channel);
-      return callback(error);
-    }
-
-    console.log("  [BigBlueButtonGW] Added redis client to this.redisClients[" + channel + "]");
-    wrobj.on(C.REDIS_MESSAGE, self.incomingMessage.bind(self));
-
-    return callback(null, wrobj);
-  });
-};
-
-/**
- * Capture messages from subscribed channels and emit an event with it's
- * identifier and payload. Check Constants.js for the identifiers.
- *
- * @param {Object} message  Redis message
- */
-BigBlueButtonGW.prototype.incomingMessage = function (message) {
-  var msg = JSON.parse(message);
-
-  // Trying to parse both message types, 1x and 2x
-  if (msg.header) {
-    var header = msg.header;
-    var payload = msg.payload;
-  }
-  else if (msg.core) {
-    var header = msg.core.header;
-    var payload = msg.core.body;
-  }
-
-  if (header){
-    switch (header.name) {
-      // interoperability with 1.1
-      case C.START_TRANSCODER_REPLY:
-        this.emit(C.START_TRANSCODER_REPLY, payload);
-        break;
-      case C.STOP_TRANSCODER_REPLY:
-        this.emit(C.STOP_TRANSCODER_REPLY, payload);
-        break;
-      // 2x messages
-      case C.START_TRANSCODER_RESP_2x:
-        payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x];
-
-        this.emit(C.START_TRANSCODER_RESP_2x, payload);
-        break;
-      case C.STOP_TRANSCODER_RESP_2x:
-        payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x];
-        this.emit(C.STOP_TRANSCODER_RESP_2x, payload);
-        break;
-
-      default:
-        console.log("  [BigBlueButtonGW] Unknown Redis message with ID =>" + header.name);
-    }
-  }
-};
-
-BigBlueButtonGW.prototype.publish = function (message, channel, callback) {
-  for(var client in this.redisClients) {
-    if(typeof this.redisClients[client].publishToChannel === 'function') {
-      this.redisClients[client].publishToChannel(message, channel);
-      return callback(null);
-    }
-  }
-  return callback("Client not found");
-};
-
-module.exports = BigBlueButtonGW;
diff --git a/labs/kurento-screenshare/lib/media-controller.js b/labs/kurento-screenshare/lib/media-controller.js
deleted file mode 100644
index df9697ec6984deb5f425fbb7c210fba4b6ce62e6..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/lib/media-controller.js
+++ /dev/null
@@ -1,141 +0,0 @@
-'use strict'
-
-const Constants = require('./bbb/messages/Constants.js');
-const config = require('config');
-const kurento = require('kurento-client');
-const mediaServerClient = null;
-
-var _mediaPipelines = {};
-var _mediaElements= {};
-
-function createMediaPipeline(id, callback) {
-  console.log(' [media] Creating media pipeline for ' + id);
-  getMediaServerClient(function (error, mediaServerClient) {
-    mediaServerClient.create('MediaPipeline', function(err, pipeline) {
-      if (error) {
-        console.log("Could not find media server at address " + kurentoUrl);
-        return callback(error);
-      }
-      return callback(null , pipeline);
-    });
-  });
-};
-
-function getMediaServerClient (callback) {
-  let kurentoUrl = config.get('kurentoUrl');
-  if (mediaServerClient) {
-    callback(null, mediaServerClient);
-  }
-  else {
-    kurento(kurentoUrl, function(error, _mediaServerClient) {
-      if (error) {
-        console.log("Could not find media server at address " + kurentoUrl);
-        return callback(error, null);
-      }
-
-      console.log(" [server] Initiating kurento client. Connecting to: " + kurentoUrl);
-      return callback(null, _mediaServerClient);
-    });
-  }
-};
-
-/* Public members */
-module.exports = {
-
-  createMediaElement : function (conference, type, callback) {
-    let self = this;
-    self.getMediaPipeline(conference, function(error, pipeline) {
-
-      pipeline.create(type, function(error, mediaElement) {
-        if (error) {
-          return callback(error, null);
-        }
-        console.log("  [MediaController] Created [" + type + "] media element: " + mediaElement.id);
-        _mediaElements[mediaElement.id] = mediaElement;
-        return callback(null, mediaElement);
-      });
-    });
-  },
-
-  connectMediaElements : function (sourceId, sinkId, type, callback) {
-    let source = _mediaElements[sourceId];
-    let sink = _mediaElements[sinkId];
-
-    if (source && sink) {
-      if (type === 'ALL') {
-        source.connect(sink, function (error) {
-          return callback (error);
-        });
-      } else {
-        console.log(typeof source.connect);
-        source.connect(sink, type, function (error) {
-          return callback (error);
-        });
-      }
-    } else {
-      return callback ("Failed to connect " + type + ": " + sourceId + " to " + sinkId);
-    }
-  },
-
-  releaseMediaElement : function (elementId) {
-    let mediaElement = _mediaElements[elementId];
-
-    if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') {
-      mediaElement.release();
-    }
-  },
-
-  releasePipeline: function (pipelineId) {
-    let MediaPipeline = _mediaPipelines[pipelineId];
-
-    if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') {
-      mediaElement.release();
-    }
-  },
-
-  processOffer : function (elementId, sdpOffer, callback) {
-    let mediaElement = _mediaElements[elementId];
-
-    if (typeof mediaElement !== 'undefined' && typeof mediaElement.processOffer === 'function') {
-      mediaElement.processOffer (sdpOffer, function (error, sdpAnswer) {
-        return callback (error, sdpAnswer);
-      });
-    } else {
-      return callback ("  [MediaController/processOffer] There is no element " + elementId, null);
-    }
-  },
-
-  getMediaPipeline : function(conference, callback) {
-    let self = this;
-
-    if (_mediaPipelines[conference]) {
-      console.log(' [media] Pipeline already exists. ' + JSON.stringify(_mediaPipelines, null, 2));
-      return callback(null, _mediaPipelines[conference]);
-    } else {
-      createMediaPipeline(conference, function(error, pipeline) {
-        _mediaPipelines[conference] = pipeline;
-        return callback(error, pipeline);
-      });
-    }
-  },
-
-  addIceCandidate : function (elementId, candidate) {
-    let mediaElement = _mediaElements[elementId];
-
-    if (typeof mediaElement !== 'undefined' && typeof mediaElement.addIceCandidate === 'function') {
-      mediaElement.addIceCandidate(candidate);
-    }
-  },
-
-  gatherCandidates : function (elementId, callback) {
-    let mediaElement = _mediaElements[elementId];
-
-    if (typeof mediaElement !== 'undefined' && typeof mediaElement.gatherCandidates === 'function') {
-      mediaElement.gatherCandidates(function (error) {
-        return callback(error);  
-      });
-    } else {
-      return callback ("  [MediaController/gatherCandidates] There is no element " + elementId, null);
-    }
-  },
-};
diff --git a/labs/kurento-screenshare/lib/screenshare.js b/labs/kurento-screenshare/lib/screenshare.js
deleted file mode 100644
index 411fbd54a9ac3cf994f7e0f703ed7c3f32f8718e..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/lib/screenshare.js
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * Lucas Fialho Zawacki
- * Paulo Renato Lanzarin
- * (C) Copyright 2017 Bigbluebutton
- *
- */
-
-'use strict'
-
-// Imports
-const C = require('./bbb/messages/Constants');
-const MediaHandler = require('./media-handler');
-const Messaging = require('./bbb/messages/Messaging');
-const moment = require('moment');
-const h264_sdp = require('./h264-sdp');
-const now = moment();
-const MediaController = require('./media-controller');
-
-// Global stuff
-var sharedScreens = {};
-var rtpEndpoints = {};
-
-const kurento = require('kurento-client');
-const config = require('config');
-const kurentoUrl = config.get('kurentoUrl');
-const kurentoIp = config.get('kurentoIp');
-const localIpAddress = config.get('localIpAddress');
-
-if (config.get('acceptSelfSignedCertificate')) {
-  process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
-}
-
-module.exports = class Screenshare {
-  constructor(ws, id, bbbgw, voiceBridge, caller, vh, vw, meetingId) {
-    this._ws = ws;
-    this._id = id;
-    this._BigBlueButtonGW = bbbgw;
-    this._presenterEndpoint = null;
-    this._ffmpegRtpEndpoint = null;
-    this._voiceBridge = voiceBridge;
-    this._meetingId = meetingId;
-    this._caller = caller;
-    this._streamUrl = "";
-    this._vw = vw;
-    this._vh = vh;
-    this._candidatesQueue = [];
-  }
-
-  // TODO isolate ICE
-  _onIceCandidate(_candidate) {
-    let candidate = kurento.getComplexType('IceCandidate')(_candidate);
-
-    if (this._presenterEndpoint) {
-      this._presenterEndpoint.addIceCandidate(candidate);
-    }
-    else {
-      this._candidatesQueue.push(candidate);
-    }
-  };
-
-  _startPresenter(id, ws, sdpOffer, callback) {
-    let self = this;
-    let _callback = callback;
-
-    // Force H264 on Firefox and Chrome
-    sdpOffer = h264_sdp.transform(sdpOffer);
-    console.log("Starting presenter for " + sdpOffer);
-    MediaController.createMediaElement(self._voiceBridge, C.WebRTC, function(error, webRtcEndpoint) {
-      if (error) {
-        console.log("Media elements error" + error);
-        return _callback(error);
-      }
-      MediaController.createMediaElement(self._voiceBridge, C.RTP, function(error, rtpEndpoint) {
-        if (error) {
-          console.log("Media elements error" + error);
-          return _callback(error);
-        }
-
-
-        while(self._candidatesQueue.length) {
-          let candidate = self._candidatesQueue.shift();
-          MediaController.addIceCandidate(webRtcEndpoint.id, candidate);
-        }
-
-        MediaController.connectMediaElements(webRtcEndpoint.id, rtpEndpoint.id, C.VIDEO, function(error) {
-          if (error) {
-            console.log("Media elements CONNECT error " + error);
-            //pipeline.release();
-            return _callback(error);
-          }
-
-          // It's a user sharing a Screen
-          sharedScreens[id] = webRtcEndpoint;
-          rtpEndpoints[id] = rtpEndpoint;
-
-          // Store our endpoint
-          self._presenterEndpoint = webRtcEndpoint;
-          self._ffmpegRtpEndpoint = rtpEndpoint;
-
-          self._presenterEndpoint.on('OnIceCandidate', function(event) {
-            let candidate = kurento.getComplexType('IceCandidate')(event.candidate);
-            ws.sendMessage({ id : 'iceCandidate', cameraId: id, candidate : candidate });
-          });
-
-          MediaController.processOffer(webRtcEndpoint.id, sdpOffer, function(error, webRtcSdpAnswer) {
-            if (error) {
-              console.log("  [webrtc] processOffer error => " + error + " for SDP " + sdpOffer);
-              //pipeline.release();
-              return _callback(error);
-            }
-
-            let sendVideoPort = MediaHandler.getVideoPort();
-
-            let rtpSdpOffer = MediaHandler.generateVideoSdp(localIpAddress, sendVideoPort);
-            console.log("  [rtpendpoint] RtpEndpoint processing => " + rtpSdpOffer);
-
-            MediaController.gatherCandidates(webRtcEndpoint.id, function(error) {
-              if (error) {
-                return _callback(error);
-              }
-
-              MediaController.processOffer(rtpEndpoint.id, rtpSdpOffer, function(error, rtpSdpAnswer) {
-                if (error) {
-                  console.log("  [rtpendpoint] processOffer error => " + error + " for SDP " + rtpSdpOffer);
-                  //pipeline.release();
-                  return _callback(error);
-                }
-
-                console.log("  [rtpendpoint] KMS answer SDP => " + rtpSdpAnswer);
-                let recvVideoPort = rtpSdpAnswer.match(/m=video\s(\d*)/)[1];
-                let rtpParams = MediaHandler.generateTranscoderParams(kurentoIp, localIpAddress,
-                    sendVideoPort, recvVideoPort, self._meetingId, "stream_type_video", C.RTP_TO_RTMP, "copy", "caller");
-
-                self._ffmpegRtpEndpoint.on('MediaFlowInStateChange', function(event) {
-                  if (event.state === 'NOT_FLOWING') {
-                    self._onRtpMediaNotFlowing();
-                  }
-                  else if (event.state === 'FLOWING') {
-                    self._onRtpMediaFlowing(self._meetingId, rtpParams);
-                  }
-                });
-                return _callback(null, webRtcSdpAnswer);
-              });
-            });
-          });
-        });
-      });
-    });
-  };
-
-  _stop() {
-
-    console.log(' [stop] Releasing endpoints for ' + this._id);
-
-    this._stopScreensharing();
-
-    if (this._presenterEndpoint) {
-      MediaController.releaseMediaElement(this._presenterEndpoint.id);
-      this._presenterEndpoint = null;
-    } else {
-      console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE");
-    }
-
-    if (this._ffmpegRtpEndpoint) {
-      MediaController.releaseMediaElement(this._ffmpegRtpEndpoint.id);
-      this._ffmpegRtpEndpoint = null;
-    } else {
-      console.log(" [rtpEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE");
-    }
-
-    console.log(' [stop] Screen is shared, releasing ' + this._id);
-
-    delete sharedScreens[this._id];
-
-    delete this._candidatesQueue;
-  };
-
-  _stopScreensharing() {
-    let self = this;
-    let strm = Messaging.generateStopTranscoderRequestMessage(this._meetingId, this._meetingId);
-
-    self._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {});
-
-    // Interoperability: capturing 1.1 stop_transcoder_reply messages
-    self._BigBlueButtonGW.once(C.STOP_TRANSCODER_REPLY, function(payload) {
-      let meetingId = payload[C.MEETING_ID];
-      self._stopRtmpBroadcast(meetingId);
-    });
-
-    // Capturing stop transcoder responses from the 2x model
-    self._BigBlueButtonGW.once(C.STOP_TRANSCODER_RESP_2x, function(payload) {
-      let meetingId = payload[C.MEETING_ID_2x];
-      self._stopRtmpBroadcast(meetingId);
-    });
-
-  }
-
-  _onRtpMediaFlowing(meetingId, rtpParams) {
-    let self = this;
-    let strm = Messaging.generateStartTranscoderRequestMessage(meetingId, meetingId, rtpParams);
-
-    // Interoperability: capturing 1.1 start_transcoder_reply messages
-    self._BigBlueButtonGW.once(C.START_TRANSCODER_REPLY, function(payload) {
-      let meetingId = payload[C.MEETING_ID];
-      let output = payload["params"].output;
-      self._startRtmpBroadcast(meetingId, output);
-    });
-
-    // Capturing stop transcoder responses from the 2x model
-    self._BigBlueButtonGW.once(C.START_TRANSCODER_RESP_2x, function(payload) {
-      let meetingId = payload[C.MEETING_ID_2x];
-      let output = payload["params"].output;
-      self._startRtmpBroadcast(meetingId, output);
-    });
-
-
-    self._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {});
-  };
-
-  _stopRtmpBroadcast (meetingId) {
-    var self = this;
-    if(self._meetingId === meetingId) {
-      // TODO correctly assemble this timestamp
-      let timestamp = now.format('hhmmss');
-      let dsrstom = Messaging.generateScreenshareRTMPBroadcastStoppedEvent2x(self._voiceBridge,
-          self._voiceBridge, self._streamUrl, self._vw, self._vh, timestamp);
-      self._BigBlueButtonGW.publish(dsrstom, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {});
-    }
-  }
-
-  _startRtmpBroadcast (meetingId, output) {
-    var self = this;
-    if(self._meetingId === meetingId) {
-      // TODO correctly assemble this timestamp
-      let timestamp = now.format('hhmmss');
-      self._streamUrl = MediaHandler.generateStreamUrl(localIpAddress, meetingId, output);
-      let dsrbstam = Messaging.generateScreenshareRTMPBroadcastStartedEvent2x(self._voiceBridge,
-          self._voiceBridge, self._streamUrl, self._vw, self._vh, timestamp);
-
-      self._BigBlueButtonGW.publish(dsrbstam, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {});
-    }
-  }
-
-  _onRtpMediaNotFlowing() {
-    console.log("  [screenshare] TODO RTP NOT_FLOWING");
-  };
-
-
-};
diff --git a/labs/kurento-screenshare/package.json b/labs/kurento-screenshare/package.json
deleted file mode 100644
index 187a3ab8a3f2e70c689e740178834ba1fadb5146..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "name": "bbb-screenshare-video-kurento-bridge",
-  "version": "1.0.0",
-  "private": true,
-  "scripts": {
-    "start": "nodejs server.js",
-    "postinstall": "npm start"
-  },
-  "dependencies": {
-    "cookie-parser": "^1.3.5",
-    "express": "~4.12.4",
-    "express-session": "~1.10.3",
-    "ws": "~1.0.1",
-    "kurento-client": "6.6.0",
-    "redis": "^2.6.2",
-    "sdp-transform": "*",
-    "moment": "*"
-  },
-  "devDependencies": {
-    "config": "^1.26.1",
-    "js-yaml": "^3.8.3"
-  }
-}
diff --git a/labs/kurento-screenshare/server.js b/labs/kurento-screenshare/server.js
deleted file mode 100755
index f645ac88a52ffa328944f6f775b868bda3b0ae34..0000000000000000000000000000000000000000
--- a/labs/kurento-screenshare/server.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * Paulo Renato Lanzarin
- * (C) Copyright 2017 Bigbluebutton
- *
- */
-
-const ConnectionManager = require('./lib/ConnectionManager');
-const CM = new ConnectionManager(); 
-
-process.on('SIGTERM', CM._stopAll.bind(CM));
-process.on('SIGINT', CM._stopAll.bind(CM));