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..7dbf35321bdd46b50cafdf6bea1347be55c79e59 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
@@ -7,11 +7,11 @@ case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String)
 
 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 0ddab1bbc734a0b18aa95ab92ade5e2156ce0f73..18b860a48d0c1c5c31e4a6408dc91968e34d2b7f 100755
--- a/bigbluebutton-client/resources/config.xml.template
+++ b/bigbluebutton-client/resources/config.xml.template
@@ -49,6 +49,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 15a8c952d1c11d41806e92b88b59900173be631d..a799be30e65458465ffedbe1e3d610741d310b9b 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..386e85339e92b6916bc595731d7d8387261633ad 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;
@@ -43,6 +44,7 @@ Kurento = function (
 
   if (chromeExtension != null) {
     this.chromeExtension = chromeExtension;
+    window.chromeExtension = chromeExtension;
   }
 
   if (onFail != null) {
@@ -57,21 +59,52 @@ 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.pingInterval) {
+      clearInterval(this.kurentoScreenshare.pingInterval);
     }
-    if(kurentoHandler.ws !== null) {
-      kurentoHandler.ws.onclose = function(){};
-      kurentoHandler.ws.close();
+
+    if(this.kurentoScreenshare.ws !== null) {
+      this.kurentoScreenshare.ws.onclose = function(){};
+      this.kurentoScreenshare.ws.close();
     }
-    kurentoHandler.disposeScreenShare();
-    this.kurentoScreenShare = null;
-    kurentoHandler = null;
+
+    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.pingInterval) {
+      clearInterval(this.kurentoVideo.pingInterval);
+    }
+
+    if(this.kurentoVideo.ws !== null) {
+      this.kurentoVideo.ws.onclose = function(){};
+      this.kurentoVideo.ws.close();
+    }
+
+    this.kurentoVideo.disposeScreenShare();
+    this.kurentoVideo = null;
+  }
+
+  if (this.kurentoVideo) {
+    this.kurentoVideo = null;
   }
 };
 
@@ -79,24 +112,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 +142,19 @@ 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.pingInterval = setInterval(self.ping.bind(self), 3000);
       self.mediaCallback();
-    };
+    }.bind(self);
   }
   else
     console.log("this browser does not support websockets");
@@ -135,13 +165,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);
+      this.webRtcPeer.addIceCandidate(parsedMessage.candidate);
       break;
     case 'pong':
       break;
@@ -159,18 +192,30 @@ Kurento.prototype.presenterResponse = function (message) {
     var errorMsg = message.message ? message.message : 'Unknow error';
     console.warn('Call not accepted for the following reason: ' + errorMsg);
     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 +223,102 @@ 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,
+    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',
     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',
+    type: 'screenshare',
+    voiceBridge: self.voiceBridge,
+    candidate : candidate,
+    callerName: self.caller_id_name
+  }
+  this.sendMessage(message);
 }
 
 Kurento.prototype.setWatchVideo = function (tag) {
@@ -276,60 +334,61 @@ 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,
+    id : 'viewer', type: 'screenshare',
+    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() {
+    let self = this;
    var message = {
     id : 'ping',
     type: 'screenshare',
-    internalMeetingId: kurentoHandler.internalMeetingId,
-    voiceBridge: kurentoHandler.voiceBridge,
-    callerName : kurentoHandler.caller_id_name,
+    internalMeetingId: self.internalMeetingId,
+    voiceBridge: self.voiceBridge,
+    callerName : self.caller_id_name,
   };
 
-  kurentoHandler.sendMessage(message);
+  this.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 +419,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 +435,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 +495,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-html5/client/main.html b/bigbluebutton-html5/client/main.html
index fb01946b07731e6b8c834346614839b865261b5b..942e492c1a348abef542e318decf2231eba5e0c0 100755
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -51,4 +51,13 @@
   <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>
+  <!--
+    TODO: find a better way to include this
+    Libs needed for kurento clientside communication.
+  -->
+  <script src="/html5client/js/bower_components/reconnectingWebsocket/reconnecting-websocket.js"></script>
+  <script src="/html5client/js/bower_components/adapter.js/release/adapter.js"></script>
+  <script src="/html5client/js/bower_components/kurento-utils/dist/kurento-utils.js"></script>
+  <script src="/html5client/js/adjust-videos.js"></script>
+  <script src="/client/lib/kurento-extension.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..00f12f5b180849329ea65ebf3b888d3fb61b70d2
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -0,0 +1,49 @@
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth';
+import BridgeService from './service';
+
+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,
+      null,
+    );
+  }
+
+  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..e20b6cd39a60214325a75d8f3428ece95eda1940
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js
@@ -0,0 +1,6 @@
+import RedisPubSub from '/imports/startup/server/redis2x';
+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..b0c21ee31cf51227d5e3aef827fb58c0f4aa56ec
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js
@@ -0,0 +1,10 @@
+import sharedWebcam from '../modifiers/sharedWebcam';
+
+export default function handleUserSharedHtml5Webcam({ header, payload }) {
+  const meetingId = header.meetingId;
+  const userId = header.userId;
+
+  check(meetingId, 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..bf0f0994f9e453285d70fc095869715c1d4cd7c6
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js
@@ -0,0 +1,10 @@
+import unsharedWebcam from '../modifiers/unsharedWebcam';
+
+export default function handleUserUnsharedHtml5Webcam({ header, payload }) {
+  const meetingId = header.meetingId;
+  const userId = header.userId;
+
+  check(meetingId, 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..6121725948bb5c968bf00df07ca2c4cb0c0a6dc0
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js
@@ -0,0 +1,38 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import Logger from '/imports/startup/server/logger';
+import RedisPubSub from '/imports/startup/server/redis2x';
+
+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,
+  };
+
+  const header = {
+    meetingId,
+    name: EVENT_NAME,
+    userId: requesterUserId,
+  };
+
+  return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header);
+}
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..62b83be491441184ea151c73d9c215974ebb4e8f
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js
@@ -0,0 +1,38 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import Logger from '/imports/startup/server/logger';
+import RedisPubSub from '/imports/startup/server/redis2x';
+
+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,
+  };
+
+  const header = {
+    meetingId,
+    name: EVENT_NAME,
+    userId: requesterUserId,
+  };
+
+  return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header);
+}
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..0a8f96321ba19e5296fa8baf16214a136a4e124b
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js
@@ -0,0 +1,32 @@
+import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
+
+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..817031199b1053c7366fe96b6308eecd1c958b62
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js
@@ -0,0 +1,32 @@
+import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
+
+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 4cfa5c6b62a1c0d0f394b60003679a81f057048d..cccf78cc5b2a9bb1c4d01f749f927ebd839b0593 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
@@ -31,6 +31,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 {
@@ -52,7 +68,13 @@ class ActionsDropdown extends Component {
   }
 
   render() {
-    const { intl, isUserPresenter } = this.props;
+    const {
+      intl,
+      isUserPresenter,
+      handleShareScreen,
+      handleUnshareScreen,
+      isVideoBroadcasting,
+    } = this.props;
 
     if (!isUserPresenter) return null;
 
@@ -76,6 +98,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 ad405b15fd069caf4e9ba80d590cf90fb0bdf962..4412ec793fc341fee7836720065ad6f36760bf1b 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -3,17 +3,30 @@ import styles from './styles.scss';
 import EmojiContainer from './emoji-menu/container';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
+import JoinVideoOptionsContainer from '../video-dock/video-menu/container';
 
 const ActionsBar = ({
   isUserPresenter,
+  handleExitAudio,
+  handleOpenJoinAudio,
+  handleExitVideo,
+  handleJoinVideo,
+  handleShareScreen,
+  handleUnshareScreen,
+  isVideoBroadcasting,
 }) => (
   <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}
       <EmojiContainer />
     </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 1037966d1dd4b3a3801fb819d34f28d170cbd464..bcc1935453241983d209ef7f99bdbcf9bcea50c9 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -4,6 +4,8 @@ import { withModalMounter } from '/imports/ui/components/modal/service';
 import ActionsBar from './component';
 import Service from './service';
 import AudioService from '../audio/service';
+import VideoService from '../video-dock/service';
+import ScreenshareService from '../screenshare/service';
 
 import AudioModal from '../audio/audio-modal/component';
 
@@ -19,10 +21,20 @@ export default withModalMounter(createContainer(({ mountModal }) => {
   const handleExitAudio = () => AudioService.exitAudio();
   const handleOpenJoinAudio = () =>
     mountModal(<AudioModal handleJoinListenOnly={AudioService.joinListenOnly} />);
+  const handleExitVideo = () => VideoService.exitVideo();
+  const handleJoinVideo = () => VideoService.joinVideo();
+  const handleShareScreen = () => ScreenshareService.shareScreen();
+  const handleUnshareScreen = () => ScreenshareService.unshareScreen();
+  const isVideoBroadcasting = () => ScreenshareService.isVideoBroadcasting();
 
   return {
     isUserPresenter: isPresenter,
     handleExitAudio,
     handleOpenJoinAudio,
+    handleExitVideo,
+    handleJoinVideo,
+    handleShareScreen,
+    handleUnshareScreen,
+    isVideoBroadcasting
   };
 }, ActionsBarContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 88abd7c9b6e1e4d076700b4a8e27247437c05537..d8d0b54459de4d3f028644695ea34bd7184b6287 100644
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -7,6 +7,7 @@ import Auth from '/imports/ui/services/auth';
 import Users from '/imports/api/users';
 import Breakouts from '/imports/api/breakouts';
 import Meetings from '/imports/api/meetings';
+import Screenshare from '/imports/api/screenshare';
 
 import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
 
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..90a1bd17d8a975b4dac305f8d902cee595b88ab8 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx
@@ -1,11 +1,317 @@
-import React from 'react';
+import React, { Component } from 'react';
 import ScreenshareContainer from '/imports/ui/components/screenshare/container';
 import styles from './styles';
 
-const VideoDock = () => (
-  <div className={styles.videoDock}>
-    <ScreenshareContainer />
-  </div>
-);
+window.addEventListener('resize', () => {
+  window.adjustVideos('webcamArea', true);
+});
 
-export default VideoDock;
+class VideoElement extends Component {
+  constructor(props) {
+    super(props);
+  }
+}
+
+
+export default class VideoDock extends Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      videos: {}
+    };
+
+    this.state = {
+      // Set a valid kurento application server socket in the settings
+      ws: new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl),
+      webRtcPeers: {},
+      wsQueue: [],
+    };
+
+    this.state.ws.onopen = () => {
+      while (this.state.wsQueue.length > 0) {
+        this.sendMessage(this.state.wsQueue.pop());
+      }
+    };
+
+    this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this);
+    this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this);
+
+    this.unshareWebcam = this.unshareWebcam.bind(this);
+    this.shareWebcam = this.shareWebcam.bind(this);
+  }
+
+  componentDidMount() {
+    const that = this;
+    const ws = this.state.ws;
+    const { users } = this.props;
+    for (let i = 0; i < users.length; i++) {
+      if (users[i].has_stream) {
+        console.log("COMPONENT DID MOUNT => " + users[i].userId);
+        this.start(users[i].userId, false, this.refs.videoInput);
+      }
+    }
+
+    document.addEventListener('joinVideo', () => { that.shareWebcam(); });// TODO find a better way to do this
+    document.addEventListener('exitVideo', () => { that.unshareWebcam(); });
+
+    ws.addEventListener('message', (msg) => {
+      const parsedMessage = JSON.parse(msg.data);
+
+      console.debug('Received message new ws message: ');
+      console.debug(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.state.webRtcPeers[parsedMessage.cameraId];
+
+          if (webRtcPeer !== null) {
+            webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => {
+              if (err) {
+                return console.error(`Error adding candidate: ${err}`);
+              }
+            });
+          } else {
+            console.error(' [ICE] Message arrived before webRtcPeer?');
+          }
+
+          break;
+
+      }
+    });
+  }
+
+  start(id, shareWebcam, videoInput) {
+    const that = this;
+
+    const ws = this.state.ws;
+
+    console.log(`Starting video call for video: ${id}`);
+    console.log('Creating WebRtcPeer and generating local sdp offer ...');
+
+    const onIceCandidate = function (candidate) {
+      const message = {
+        id: 'onIceCandidate',
+        candidate,
+        cameraId: id,
+      };
+      that.sendMessage(message);
+    };
+
+    const options = {
+      mediaConstraints: { audio: false,
+        video: {
+          width: {min: 320, ideal: 320},
+          height: {min: 240, ideal:240},
+          frameRate: { min: 5, ideal: 10}
+        }
+      },
+      onicecandidate: onIceCandidate,
+    };
+
+    let peerObj;
+    if (shareWebcam) {
+      options.localVideo = videoInput;
+      peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
+    } else {
+      peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
+
+      options.remoteVideo = document.createElement('video');
+      options.remoteVideo.id = `video-elem-${id}`;
+      options.remoteVideo.width = 120;
+      options.remoteVideo.height = 90;
+      options.remoteVideo.autoplay = true;
+      options.remoteVideo.playsinline = true;
+
+      document.getElementById('webcamArea').appendChild(options.remoteVideo);
+    }
+
+    this.state.webRtcPeers[id] = new peerObj(options, function (error) {
+      if (error) {
+        console.error(' [ERROR] Webrtc error');
+        console.error(error);
+        return;
+      }
+
+      if (shareWebcam) {
+        that.state.sharedWebcam = that.state.webRtcPeers[id];
+        that.state.myId = id;
+      }
+
+      this.generateOffer((error, offerSdp) => {
+        if (error) {
+          return console.error(error);
+        }
+
+        console.info(`Invoking SDP offer callback function ${location.host}`);
+        const message = {
+          id: 'start',
+          sdpOffer: offerSdp,
+          cameraId: id,
+          cameraShared: shareWebcam,
+        };
+        that.sendMessage(message);
+      });
+    });
+  }
+
+  stop(id) {
+    const { users } = this.props;
+    if (id == users[0].userId) {
+      this.unshareWebcam();
+    }
+    const webRtcPeer = this.state.webRtcPeers[id];
+
+    if (webRtcPeer) {
+      console.log('Stopping WebRTC peer');
+
+      if (id == this.state.myId) {
+        this.state.sharedWebcam.dispose();
+        this.state.sharedWebcam = null;
+      }
+
+      webRtcPeer.dispose();
+      delete this.state.webRtcPeers[id];
+    } else {
+      console.log('NO WEBRTC PEER TO STOP?');
+    }
+
+    const videoTag = document.getElementById(`video-elem-${id}`);
+    if (videoTag) {
+      document.getElementById('webcamArea').removeChild(videoTag);
+    }
+
+    this.sendMessage({ id: 'stop', cameraId: id });
+
+    window.adjustVideos('webcamArea', true);
+  }
+
+  shareWebcam() {
+    const { users } = this.props;
+    const id = users[0].userId;
+
+    this.start(id, true, this.refs.videoInput);
+  }
+
+  unshareWebcam() {
+    console.log("Unsharing webcam");
+    const { users } = this.props;
+    const id = users[0].userId;
+    this.sendUserUnshareWebcam(id);
+  }
+
+  startResponse(message) {
+    const id = message.cameraId;
+    const webRtcPeer = this.state.webRtcPeers[id];
+
+    if (message.sdpAnswer == null) {
+      return console.debug('Null sdp answer. Camera unplugged?');
+    }
+
+    if (webRtcPeer == null) {
+      return console.debug('Null webrtc peer ????');
+    }
+
+    console.log('SDP answer received from server. Processing ...');
+
+    webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
+      if (error) {
+        return console.error(error);
+      }
+    });
+
+    this.sendUserShareWebcam(id);
+  }
+
+  sendMessage(message) {
+    const ws = this.state.ws;
+
+    if (ws.readyState == WebSocket.OPEN) {
+      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 {
+      this.state.wsQueue.push(message);
+    }
+  }
+
+  handlePlayStop(message) {
+    console.log('Handle play stop <--------------------');
+
+    this.stop(message.cameraId);
+  }
+
+  handlePlayStart(message) {
+    console.log('Handle play start <===================');
+
+    window.adjustVideos('webcamArea', true);
+  }
+
+  handleError(message) {
+    console.log(` Handle error ---------------------> ${message.message}`);
+  }
+
+  render() {
+    return (
+
+      <div className={styles.videoDock}>
+        <div id="webcamArea" />
+        <video id="shareWebcamVideo" className={styles.sharedWebcamVideo} ref="videoInput" />
+      </div>
+    );
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    const { users } = this.props;
+    const nextUsers = nextProps.users;
+
+    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) {
+              this.start(users[i].userId, false, this.refs.videoInput);
+            } else {
+              this.stop(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..0bd4ea2ee4d5f16a57ee285175169b4c161c94aa 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx
@@ -1,15 +1,26 @@
-import React from 'react';
+import React, { Component } from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
 
 import VideoDock from './component';
+import VideoService from './service';
+import Users from '/imports/api/users';
 
-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: Users.find().fetch(),
+}), 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..99a791ecbfa05e767ec0f3155424a387c0c62b1f
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/service.js
@@ -0,0 +1,23 @@
+import { makeCall } from '/imports/ui/services/api';
+
+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);
+};
+
+export default {
+  sendUserShareWebcam, sendUserUnshareWebcam, joinVideo, exitVideo,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
index a37e967d4ce74c1e91f784b5d0fac052d3fd6009..4157b2c58194558e64ead582c2b654f8464acf8f 100644
--- a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss
@@ -7,9 +7,15 @@
   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;
 }
+
+.secretButtons {
+}
+
+.sharedWebcamVideo {
+  display: none;
+}
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..9a2ca1c0181283ead37a71a3f41a49a083d8b02a
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import Button from '/imports/ui/components/button/component';
+import { withRouter } from 'react-router';
+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)}
+            color={'danger'}
+            icon={'video'}
+            size={'lg'}
+            circle
+          />
+        );
+    }
+
+    return (
+      <Button
+        onClick={handleJoinVideo}
+        label={intl.formatMessage(intlMessages.joinVideo)}
+        color={'primary'}
+        icon={'video_off'}
+        size={'lg'}
+        circle
+      />
+    );
+  }
+}
+
+export default withRouter(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..da5950f90a3da413c568f53f93964149487501b2
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import Users from '/imports/api/users';
+import Auth from '/imports/ui/services/auth/index';
+import JoinVideoOptions from './component';
+
+const JoinVideoOptionsContainer = props => (<JoinVideoOptions {...props} />);
+
+export default createContainer((params) => {
+  const userId = Auth.userID;
+  const user = Users.findOne({ userId: userId });
+
+  const isSharingVideo = user.has_stream ? true : false;
+ 
+  return {
+    isSharingVideo,
+    handleJoinVideo: params.handleJoinVideo,
+    handleCloseVideo: params.handleCloseVideo,
+  };
+}, JoinVideoOptionsContainer);
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 e267a8ca7e33db7f77b4fe04b9d33f4b30c71a67..65b346187b2d62a584a15b143556c32f2979d15f 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -165,9 +165,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",
@@ -256,4 +258,6 @@
     "app.guest.waiting": "Waiting for approval to join",
     "app.notification.recordingStart": "This session is now being recorded",
     "app.notification.recordingStop": "This session is not being recorded anymore"
+    "app.video.joinVideo": "Cam off",
+    "app.video.leaveVideo": "Cam on"
 }
diff --git a/bigbluebutton-html5/public/js/adjust-videos.js b/bigbluebutton-html5/public/js/adjust-videos.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d8e44e9420eb6c7941f188bb3ded85642d8d7aa
--- /dev/null
+++ b/bigbluebutton-html5/public/js/adjust-videos.js
@@ -0,0 +1,91 @@
+
+(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);
+  }
+
+  console.log(" ---------------------------------- bro!!!");
+
+  window.adjustVideos = adjustVideos;
+})();
diff --git a/bigbluebutton-html5/public/js/bower.json b/bigbluebutton-html5/public/js/bower.json
new file mode 100644
index 0000000000000000000000000000000000000000..965dfb86df825ec42fedc9c14cfb63cc40714146
--- /dev/null
+++ b/bigbluebutton-html5/public/js/bower.json
@@ -0,0 +1,32 @@
+{
+  "name": "kurento-hello-world",
+  "description": "Kurento Browser JavaScript Tutorial",
+  "authors": [
+    "Kurento <info@kurento.org>"
+  ],
+  "main": "index.html",
+  "moduleType": [
+    "globals"
+  ],
+  "license": "LGPL",
+  "homepage": "http://www.kurento.org/",
+  "private": true,
+  "ignore": [
+    "**/.*",
+    "node_modules",
+    "bower_components",
+    "test",
+    "tests"
+  ],
+  "dependencies": {
+    "adapter.js": "5.0.6",
+    "bootstrap": "3.3.6",
+    "kurento-utils": "https://github.com/lfzawacki/kurento-utils-js.git#safari11",
+    "react": "15.1.0",
+    "reconnectingWebsocket": "1.0.0",
+    "requirejs": "2.2.0",
+    "requirejs-react-jsx": "1.0.2",
+    "requirejs-text": "2.0.15",
+    "font-awesome": "fontawesome#^4.6.3"
+  }
+}
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..54dc22f33aab7a3521e53565bc5e4ec2fa8beb19
--- /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-redis-channel"
+to-screenshare: "to-screenshare-redis-channel"
+from-video: "from-video-redis-channel"
+to-video: "to-video-redis-channel"
+from-audio: "from-audio-redis-channel"
+to-audio: "to-audio-redis-channel"
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 89%
rename from labs/kurento-screenshare/lib/bbb/messages/Constants.js
rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js
index 6cccf19f93cadf15684b9a09c2f2ddde37909dbd..a72ae88639b8c9f868120d830ad7a49d5aac452e 100644
--- a/labs/kurento-screenshare/lib/bbb/messages/Constants.js
+++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js
@@ -17,9 +17,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: "from-screenshare-redis-channel",
+        TO_SCREENSHARE: "to-screenshare-redis-channel",
+        FROM_VIDEO: "from-video-redis-channel",
+        TO_VIDEO: "to-video-redis-channel",
+        FROM_AUDIO: "from-audio-redis-channel",
+        TO_AUDIO: "to-audio-redis-channel",
 
         // 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..1f2a3fe2134440e14509f4a78d908c5563067f63
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js
@@ -0,0 +1,119 @@
+/**
+ * @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("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) {
+      console.log("  [RedisWrapper] Sending message to channel [" + channel + "] : " + message );
+      console.log(message);
+      this.redisPub.publish(channel, message);
+    }
+  }
+
+  /* Private members */
+
+  _onMessage (pattern, channel, _message) {
+    let message = (typeof _message !== 'object')?JSON.parse(_message):_message;
+    console.log(" [RedisWrapper] Message received from channel [" + channel +  "] : " + 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..427083768e12f81a0cdd69ea95790837d5b3c294
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
@@ -0,0 +1,121 @@
+/**
+ * @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:
+          console.log("  [BigBlueButtonGW] Unknown Redis message with ID =>" + header.name);
+          this.emit(C.GATEWAY_MESSAGE, msg);
+      }
+    }
+    else {
+      console.log("  [BigBlueButtonGW] Unknown Redis message =>");
+      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) {
+    console.log(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..dcfbdb3876eb7726d2637b1e770ee5ac448a394e
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js
@@ -0,0 +1,104 @@
+/*
+ * 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) => {
+      console.log("  [ConnectionManager] RECEIVED DATA FROM WEBSOCKET");
+      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) => {
+        console.log("  [ConnectionManager] RECEIVED DATA FROM REDIS");
+        this._emitter.emit('response', data);
+      });
+
+      video.on(C.REDIS_MESSAGE, (data) => {
+        console.log("  [ConnectionManager] RECEIVED DATA FROM REDIS");
+        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..d83b475169a11e7a486356e0e0601e037d75442e
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
@@ -0,0 +1,121 @@
+'use strict';
+
+const ws = require('ws');
+const C = require('../bbb/messages/Constants');
+
+ws.prototype.setErrorCallback = function(callback) {
+
+  this._errorCallback = callback;
+};
+
+ws.prototype.sendMessage = function(json) {
+
+  let websocket = this;
+
+  if (this._closeCode === 1000) {
+    console.log("Websocket closed, not sending");
+    this._errorCallback("Error: not opened");
+  }
+
+  return this.send(JSON.stringify(json), function(error) {
+    if(error) {
+      console.log('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.on('message', (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) {
+          if (message.callerName && !ws.connectionId) {
+            ws.connectionId = data.callerName;
+          }
+
+          this.emitter.emit(C.WEBSOCKET_MESSAGE, message);
+        }
+      });
+
+      //ws.on('message', this._onMessage.bind(this));
+      ws.setErrorCallback(this._onError.bind(this));
+
+      ws.on('close', this._onClose);
+      ws.on('error', this._onError);
+
+      // TODO: should we delete this listener after websocket dies?
+      this.emitter.on('response', (data) => {
+        console.log('  [WebsocketConnectionManager] Receiving event ');
+        console.log(data);
+        if (ws.connectionId == data.callerName) {
+          ws.sendMessage(data);
+        }
+      });
+    });
+  }
+
+  setEventEmitter (emitter) {
+    console.log(emitter);
+    this.emitter = emitter;
+  }
+
+  _onServerResponse (data) {
+
+    console.log('  [WebsocketConnectionManager] Receiving event ');
+    console.log(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');
+
+  }
+
+  _onClose (err) {
+    console.log('  [WebsocketConnectionManager] Closed Connection');
+  }
+
+  _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..19a5ed1e41e2fb0470bf7d4bf44be0af62cb5348
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js
@@ -0,0 +1,146 @@
+'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) {
+    let self = this;
+    try {
+      const answer = await this._mediaController.join(room, type, params);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      console.log(err);
+      Promise.reject(err);
+    }
+  }
+
+  // Not yet implemented in MediaController, should be simple nonetheless
+  async leave (room, userId) {
+    try {
+      const answer = await this._mediaController.leave(room, 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 {
+      const answer = await this._mediaController.unpublish(user, mediaId);
+      return Promise.resolve(answer);
+    }
+    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, sdp, params) {
+    try {
+      await this._mediaController.unsubscribe(user, mediaId);
+      return Promise.resolve(answer);
+    }
+    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 {
+      console.log("  [api] Adding ice candidate for => " + mediaId);
+      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..de9b888cfef941ce1412f9c9c073f2bd1d25344c
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js
@@ -0,0 +1,309 @@
+'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[roomdId];
+  }
+
+  async join (roomId, type, params) {
+    console.log("[mcs] Join room => " + roomId + ' as ' + type);
+    try {
+      let session;
+      const room = await this.createRoomMCS(roomId);
+      const user = await this.createUserMCS(roomId, type, params);
+      let userId = user.id;
+      room.setUser(user);
+      if (params.sdp) {
+        session = user.addSdp(params.sdp);
+      }
+      if (params.uri) {
+        session = user.addUri(params.sdp);
+      }
+
+      console.log("[mcs] Resolving user " + userId);
+      return Promise.resolve(userId);
+    }
+    catch (err) {
+      console.log("[mcs] JOIN ERROR " + 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);
+          
+          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, type, sourceId, 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);
+
+          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 user = this.getUserMCS(userId);
+      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);
+      const answer = await user.unsubscribe(mediaId);
+      return Promise.resolve();
+      this._mediaSessions[mediaId] = null;
+    }
+    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 {
+      console.log("  [mcs] Adding ICE candidate for => " + mediaId);
+      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..7ba91f197ee19ae540f2eba3ef64ccd6be02b5b9
--- /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("[mcs] Invalid connect type");
+        }
+      });
+    }
+    else {
+      return Promise.reject("Failed to connect " + type + ": " + sourceId + " to " + sinkId);
+    }
+  }
+
+  stop (elementId) {
+    let mediaElement = this._mediaElements[elementId];
+    // TODO remove event listeners
+    if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') {
+      mediaElement.release();
+    }
+  }
+
+  
+  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) {
+          console.log("  [media] Relaying ICE for MediaState" + elementId);
+          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..8bcc8a7d8e61b170c0a74315504f3b9ef64490c8
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js
@@ -0,0 +1,43 @@
+/**
+ * @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];
+  }
+
+  getMcuUser (id) {
+    return this._mcuUsers[id];
+  }
+
+  setUser (user) {
+  if (typeof this._users[user.id] == 'undefined' ||
+        !this._users[user.id]) {
+      this._users[user.id] = {};
+    }
+    this._users[user.id] = user;
+  }
+
+  destroyUser(user) {
+    let _user = this._users[user.id];
+    _user.destroy();
+    delete this._users[user.id];
+  }
+
+  destroyMcuUser (user) {
+    let _user  = this._mcuUsers[user.id];
+    _user.destroy();
+    delete this._mcuUsers[user.id];
+  }
+
+}
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..22ba6c247ecb14800124c3b3d62cbd208440d853
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
@@ -0,0 +1,117 @@
+/**
+ * @classdesc
+ * Model class for external devices
+ */
+
+'use strict'
+
+const C = require('../constants/Constants');
+const SdpWrapper = require('../utils/SdpWrapper');
+const uuidv4 = require('uuid/v4');
+const EventEmitter = require('events').EventEmitter;
+const MediaServer = require('../media/media-server');
+const config = require('config');
+const kurentoUrl = config.get('kurentoUrl');
+
+module.exports = class SdpSession {
+  constructor(emitter, sdp = null, room, type = 'WebRtcEndpoint') {
+    this.id = uuidv4();
+    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;
+  }
+
+  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(() => {
+          console.log("  [SdpSession] Relaying EVENT MediaState" + this.id);
+          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.getMainDescription()); 
+
+      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.id);
+      this._status = C.STATUS.STOPPED;
+      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 {
+      console.log("  [SdpSession] Adding ICE candidate for => " + this._mediaElement);
+      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..94301309cacedc06c9c4ef260d7ddc8b454401a3
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js
@@ -0,0 +1,164 @@
+/**
+ * @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);
+
+    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();
+      console.log("WELL");
+      console.log(answer);
+      return Promise.resolve(answer);
+    }
+    catch (err) {
+      this.handleError(err);
+      return Promise.reject(new Error(err));
+    }
+  }
+
+  async subscribe (sdp, mediaId) {
+    let session = await this.addSdp(sdp);
+    try {
+      await this.startSession(session.id);
+      await this.connect(session.id, mediaId);
+      Promise.resolve();
+    } 
+    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 (sdp, mediaId) {
+    try {
+      await this.stopSession(mediaId);
+      Promise.resolve();
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async unpublish (sdp, mediaId) {
+    try {
+      await this.stopSession(mediaId);
+      Promise.resolve();
+    } 
+    catch (err) {
+      this.handleError(err);
+      Promise.reject(new Error(err));
+    }
+  }
+
+  async stopSession (sdpId) {
+    let session = this._mediaSessions[sdpId];
+
+    try {
+      await session.stop();
+      this._mediaSessions[sdpId] = 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"));
+    }
+  }
+
+  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..74b7795bcc8b8c75918b965355f7ba3716985bf9
--- /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 uuidv4 = require('uuid/v4');
+const EventEmitter = require('events').EventEmitter;
+const MediaServer = require('../media/media-server');
+
+module.exports = class UriSession extends EventEmitter {
+  constructor(uri = null) {
+    super();
+    this.id = uuidv4();
+    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..b3d073a33788ec9c3978d23f81ecc737a5ba751d
--- /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 uuidv4 = require('uuid/v4');
+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 = uuidv4();
+    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-controller.js b/labs/bbb-webrtc-sfu/lib/media-controller.js
similarity index 100%
rename from labs/kurento-screenshare/lib/media-controller.js
rename to labs/bbb-webrtc-sfu/lib/media-controller.js
diff --git a/labs/kurento-screenshare/lib/media-handler.js b/labs/bbb-webrtc-sfu/lib/media-handler.js
similarity index 100%
rename from labs/kurento-screenshare/lib/media-handler.js
rename to labs/bbb-webrtc-sfu/lib/media-handler.js
diff --git a/labs/kurento-screenshare/lib/ConnectionManager.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js
similarity index 70%
rename from labs/kurento-screenshare/lib/ConnectionManager.js
rename to labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js
index 2dbef1e75d32f7759337ef0f56a3b11ced7d4ed6..1748ac0fbfbb4815c9de207616c8a5767e208fdb 100644
--- a/labs/kurento-screenshare/lib/ConnectionManager.js
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js
@@ -5,26 +5,28 @@
  *
  */
 
-'use strict'
+"use strict";
 
+const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw');
 const cookieParser = require('cookie-parser')
 const express = require('express');
 const session = require('express-session')
-const wsModule = require('./websocket');
+const wsModule = require('../websocket');
 const http = require('http');
 const fs = require('fs');
-const BigBlueButtonGW = require('./bbb/pubsub/bbb-gw');
+const MediaController = require('../media-controller');
 var Screenshare = require('./screenshare');
-var C = require('./bbb/messages/Constants');
-
+var C = require('../bbb/messages/Constants');
 // Global variables
 
-module.exports = class ConnectionManager {
+module.exports = class ScreenshareManager {
 
   constructor (settings, logger) {
     this._logger = logger;
     this._clientId = 0;
     this._app = express();
+
+    this._sessions = {};
     this._screenshareSessions = {};
 
     this._setupExpressSession();
@@ -79,6 +81,7 @@ module.exports = class ConnectionManager {
     let connectionId;
     let request = webSocket.upgradeReq;
     let sessionId;
+    let callerName;
     let response = {
       writeHead : {}
     };
@@ -95,7 +98,16 @@ module.exports = class ConnectionManager {
 
     webSocket.on('close', function() {
       console.log('Connection ' + connectionId + ' closed');
-      self._stopSession(sessionId);
+      console.log(webSocket.presenter);
+
+      if (webSocket.presenter && self._screenshareSessions[sessionId]) { // if presenter  // FIXME  (this conditional was added to prevent screenshare stop when an iOS user quits)
+      console.log("  [CM] Stopping presenter " + sessionId);
+        self._stopSession(sessionId);
+      }
+      if (webSocket.viewer && typeof webSocket.session !== 'undefined') {
+        console.log("  [CM] Stopping viewer " + webSocket.viewerId);
+        webSocket.session._stopViewer(webSocket.viewerId);
+      }
     });
 
     webSocket.on('message', function(_message) {
@@ -103,9 +115,9 @@ module.exports = class ConnectionManager {
       let session;
       // The sessionId is voiceBridge for screensharing sessions
       sessionId = message.voiceBridge;
-
       if(self._screenshareSessions[sessionId]) {
         session = self._screenshareSessions[sessionId];
+        webSocket.session = session;
       }
 
       switch (message.id) {
@@ -114,7 +126,14 @@ module.exports = class ConnectionManager {
 
           // Checking if there's already a Screenshare session started
           // because we shouldn't overwrite it
+          webSocket.presenter = true;
 
+          if (!self._screenshareSessions[message.voiceBridge]) {
+            self._screenshareSessions[message.voiceBridge] = {}
+            self._screenshareSessions[message.voiceBridge] = session;
+          }
+
+          //session.on('message', self._assembleSessionMessage.bind(self));
           if(session) {
             break;
           }
@@ -147,11 +166,23 @@ module.exports = class ConnectionManager {
           break;
 
         case 'viewer':
-          console.log('Viewer message => [' + message.id + '] connection [' + connectionId + '][' + message.presenterId + '][' + message.sessionId + '][' + message.callerName + ']');
-
+          console.log("[viewer] Session output \n " + session);
+
+          webSocket.viewer = true;
+          webSocket.viewerId = message.callerName;
+
+          if (message.sdpOffer && message.voiceBridge) {
+            if (session) {
+              session._startViewer(webSocket, message.voiceBridge, message.sdpOffer, message.callerName, self._screenshareSessions[message.voiceBridge]._presenterEndpoint);
+            } else {
+              webSocket.sendMessage("voiceBridge not recognized");
+              webSocket.sendMessage(Object.keys(self._screenshareSessions));
+              webSocket.sendMessage(message.voiceBridge);
+            }
+          }
           break;
-        case 'stop':
 
+        case 'stop':
           console.log('[' + message.id + '] connection ' + connectionId);
 
           if (session) {
@@ -163,6 +194,7 @@ module.exports = class ConnectionManager {
 
         case 'onIceCandidate':
           if (session) {
+            console.log(" [CM] What the fluff is happening");
             session._onIceCandidate(message.candidate);
           } else {
             console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?");
@@ -176,6 +208,16 @@ module.exports = class ConnectionManager {
           }));
           break;
 
+
+        case 'viewerIceCandidate':
+          console.log("[viewerIceCandidate] Session output => " + session);
+          if (session) {
+            session._onViewerIceCandidate(message.candidate, message.callerName);
+          } else {
+            console.log("[iceCandidate] Why is there no session on ICE CANDIDATE?");
+          }
+          break;
+
         default:
           webSocket.sendMessage({ id : 'error', message : 'Invalid message ' + message });
           break;
@@ -203,4 +245,4 @@ module.exports = class ConnectionManager {
 
     setTimeout(process.exit, 1000);
   }
-}
+};
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..e8755e9024cb276cb52ecc0eacdae81e711f094c
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js
@@ -0,0 +1,12 @@
+const ScreenshareManager = require('./ScreenshareManager');
+
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+});
+
+process.on('disconnect',function() {
+  console.log("Parent exited!");
+  process.kill();
+});
+
+c = new ScreenshareManager();
diff --git a/labs/kurento-screenshare/lib/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
similarity index 66%
rename from labs/kurento-screenshare/lib/screenshare.js
rename to labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
index 411fbd54a9ac3cf994f7e0f703ed7c3f32f8718e..75cfccf742da95673e571168d7fba305d29669ba 100644
--- a/labs/kurento-screenshare/lib/screenshare.js
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
@@ -8,13 +8,13 @@
 'use strict'
 
 // Imports
-const C = require('./bbb/messages/Constants');
-const MediaHandler = require('./media-handler');
-const Messaging = require('./bbb/messages/Messaging');
+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 h264_sdp = require('../h264-sdp');
 const now = moment();
-const MediaController = require('./media-controller');
+const MediaController = require('../media-controller');
 
 // Global stuff
 var sharedScreens = {};
@@ -44,6 +44,8 @@ module.exports = class Screenshare {
     this._vw = vw;
     this._vh = vh;
     this._candidatesQueue = [];
+    this._viewersEndpoint = [];
+    this._viewersCandidatesQueue = [];
   }
 
   // TODO isolate ICE
@@ -51,12 +53,93 @@ module.exports = class Screenshare {
     let candidate = kurento.getComplexType('IceCandidate')(_candidate);
 
     if (this._presenterEndpoint) {
+      console.log("  [screenshare] Adding ICE candidate to presenter");
       this._presenterEndpoint.addIceCandidate(candidate);
     }
     else {
       this._candidatesQueue.push(candidate);
     }
   };
+  
+  _onViewerIceCandidate(_candidate, callerName) {
+    console.log("onviewericecandidate callerName = " + callerName);
+    let candidate = kurento.getComplexType('IceCandidate')(_candidate);
+    
+    if (this._viewersEndpoint[callerName]) {
+      this._viewersEndpoint[callerName].addIceCandidate(candidate);
+    }
+    else {
+      if (!this._viewersCandidatesQueue[callerName]) {
+        this._viewersCandidatesQueue[callerName] = [];
+      }
+      this._viewersCandidatesQueue[callerName].push(candidate);
+    }
+  }
+
+  _startViewer(ws, voiceBridge, sdp, callerName, presenterEndpoint, callback) {
+    let self = this;
+    let _callback = function(){};
+    console.log("startviewer callerName = " + callerName);
+    self._viewersCandidatesQueue[callerName] = [];
+    
+    console.log("VIEWER VOICEBRIDGE:    "+self._voiceBridge);
+ 
+    MediaController.createMediaElement(voiceBridge, C.WebRTC, function(error, webRtcEndpoint) {
+      if (error) {
+        console.log("Media elements error" + error);
+        return _callback(error);
+      }
+
+      self._viewersEndpoint[callerName] = webRtcEndpoint;
+
+      // QUEUES UP ICE CANDIDATES IF NEGOTIATION IS NOT YET READY
+      while(self._viewersCandidatesQueue[callerName].length) {
+        let candidate = self._viewersCandidatesQueue[callerName].shift();
+        MediaController.addIceCandidate(self._viewersEndpoint[callerName].id, candidate);
+      }
+      // CONNECTS TWO MEDIA ELEMENTS
+      MediaController.connectMediaElements(presenterEndpoint.id, self._viewersEndpoint[callerName].id, C.VIDEO, function(error) {
+        if (error) {
+          console.log("Media elements CONNECT error " + error);
+          //pipeline.release();
+          return _callback(error);
+        }
+      });
+
+      // ICE NEGOTIATION WITH THE ENDPOINT
+      self._viewersEndpoint[callerName].on('OnIceCandidate', function(event) {
+        let candidate = kurento.getComplexType('IceCandidate')(event.candidate); ws.sendMessage({ id : 'iceCandidate', candidate : candidate });
+      });
+
+      sdp = h264_sdp.transform(sdp);
+      // PROCESS A SDP OFFER
+      MediaController.processOffer(webRtcEndpoint.id, sdp, function(error, webRtcSdpAnswer) {
+        if (error) {
+          console.log("  [webrtc] processOffer error => " + error + " for SDP " + sdp);
+          //pipeline.release();
+          return _callback(error);
+        }
+        ws.sendMessage({id: "viewerResponse", sdpAnswer: webRtcSdpAnswer, response: "accepted"});
+        console.log(" Sent sdp message to client with callerName:" + callerName);
+
+        MediaController.gatherCandidates(webRtcEndpoint.id, function(error) {
+          if (error) {
+            return _callback(error);
+          }
+
+          self._viewersEndpoint[callerName].on('MediaFlowInStateChange', function(event) {
+            if (event.state === 'NOT_FLOWING') {
+              console.log(" NOT FLOWING ");
+            }
+            else if (event.state === 'FLOWING') {
+              console.log(" FLOWING ");
+            }
+          });
+        });
+      });
+    });
+  }
+
 
   _startPresenter(id, ws, sdpOffer, callback) {
     let self = this;
@@ -65,6 +148,7 @@ module.exports = class Screenshare {
     // Force H264 on Firefox and Chrome
     sdpOffer = h264_sdp.transform(sdpOffer);
     console.log("Starting presenter for " + sdpOffer);
+    console.log("PRESENTER VOICEBRIDGE:   " + self._voiceBridge);
     MediaController.createMediaElement(self._voiceBridge, C.WebRTC, function(error, webRtcEndpoint) {
       if (error) {
         console.log("Media elements error" + error);
@@ -160,7 +244,6 @@ module.exports = class Screenshare {
     } else {
       console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE");
     }
-
     if (this._ffmpegRtpEndpoint) {
       MediaController.releaseMediaElement(this._ffmpegRtpEndpoint.id);
       this._ffmpegRtpEndpoint = null;
@@ -196,6 +279,7 @@ module.exports = class Screenshare {
   }
 
   _onRtpMediaFlowing(meetingId, rtpParams) {
+    console.log("  [screenshare] Media FLOWING for meeting => " + meetingId);
     let self = this;
     let strm = Messaging.generateStartTranscoderRequestMessage(meetingId, meetingId, rtpParams);
 
@@ -218,7 +302,8 @@ module.exports = class Screenshare {
   };
 
   _stopRtmpBroadcast (meetingId) {
-    var self = this;
+    console.log("  [screenshare] _stopRtmpBroadcast for meeting => " + meetingId);
+    let self = this;
     if(self._meetingId === meetingId) {
       // TODO correctly assemble this timestamp
       let timestamp = now.format('hhmmss');
@@ -229,6 +314,7 @@ module.exports = class Screenshare {
   }
 
   _startRtmpBroadcast (meetingId, output) {
+    console.log("  [screenshare] _startRtmpBroadcast for meeting => " + meetingId);
     var self = this;
     if(self._meetingId === meetingId) {
       // TODO correctly assemble this timestamp
@@ -245,5 +331,17 @@ module.exports = class Screenshare {
     console.log("  [screenshare] TODO RTP NOT_FLOWING");
   };
 
+  _stopViewer(id) {
+    let viewer = this._viewersEndpoint[id];
+    console.log(' [stop] Releasing endpoints for ' + id);
 
+    if (viewer) {
+      MediaController.releaseMediaElement(viewer.id);
+      this._viewersEndpoint[viewer.id] = null;
+    } else {
+      console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE");
+    }
+
+    delete this._viewersCandidatesQueue[id];
+  };
 };
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..5bd40e44e9ca44704d12588ab9ba01a97a4bcc44
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js
@@ -0,0 +1,159 @@
+/*
+ * Lucas Fialho Zawacki
+ * (C) Copyright 2017 Bigbluebutton
+ *
+ */
+
+var cookieParser = require('cookie-parser')
+var express = require('express');
+var session = require('express-session')
+var ws = require('./websocket');
+var http = require('http');
+var fs = require('fs');
+
+var Video = require('./video');
+
+// Global variables
+var app = express();
+var sessions = {};
+
+/*
+ * Management of sessions
+ */
+app.use(cookieParser());
+
+var sessionHandler = session({
+  secret : 'Shawarma', rolling : true, resave : true, saveUninitialized : true
+});
+
+app.use(sessionHandler);
+
+/*
+ * Server startup
+ */
+var server = http.createServer(app).listen(3002, function() {
+  console.log(' [*] Running bbb-html5 kurento video service.');
+});
+
+var wss = new ws.Server({
+  server : server,
+  path : '/html5video'
+});
+
+var clientId = 0;
+
+wss.on('connection', function(ws) {
+  var sessionId;
+  var request = ws.upgradeReq;
+  var response = {
+    writeHead : {}
+  };
+
+  sessionHandler(request, response, function(err) {
+    sessionId = request.session.id + "_" + clientId++;
+
+    if (!sessions[sessionId]) {
+      sessions[sessionId] = {};
+    }
+
+    console.log('Connection received with sessionId ' + sessionId);
+  });
+
+  ws.on('error', function(error) {
+    console.log('Connection ' + sessionId + ' error');
+    // stop(sessionId);
+  });
+
+  ws.on('close', function() {
+    console.log('Connection ' + sessionId + ' closed');
+    stopSession(sessionId);
+  });
+
+  ws.on('message', function(_message) {
+    var message = JSON.parse(_message);
+
+    var video;
+    if (message.cameraId && sessions[sessionId][message.cameraId]) {
+      video = sessions[sessionId][message.cameraId];
+    }
+
+    switch (message.id) {
+
+      case 'start':
+
+        console.log('[' + message.id + '] connection ' + sessionId);
+
+        var video = new Video(ws, message.cameraId, message.cameraShared);
+        sessions[sessionId][message.cameraId] = video;
+
+        video.start(message.sdpOffer, function(error, sdpAnswer) {
+          if (error) {
+            return ws.sendMessage({id : 'error', message : error });
+          }
+
+          ws.sendMessage({id : 'startResponse', cameraId: message.cameraId, sdpAnswer : sdpAnswer});
+        });
+
+        break;
+
+        case 'stop':
+
+          console.log('[' + message.id + '] connection ' + sessionId);
+
+          if (video) {
+            video.stop(sessionId);
+          } else {
+            console.log(" [stop] Why is there no video on STOP?");
+          }
+          break;
+
+        case 'onIceCandidate':
+
+          if (video) {
+            video.onIceCandidate(message.candidate);
+          } else {
+            console.log(" [iceCandidate] Why is there no video on ICE CANDIDATE?");
+          }
+          break;
+
+        default:
+          ws.sendMessage({ id : 'error', message : 'Invalid message ' + message });
+          break;
+      }
+
+    });
+});
+
+var stopSession = function(sessionId) {
+
+  console.log(' [>] Stopping session ' + sessionId);
+
+  var videoIds = Object.keys(sessions[sessionId]);
+
+  for (var i = 0; i < videoIds.length; i++) {
+
+    var video = sessions[sessionId][videoIds[i]];
+    video.stop();
+
+    delete sessions[sessionId][videoIds[i]];
+  }
+
+  delete sessions[sessionId];
+}
+
+var stopAll = function() {
+
+  console.log('\n [x] Stopping everything! ');
+
+  var sessionIds = Object.keys(sessions);
+
+  for (var i = 0; i < sessionIds.length; i++) {
+
+    stopSession(sessionIds[i]);
+  }
+
+  setTimeout(process.exit, 1000);
+}
+
+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..af1f3b35daa3ad3748175c80254626e2bfe6f113
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js
@@ -0,0 +1,10 @@
+const VideoManager = require('./VideoManager');
+
+process.on('uncaughtException', function (error) {
+  console.log(error.stack);
+});
+
+process.on('disconnect',function() {
+  console.log("Parent exited!");
+  process.kill();
+});
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..f1544911343e948aad620811b224e40bcf57cf48
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/lib/video/video.js
@@ -0,0 +1,152 @@
+'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');
+
+if (config.get('acceptSelfSignedCertificate')) {
+  process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
+}
+
+module.exports = class Video {
+  constructor(_ws, _id, _shared) {
+    this.mcs = new MCSApi();
+    this.ws = _ws;
+    this.id = _id;
+    this.meetingId = _id;
+    this.shared = _shared;
+    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.ws.sendMessage({ id : 'iceCandidate', cameraId: this.id, candidate : candidate });
+        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.ws.sendMessage({ id : 'playStop', cameraId : this.id });
+        } 
+        else if (msEvent.state === 'FLOWING') {
+          this.ws.sendMessage({ id : 'playStart', cameraId : this.id });
+        }
+
+        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, 'WebRtcEndpoint', sharedWebcams[this.id], {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 returned => " + this.mediaId);
+
+        return callback(null, sdpAnswer);
+      }
+    }
+    catch (err) {
+      console.log("  [video] MCS returned error => " + err);
+      return callback(err);
+    }
+  };
+
+  stop () {
+
+    //console.log(' [stop] Releasing webrtc endpoint for ' + id);
+
+    //if (webRtcEndpoint) {
+    //  webRtcEndpoint.release();
+    //  webRtcEndpoint = null;
+    //} else {
+    //  console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE");
+    //}
+
+    //if (shared) {
+    //  console.log(' [stop] Webcam is shared, releasing ' + id);
+
+    //  if (mediaPipelines[id]) {
+    //    mediaPipelines[id].release();
+    //  } else {
+    //    console.log(" [mediaPipeline] PLEASE DONT TRY STOPPING THINGS TWICE");
+    //  }
+
+    //  delete mediaPipelines[id];
+    //  delete sharedWebcams[id];
+    //}
+
+    //delete this.candidatesQueue;
+  };
+};
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..a6f53daa3c4629ec0783a636917c82c37edaad1c
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/package-lock.json
@@ -0,0 +1,689 @@
+{
+  "name": "bbb-screenshare-video-kurento-bridge",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "accepts": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
+      "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=",
+      "requires": {
+        "mime-types": "2.1.17",
+        "negotiator": "0.5.3"
+      }
+    },
+    "argparse": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
+      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+      "dev": true,
+      "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"
+      }
+    },
+    "backoff": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.3.0.tgz",
+      "integrity": "sha1-7nx+OAk/kuRyhZ22NedlJFT8Ieo="
+    },
+    "base64-url": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz",
+      "integrity": "sha1-GZ/WYXAqDnt9yubgaYuwicUvbXg="
+    },
+    "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=",
+      "dev": true,
+      "requires": {
+        "json5": "0.4.0",
+        "os-homedir": "1.0.2"
+      }
+    },
+    "content-disposition": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz",
+      "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4="
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "cookie-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz",
+      "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=",
+      "requires": {
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6"
+      }
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "crc": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz",
+      "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII="
+    },
+    "debug": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
+      "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
+      "requires": {
+        "ms": "0.7.1"
+      }
+    },
+    "depd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
+      "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo="
+    },
+    "destroy": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz",
+      "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk="
+    },
+    "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="
+    },
+    "ee-first": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz",
+      "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q="
+    },
+    "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=="
+    },
+    "escape-html": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
+      "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A="
+    },
+    "esprima": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+      "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
+      "dev": true
+    },
+    "etag": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.6.0.tgz",
+      "integrity": "sha1-i8ssavElTEgd/IuZfJBu9ORCwgc=",
+      "requires": {
+        "crc": "3.2.1"
+      }
+    },
+    "express": {
+      "version": "4.12.4",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.12.4.tgz",
+      "integrity": "sha1-j+wlECVbxrLlgQfEgjnA+jB8GqI=",
+      "requires": {
+        "accepts": "1.2.13",
+        "content-disposition": "0.5.0",
+        "content-type": "1.0.4",
+        "cookie": "0.1.2",
+        "cookie-signature": "1.0.6",
+        "debug": "2.2.0",
+        "depd": "1.0.1",
+        "escape-html": "1.0.1",
+        "etag": "1.6.0",
+        "finalhandler": "0.3.6",
+        "fresh": "0.2.4",
+        "merge-descriptors": "1.0.0",
+        "methods": "1.1.2",
+        "on-finished": "2.2.1",
+        "parseurl": "1.3.2",
+        "path-to-regexp": "0.1.3",
+        "proxy-addr": "1.0.10",
+        "qs": "2.4.2",
+        "range-parser": "1.0.3",
+        "send": "0.12.3",
+        "serve-static": "1.9.3",
+        "type-is": "1.6.15",
+        "utils-merge": "1.0.0",
+        "vary": "1.0.1"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
+          "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE="
+        }
+      }
+    },
+    "express-session": {
+      "version": "1.10.4",
+      "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.10.4.tgz",
+      "integrity": "sha1-BOHZLgBZOJPh92Vp6zrWMRPa+Uw=",
+      "requires": {
+        "cookie": "0.1.2",
+        "cookie-signature": "1.0.6",
+        "crc": "3.2.1",
+        "debug": "2.1.3",
+        "depd": "1.0.1",
+        "on-headers": "1.0.1",
+        "parseurl": "1.3.2",
+        "uid-safe": "1.1.0",
+        "utils-merge": "1.0.0"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
+          "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE="
+        },
+        "debug": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
+          "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=",
+          "requires": {
+            "ms": "0.7.0"
+          }
+        },
+        "ms": {
+          "version": "0.7.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
+          "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M="
+        }
+      }
+    },
+    "extend": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+      "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
+    },
+    "finalhandler": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.6.tgz",
+      "integrity": "sha1-2vnEFhsbBuABRmsUEd/baXO+E4s=",
+      "requires": {
+        "debug": "2.2.0",
+        "escape-html": "1.0.1",
+        "on-finished": "2.2.1"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+    },
+    "fresh": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz",
+      "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw="
+    },
+    "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="
+    },
+    "ipaddr.js": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz",
+      "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c="
+    },
+    "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.1",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-13.0.1.tgz",
+      "integrity": "sha512-ChTMfmbIg5yrN9pUdeaLL8vzylMQhUteXiXa1MWINsMUs3jTQ8I87lUZwR5GdfCLJlpK04U7UgrxgmU8Zp7PhQ==",
+      "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==",
+      "dev": true,
+      "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=",
+      "dev": true
+    },
+    "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"
+      }
+    },
+    "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"
+      },
+      "dependencies": {
+        "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="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "merge-descriptors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz",
+      "integrity": "sha1-IWnPdTjhsMyH+4jhUC2EdLv3mGQ="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "mime": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz",
+      "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM="
+    },
+    "mime-db": {
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
+      "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
+    },
+    "mime-types": {
+      "version": "2.1.17",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
+      "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
+      "requires": {
+        "mime-db": "1.30.0"
+      }
+    },
+    "minimist": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+    },
+    "moment": {
+      "version": "2.19.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz",
+      "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc="
+    },
+    "ms": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
+      "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
+    },
+    "nan": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
+      "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY="
+    },
+    "native-or-bluebird": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/native-or-bluebird/-/native-or-bluebird-1.1.2.tgz",
+      "integrity": "sha1-OSHhECMtHreQ89rGG7NwUxx9NW4="
+    },
+    "negotiator": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz",
+      "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g="
+    },
+    "on-finished": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz",
+      "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=",
+      "requires": {
+        "ee-first": "1.1.0"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
+    },
+    "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=",
+      "dev": true
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+    },
+    "path-to-regexp": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz",
+      "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss="
+    },
+    "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="
+    },
+    "proxy-addr": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
+      "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=",
+      "requires": {
+        "forwarded": "0.1.2",
+        "ipaddr.js": "1.0.5"
+      }
+    },
+    "punycode": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
+      "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
+    },
+    "qs": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz",
+      "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o="
+    },
+    "range-parser": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz",
+      "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU="
+    },
+    "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"
+      }
+    },
+    "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="
+    },
+    "sdp-transform": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
+      "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY="
+    },
+    "send": {
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.12.3.tgz",
+      "integrity": "sha1-zRLcWP3iHk+RkCs5sv2gWnptm9w=",
+      "requires": {
+        "debug": "2.2.0",
+        "depd": "1.0.1",
+        "destroy": "1.0.3",
+        "escape-html": "1.0.1",
+        "etag": "1.6.0",
+        "fresh": "0.2.4",
+        "mime": "1.3.4",
+        "ms": "0.7.1",
+        "on-finished": "2.2.1",
+        "range-parser": "1.0.3"
+      }
+    },
+    "serve-static": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.9.3.tgz",
+      "integrity": "sha1-X42gcyOtOF/z3FQfGnkXsuQ261c=",
+      "requires": {
+        "escape-html": "1.0.1",
+        "parseurl": "1.3.2",
+        "send": "0.12.3",
+        "utils-merge": "1.0.0"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "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"
+      }
+    },
+    "type-is": {
+      "version": "1.6.15",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
+      "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "2.1.17"
+      }
+    },
+    "uid-safe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-1.1.0.tgz",
+      "integrity": "sha1-WNbF2r+N+9jVKDSDmAbAP9YUMjI=",
+      "requires": {
+        "base64-url": "1.2.1",
+        "native-or-bluebird": "1.1.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="
+        }
+      }
+    },
+    "utils-merge": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
+      "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg="
+    },
+    "uuid": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
+      "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
+    },
+    "vary": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz",
+      "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA="
+    },
+    "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": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz",
+      "integrity": "sha1-fQsqLljN3YGQOcKcneZQReGzEOk=",
+      "requires": {
+        "options": "0.0.6",
+        "ultron": "1.0.2"
+      }
+    }
+  }
+}
diff --git a/labs/kurento-screenshare/package.json b/labs/bbb-webrtc-sfu/package.json
similarity index 58%
rename from labs/kurento-screenshare/package.json
rename to labs/bbb-webrtc-sfu/package.json
index 187a3ab8a3f2e70c689e740178834ba1fadb5146..63bf2947e1efd675d6c585ee335dca30e8b95c92 100644
--- a/labs/kurento-screenshare/package.json
+++ b/labs/bbb-webrtc-sfu/package.json
@@ -1,20 +1,21 @@
 {
-  "name": "bbb-screenshare-video-kurento-bridge",
-  "version": "1.0.0",
+  "name": "bbb-webrtc-sfu",
+  "version": "0.0.1",
   "private": true,
   "scripts": {
-    "start": "nodejs server.js",
-    "postinstall": "npm start"
+    "start": "node server.js"
   },
   "dependencies": {
     "cookie-parser": "^1.3.5",
     "express": "~4.12.4",
     "express-session": "~1.10.3",
-    "ws": "~1.0.1",
-    "kurento-client": "6.6.0",
+    "kurento-client": "https://github.com/Kurento/kurento-client-js#master",
+    "moment": "*",
     "redis": "^2.6.2",
     "sdp-transform": "*",
-    "moment": "*"
+    "uuid": "^3.1.0",
+    "ws": "~1.0.1",
+    "joi": "*"
   },
   "devDependencies": {
     "config": "^1.26.1",
diff --git a/labs/bbb-webrtc-sfu/server.js b/labs/bbb-webrtc-sfu/server.js
new file mode 100755
index 0000000000000000000000000000000000000000..94b4ec868b85e7a6608680639cd620d4ce3f8dd9
--- /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(), '/kurento-screenshare');
+
+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/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/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));