diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala
index cae06abb02763bff02e2584313f7484d8f9c4e6b..dcf6934b677f3c22ac96fbe208eaa5f39a023a95 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/ScreenshareModel.scala
@@ -10,6 +10,7 @@ object ScreenshareModel {
     status.voiceConf = ""
     status.screenshareConf = ""
     status.timestamp = ""
+    status.hasAudio = false
   }
 
   def getScreenshareStarted(status: ScreenshareModel): Boolean = {
@@ -79,6 +80,14 @@ object ScreenshareModel {
   def getTimestamp(status: ScreenshareModel): String = {
     status.timestamp
   }
+
+  def setHasAudio(status: ScreenshareModel, hasAudio: Boolean): Unit = {
+    status.hasAudio = hasAudio
+  }
+
+  def getHasAudio(status: ScreenshareModel): Boolean = {
+    status.hasAudio
+  }
 }
 
 class ScreenshareModel {
@@ -90,4 +99,5 @@ class ScreenshareModel {
   private var voiceConf: String = ""
   private var screenshareConf: String = ""
   private var timestamp: String = ""
+  private var hasAudio = false
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala
index fbea8767508712f11c5940b2646540f0d8fd8fcc..1edea1b84c0f7e7d6129ac628328526ce26b3a13 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/GetScreenshareStatusReqMsgHdlr.scala
@@ -25,9 +25,10 @@ trait GetScreenshareStatusReqMsgHdlr {
       val vidWidth = ScreenshareModel.getScreenshareVideoWidth(liveMeeting.screenshareModel)
       val vidHeight = ScreenshareModel.getScreenshareVideoHeight(liveMeeting.screenshareModel)
       val timestamp = ScreenshareModel.getTimestamp(liveMeeting.screenshareModel)
+      val hasAudio = ScreenshareModel.getHasAudio(liveMeeting.screenshareModel)
 
       val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
-        stream, vidWidth, vidHeight, timestamp)
+        stream, vidWidth, vidHeight, timestamp, hasAudio)
       val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
       BbbCommonEnvCoreMsg(envelope, event)
     }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala
index e72788eb6b68e87c3c9cc9ab2158612579df5a48..60ee455a0f115fcd71e264a969d1b9069884d6bd 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/screenshare/ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr.scala
@@ -10,7 +10,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
 
   def handle(msg: ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
     def broadcastEvent(voiceConf: String, screenshareConf: String, stream: String, vidWidth: Int, vidHeight: Int,
-                       timestamp: String): BbbCommonEnvCoreMsg = {
+                       timestamp: String, hasAudio: Boolean): BbbCommonEnvCoreMsg = {
 
       val routing = Routing.addMsgToClientRouting(
         MessageTypes.BROADCAST_TO_MEETING,
@@ -23,7 +23,7 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
       )
 
       val body = ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf, screenshareConf,
-        stream, vidWidth, vidHeight, timestamp)
+        stream, vidWidth, vidHeight, timestamp, hasAudio)
       val event = ScreenshareRtmpBroadcastStartedEvtMsg(header, body)
       BbbCommonEnvCoreMsg(envelope, event)
     }
@@ -41,12 +41,13 @@ trait ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgHdlr {
       ScreenshareModel.setVoiceConf(liveMeeting.screenshareModel, msg.body.voiceConf)
       ScreenshareModel.setScreenshareConf(liveMeeting.screenshareModel, msg.body.screenshareConf)
       ScreenshareModel.setTimestamp(liveMeeting.screenshareModel, msg.body.timestamp)
+      ScreenshareModel.setHasAudio(liveMeeting.screenshareModel, msg.body.hasAudio)
 
       log.info("START broadcast ALLOWED when isBroadcastingRTMP=false")
 
       // Notify viewers in the meeting that there's an rtmp stream to view
       val msgEvent = broadcastEvent(msg.body.voiceConf, msg.body.screenshareConf, msg.body.stream,
-        msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp)
+        msg.body.vidWidth, msg.body.vidHeight, msg.body.timestamp, msg.body.hasAudio)
       bus.outGW.send(msgEvent)
     } else {
       log.info("START broadcast NOT ALLOWED when isBroadcastingRTMP=true")
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
index f9327ec6adb7523344d8000cca9348fa63675284..0242b524e0501582e6007c0b506dfea901d46a4c 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
@@ -83,7 +83,7 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
           if (((ScreenshareRTMPBroadcastEvent) event).getBroadcast()) {
             ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event;
             vcs.deskShareRTMPBroadcastStarted(evt.getRoom(), evt.getBroadcastingStreamUrl(),
-              evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp());
+              evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp(), evt.getHasAudio());
           } else {
             ScreenshareRTMPBroadcastEvent evt = (ScreenshareRTMPBroadcastEvent) event;
             vcs.deskShareRTMPBroadcastStopped(evt.getRoom(), evt.getBroadcastingStreamUrl(),
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
index 77012629d24f1fdd011d28eca0a1b40adde74b18..c60c26984314f2e73e84728a76116adccd6bda1a 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
@@ -55,7 +55,8 @@ public interface IVoiceConferenceService {
                                      String streamname,
                                      Integer videoWidth,
                                      Integer videoHeight,
-                                     String timestamp);
+                                     String timestamp,
+                                     boolean hasAudio);
 
   void deskShareRTMPBroadcastStopped(String room,
                                      String streamname,
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java
index ea8c826888d50921df2be86b7ed860de6c660b84..814a64ff1dce62f5b1910690a6ded0b2047bc557 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/ScreenshareRTMPBroadcastEvent.java
@@ -25,6 +25,7 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
 	private String streamUrl;
 	private Integer vw;
 	private Integer vh;
+	private boolean hasAudio;
 
 	private final String SCREENSHARE_SUFFIX = "-SCREENSHARE";
 
@@ -46,6 +47,10 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
 
 	public void setVideoHeight(Integer vh) {this.vh = vh;}
 
+	public void setHasAudio(boolean hasAudio) {
+		this.hasAudio = hasAudio;
+	}
+
 	public Integer getVideoHeight() {return vh;}
 
 	public Integer getVideoWidth() {return vw;}
@@ -65,4 +70,8 @@ public class ScreenshareRTMPBroadcastEvent extends VoiceConferenceEvent {
 	public boolean getBroadcast() {
 		return broadcast;
 	}
+
+	public boolean getHasAudio() {
+		return hasAudio;
+	}
 }
diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
index 7d63c4f1167e9a93bd83fb5330519507c9c3c8bf..c6da363f8c92b878ec8fa31ca50536ac18f5a703 100755
--- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
+++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
@@ -237,13 +237,14 @@ class VoiceConferenceService(healthz: HealthzService,
       streamname:  String,
       vw:          java.lang.Integer,
       vh:          java.lang.Integer,
-      timestamp:   String
+      timestamp:   String,
+      hasAudio:    Boolean
   ) {
 
     val header = BbbCoreVoiceConfHeader(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, voiceConfId)
     val body = ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf = voiceConfId, screenshareConf = voiceConfId,
       stream = streamname, vidWidth = vw.intValue(), vidHeight = vh.intValue(),
-      timestamp)
+      timestamp, hasAudio)
     val envelope = BbbCoreEnvelope(ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
 
     val msg = new ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(header, body)
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
index 9916ffddebc43d0b5e9dfe2b2e309e15d9942bed..a6d15a7ffa19d4d5a6b6ca3af0d3bcb8a71e3f77 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
@@ -24,7 +24,7 @@ case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsg(
   extends VoiceStandardMsg
 case class ScreenshareRtmpBroadcastStartedVoiceConfEvtMsgBody(voiceConf: String, screenshareConf: String,
                                                               stream: String, vidWidth: Int, vidHeight: Int,
-                                                              timestamp: String)
+                                                              timestamp: String, hasAudio: Boolean)
 
 /**
  * Sent to clients to notify them of an RTMP stream starting.
@@ -37,7 +37,7 @@ case class ScreenshareRtmpBroadcastStartedEvtMsg(
   extends BbbCoreMsg
 case class ScreenshareRtmpBroadcastStartedEvtMsgBody(voiceConf: String, screenshareConf: String,
                                                      stream: String, vidWidth: Int, vidHeight: Int,
-                                                     timestamp: String)
+                                                     timestamp: String, hasAudio: Boolean)
 
 /**
  * Send by FS that RTMP stream has stopped.
diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
index 00c5bc98cf2565ab43440cb1b4b7cc36ed50cda1..09a8a2af0533d7c69ca501fa0239f3d642bf720e 100755
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -82,7 +82,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
   </script>
   <script src="compatibility/adapter.js?v=VERSION" language="javascript"></script>
   <script src="compatibility/sip.js?v=VERSION" language="javascript"></script>
-  <script src="compatibility/kurento-extension.js?v=VERSION" language="javascript"></script>
   <script src="compatibility/kurento-utils.js?v=VERSION" language="javascript"></script>
 </head>
 <body style="background-color: #06172A">
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js
new file mode 100644
index 0000000000000000000000000000000000000000..0adbe86643ab70df1118d1daa744ba048faf2a66
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/errors.js
@@ -0,0 +1,61 @@
+import {
+  SFU_CLIENT_SIDE_ERRORS,
+  SFU_SERVER_SIDE_ERRORS
+} from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors';
+
+// Mapped getDisplayMedia errors. These are bridge agnostic
+// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
+const GDM_ERRORS = {
+  // Fallback error: 1130
+  1130: 'GetDisplayMediaGenericError',
+  1131: 'AbortError',
+  1132: 'InvalidStateError',
+  1133: 'OverconstrainedError',
+  1134: 'TypeError',
+  1135: 'NotFoundError',
+  1136: 'NotAllowedError',
+  1137: 'NotSupportedError',
+  1138: 'NotReadableError',
+};
+
+// Import as many bridge specific errors you want in this utilitary and shove
+// them into the error class slots down below.
+const CLIENT_SIDE_ERRORS = {
+  1101: "SIGNALLING_TRANSPORT_DISCONNECTED",
+  1102: "SIGNALLING_TRANSPORT_CONNECTION_FAILED",
+  1104: "SCREENSHARE_PLAY_FAILED",
+  1105: "PEER_NEGOTIATION_FAILED",
+  1107: "ICE_STATE_FAILED",
+  1120: "MEDIA_TIMEOUT",
+  1121: "UNKNOWN_ERROR",
+};
+
+const SERVER_SIDE_ERRORS =  {
+  ...SFU_SERVER_SIDE_ERRORS,
+}
+
+const AGGREGATED_ERRORS = {
+  ...CLIENT_SIDE_ERRORS,
+  ...SERVER_SIDE_ERRORS,
+  ...GDM_ERRORS,
+}
+
+const expandErrors = () => {
+  const expandedErrors = Object.keys(AGGREGATED_ERRORS).reduce((map, key) => {
+    map[AGGREGATED_ERRORS[key]] = { errorCode: key, errorMessage: AGGREGATED_ERRORS[key] };
+    return map;
+  }, {});
+
+  return { ...AGGREGATED_ERRORS, ...expandedErrors };
+}
+
+const SCREENSHARING_ERRORS = expandErrors();
+
+export {
+  GDM_ERRORS,
+  BRIDGE_SERVER_SIDE_ERRORS,
+  BRIDGE_CLIENT_SIDE_ERRORS,
+  // All errors, [code]: [message]
+  // Expanded errors. It's AGGREGATED + message: { errorCode, errorMessage }
+  SCREENSHARING_ERRORS,
+}
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
index d3ffdff3b8e00d297ec3c56c6a65360f9b843482..d16adb4c2c6bf202a5d6b23a4e40d498ee76440d 100755
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -1,236 +1,297 @@
 import Auth from '/imports/ui/services/auth';
-import BridgeService from './service';
-import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
-import playAndRetry from '/imports/utils/mediaElementPlayRetry';
 import logger from '/imports/startup/client/logger';
+import BridgeService from './service';
+import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
+import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
+import { SCREENSHARING_ERRORS } from './errors';
 
 const SFU_CONFIG = Meteor.settings.public.kurento;
 const SFU_URL = SFU_CONFIG.wsUrl;
-const CHROME_DEFAULT_EXTENSION_KEY = SFU_CONFIG.chromeDefaultExtensionKey;
-const CHROME_CUSTOM_EXTENSION_KEY = SFU_CONFIG.chromeExtensionKey;
-const CHROME_SCREENSHARE_SOURCES = SFU_CONFIG.screenshare.chromeScreenshareSources;
-const FIREFOX_SCREENSHARE_SOURCE = SFU_CONFIG.screenshare.firefoxScreenshareSource;
-const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
 
-const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY;
+const BRIDGE_NAME = 'kurento'
+const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
+const SEND_ROLE = 'send';
+const RECV_ROLE = 'recv';
 
-const getUserId = () => Auth.userID;
+// the error-code mapping is bridge specific; that's why it's not in the errors util
+const ERROR_MAP = {
+  1301: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_DISCONNECTED,
+  1302: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED,
+  1305: SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED,
+  1307: SCREENSHARING_ERRORS.ICE_STATE_FAILED,
+}
 
-const getMeetingId = () => Auth.meetingID;
+const mapErrorCode = (error) => {
+  const { errorCode } = error;
+  const mappedError = ERROR_MAP[errorCode];
 
-const getUsername = () => Auth.fullname;
+  if (errorCode == null || mappedError == null) return error;
+  error.errorCode = mappedError.errorCode;
+  error.errorMessage = mappedError.errorMessage;
+  error.message = mappedError.errorMessage;
 
-const getSessionToken = () => Auth.sessionToken;
+  return error;
+}
 
 export default class KurentoScreenshareBridge {
-  static normalizeError(error = {}) {
-    const errorMessage = error.name || error.message || error.reason || 'Unknown error';
-    const errorCode = error.code || undefined;
-    const errorReason = error.reason || error.id || 'Undefined reason';
+  constructor() {
+    this.role;
+    this.broker;
+    this._gdmStream;
+    this.hasAudio = false;
+    this.connectionAttempts = 0;
+    this.reconnecting = false;
+    this.reconnectionTimeout;
+    this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
+  }
+
+  get gdmStream() {
+    return this._gdmStream;
+  }
+
+  set gdmStream(stream) {
+    this._gdmStream = stream;
+  }
+
+  outboundStreamReconnect() {
+    const currentRestartIntervalMs = this.restartIntervalMs;
+    const stream = this.gdmStream;
+
+    logger.warn({
+      logCode: 'screenshare_presenter_reconnect',
+      extraInfo: {
+        reconnecting: this.reconnecting,
+        role: this.role,
+        bridge: BRIDGE_NAME
+      },
+    }, `Screenshare presenter session is reconnecting`);
 
-    return { errorMessage, errorCode, errorReason };
+    this.stop();
+    this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
+    this.share(stream, this.onerror).then(() => {
+      this.clearReconnectionTimeout();
+    }).catch(error => {
+      // Error handling is a no-op because it will be "handled" in handlePresenterFailure
+      logger.debug({
+        logCode: 'screenshare_reconnect_failed',
+        extraInfo: {
+          errorCode: error.errorCode,
+          errorMessage: error.errorMessage,
+          reconnecting: this.reconnecting,
+          role: this.role,
+          bridge: BRIDGE_NAME
+        },
+      }, 'Screensharing reconnect failed');
+    });
   }
 
-  static handlePresenterFailure(error, started = false) {
-    const normalizedError = KurentoScreenshareBridge.normalizeError(error);
-    if (!started) {
-      logger.error({
-        logCode: 'screenshare_presenter_error_failed_to_connect',
-        extraInfo: { ...normalizedError },
-      }, `Screenshare presenter failed when trying to start due to ${normalizedError.errorMessage}`);
-    } else {
-      logger.error({
-        logCode: 'screenshare_presenter_error_failed_after_success',
-        extraInfo: { ...normalizedError },
-      }, `Screenshare presenter failed during working session due to ${normalizedError.errorMessage}`);
+  inboundStreamReconnect() {
+    const currentRestartIntervalMs = this.restartIntervalMs;
+
+    logger.warn({
+      logCode: 'screenshare_viewer_reconnect',
+      extraInfo: {
+        reconnecting: this.reconnecting,
+        role: this.role,
+        bridge: BRIDGE_NAME
+      },
+    }, `Screenshare viewer session is reconnecting`);
+
+    // Cleanly stop everything before triggering a reconnect
+    this.stop();
+    // Create new reconnect interval time
+    this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
+    this.view(this.hasAudio).then(() => {
+      this.clearReconnectionTimeout();
+    }).catch(error => {
+      // Error handling is a no-op because it will be "handled" in handleViewerFailure
+      logger.debug({
+        logCode: 'screenshare_reconnect_failed',
+        extraInfo: {
+          errorCode: error.errorCode,
+          errorMessage: error.errorMessage,
+          reconnecting: this.reconnecting,
+          role: this.role,
+          bridge: BRIDGE_NAME
+        },
+      }, 'Screensharing reconnect failed');
+    });
+  }
+
+  handleConnectionTimeoutExpiry() {
+    this.reconnecting = true;
+
+    switch (this.role) {
+      case RECV_ROLE:
+        return this.inboundStreamReconnect();
+      case SEND_ROLE:
+        return this.outboundStreamReconnect();
+      default:
+        this.reconnecting = false;
+        logger.error({
+          logCode: 'screenshare_invalid_role'
+        }, 'Screen sharing with invalid role, wont reconnect');
+        break;
     }
-    return normalizedError;
   }
 
-  static handleViewerFailure(error, started = false) {
-    const normalizedError = KurentoScreenshareBridge.normalizeError(error);
-    if (!started) {
-      logger.error({
-        logCode: 'screenshare_viewer_error_failed_to_connect',
-        extraInfo: { ...normalizedError },
-      }, `Screenshare viewer failed when trying to start due to ${normalizedError.errorMessage}`);
-    } else {
-      logger.error({
-        logCode: 'screenshare_viewer_error_failed_after_success',
-        extraInfo: { ...normalizedError },
-      }, `Screenshare viewer failed during working session due to ${normalizedError.errorMessage}`);
+  maxConnectionAttemptsReached () {
+    return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
+  }
+
+  scheduleReconnect () {
+    if (this.reconnectionTimeout == null) {
+      this.reconnectionTimeout = setTimeout(
+        this.handleConnectionTimeoutExpiry.bind(this),
+        this.restartIntervalMs
+      );
     }
-    return normalizedError;
   }
 
-  static playElement(screenshareMediaElement) {
-    const mediaTagPlayed = () => {
-      logger.info({
-        logCode: 'screenshare_media_play_success',
-      }, 'Screenshare media played successfully');
-    };
+  clearReconnectionTimeout () {
+    this.reconnecting = false;
+    this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
 
-    if (screenshareMediaElement.paused) {
-      // Tag isn't playing yet. Play it.
-      screenshareMediaElement.play()
-        .then(mediaTagPlayed)
-        .catch((error) => {
-          // NotAllowedError equals autoplay issues, fire autoplay handling event.
-          // This will be handled in the screenshare react component.
-          if (error.name === 'NotAllowedError') {
-            logger.error({
-              logCode: 'screenshare_error_autoplay',
-              extraInfo: { errorName: error.name },
-            }, 'Screenshare play failed due to autoplay error');
-            const tagFailedEvent = new CustomEvent('screensharePlayFailed',
-              { detail: { mediaElement: screenshareMediaElement } });
-            window.dispatchEvent(tagFailedEvent);
-          } else {
-            // Tag failed for reasons other than autoplay. Log the error and
-            // try playing again a few times until it works or fails for good
-            const played = playAndRetry(screenshareMediaElement);
-            if (!played) {
-              logger.error({
-                logCode: 'screenshare_error_media_play_failed',
-                extraInfo: { errorName: error.name },
-              }, `Screenshare media play failed due to ${error.name}`);
-            } else {
-              mediaTagPlayed();
-            }
-          }
-        });
-    } else {
-      // Media tag is already playing, so log a success. This is really a
-      // logging fallback for a case that shouldn't happen. But if it does
-      // (ie someone re-enables the autoPlay prop in the element), then it
-      // means the stream is playing properly and it'll be logged.
-      mediaTagPlayed();
+    if (this.reconnectionTimeout) {
+      clearTimeout(this.reconnectionTimeout);
+      this.reconnectionTimeout = null;
     }
   }
 
-  static screenshareElementLoadAndPlay(stream, element, muted) {
-    element.muted = muted;
-    element.pause();
-    element.srcObject = stream;
-    KurentoScreenshareBridge.playElement(element);
+  handleViewerStart() {
+    const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
+
+    if (mediaElement && this.broker && this.broker.webRtcPeer) {
+      const stream = this.broker.webRtcPeer.getRemoteStream();
+      BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio);
+    }
+
+    this.clearReconnectionTimeout();
   }
 
-  kurentoViewLocalPreview() {
-    const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
-    const { webRtcPeer } = window.kurentoManager.kurentoScreenshare;
+  handleBrokerFailure(error) {
+    mapErrorCode(error);
+    const { errorMessage, errorCode } = error;
+
+    logger.error({
+      logCode: 'screenshare_broker_failure',
+      extraInfo: {
+        errorCode, errorMessage,
+        role: this.broker.role,
+        started: this.broker.started,
+        reconnecting: this.reconnecting,
+        bridge: BRIDGE_NAME
+      },
+    }, 'Screenshare broker failure');
 
-    if (webRtcPeer) {
-      const stream = webRtcPeer.getLocalStream();
-      KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true);
+    // Screensharing was already successfully negotiated and error occurred during
+    // during call; schedule a reconnect
+    // If the session has not yet started, a reconnect should already be scheduled
+    if (this.broker.started) {
+      this.scheduleReconnect();
     }
+
+    return error;
   }
 
-  async kurentoViewScreen() {
-    const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
-    let iceServers = [];
-    let started = false;
-
-    try {
-      iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
-    } catch (error) {
-      logger.error({
-        logCode: 'screenshare_viewer_fetchstunturninfo_error',
-        extraInfo: { error },
-      }, 'Screenshare bridge failed to fetch STUN/TURN info, using default');
-      iceServers = getMappedFallbackStun();
-    } finally {
-      const options = {
-        wsUrl: Auth.authenticateURL(SFU_URL),
-        iceServers,
-        logger,
-        userName: getUsername(),
-      };
+  async view(hasAudio = false) {
+    this.hasAudio = hasAudio;
+    this.role = RECV_ROLE;
+    const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
+    const options = {
+      iceServers,
+      userName: Auth.fullname,
+      hasAudio,
+    };
 
-      const onFail = (error) => {
-        KurentoScreenshareBridge.handleViewerFailure(error, started);
-      };
+    this.broker = new ScreenshareBroker(
+      Auth.authenticateURL(SFU_URL),
+      BridgeService.getConferenceBridge(),
+      Auth.userID,
+      Auth.meetingID,
+      this.role,
+      options,
+    );
 
-      // Callback for the kurento-extension.js script. It's called when the whole
-      // negotiation with SFU is successful. This will load the stream into the
-      // screenshare media element and play it manually.
-      const onSuccess = () => {
-        started = true;
-        const { webRtcPeer } = window.kurentoManager.kurentoVideo;
-        if (webRtcPeer) {
-          const stream = webRtcPeer.getRemoteStream();
-          KurentoScreenshareBridge.screenshareElementLoadAndPlay(
-            stream,
-            screenshareMediaElement,
-            true,
-          );
-        }
-      };
+    this.broker.onstart = this.handleViewerStart.bind(this);
+    this.broker.onerror = this.handleBrokerFailure.bind(this);
+    this.broker.onended = this.handleEnded.bind(this);
 
-      window.kurentoWatchVideo(
-        SCREENSHARE_VIDEO_TAG,
-        BridgeService.getConferenceBridge(),
-        getUserId(),
-        getMeetingId(),
-        onFail,
-        onSuccess,
-        options,
-      );
-    }
+    return this.broker.view().finally(this.scheduleReconnect.bind(this));
   }
 
-  kurentoExitVideo() {
-    window.kurentoExitVideo();
+  handlePresenterStart() {
+    logger.info({
+      logCode: 'screenshare_presenter_start_success',
+    }, 'Screenshare presenter started succesfully');
+    this.clearReconnectionTimeout();
+    this.reconnecting = false;
+    this.connectionAttempts = 0;
   }
 
-  async kurentoShareScreen(onFail, stream) {
-    let iceServers = [];
-    try {
-      iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
-    } catch (error) {
-      logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' },
+  handleEnded() {
+    screenShareEndAlert();
+  }
 
-        'Screenshare bridge failed to fetch STUN/TURN info, using default');
-      iceServers = getMappedFallbackStun();
-    } finally {
-      const options = {
-        wsUrl: Auth.authenticateURL(SFU_URL),
-        chromeExtension: CHROME_EXTENSION_KEY,
-        chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES,
-        firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE,
-        iceServers,
-        logger,
-        userName: getUsername(),
-      };
+  share(stream, onFailure) {
+    return new Promise(async (resolve, reject) => {
+      this.onerror = onFailure;
+      this.connectionAttempts += 1;
+      this.role = SEND_ROLE;
+      this.hasAudio = BridgeService.streamHasAudioTrack(stream);
+      this.gdmStream = stream;
 
-      let started = false;
+      const onerror = (error) => {
+        const normalizedError = this.handleBrokerFailure(error);
+        if (this.maxConnectionAttemptsReached()) {
+          this.clearReconnectionTimeout();
+          this.connectionAttempts = 0;
+          onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
 
-      const failureCallback = (error) => {
-        const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started);
-        onFail(normalizedError);
+          return reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
+        }
       };
 
-      const successCallback = () => {
-        started = true;
-        logger.info({
-          logCode: 'screenshare_presenter_start_success',
-        }, 'Screenshare presenter started succesfully');
+      const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
+      const options = {
+        iceServers,
+        userName: Auth.fullname,
+        stream,
+        hasAudio: this.hasAudio,
       };
 
-      options.stream = stream || undefined;
-
-      window.kurentoShareScreen(
-        SCREENSHARE_VIDEO_TAG,
+      this.broker = new ScreenshareBroker(
+        Auth.authenticateURL(SFU_URL),
         BridgeService.getConferenceBridge(),
-        getUserId(),
-        getMeetingId(),
-        failureCallback,
-        successCallback,
+        Auth.userID,
+        Auth.meetingID,
+        this.role,
         options,
       );
-    }
-  }
 
-  kurentoExitScreenShare() {
-    window.kurentoExitScreenShare();
+      this.broker.onerror = onerror.bind(this);
+      this.broker.onstreamended = this.stop.bind(this);
+      this.broker.onstart = this.handlePresenterStart.bind(this);
+      this.broker.onended = this.handleEnded.bind(this);
+
+      this.broker.share().then(() => {
+          this.scheduleReconnect();
+          return resolve();
+        }).catch(reject);
+    });
+  };
+
+  stop() {
+    if (this.broker) {
+      this.broker.stop();
+      // Checks if this session is a sharer and if it's not reconnecting
+      // If that's the case, clear the local sharing state in screen sharing UI
+      // component tracker to be extra sure we won't have any client-side state
+      // inconsistency - prlanzarin
+      if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false);
+      this.broker = null;
+    }
+    this.gdmStream = null;
+    this.clearReconnectionTimeout();
   }
 }
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js
index a8c0c901752b450d98d222cd285c6b4401d441af..202fc80a05f3d3a0d3665793f427540c29bf663d 100644
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js
@@ -1,37 +1,66 @@
 import Meetings from '/imports/api/meetings';
 import logger from '/imports/startup/client/logger';
+import { fetchWebRTCMappedStunTurnServers, getMappedFallbackStun } from '/imports/utils/fetchStunTurnServers';
+import loadAndPlayMediaStream from '/imports/ui/services/bbb-webrtc-sfu/load-play';
+import { SCREENSHARING_ERRORS } from './errors';
 
 const {
   constraints: GDM_CONSTRAINTS,
+  mediaTimeouts: MEDIA_TIMEOUTS,
 } = Meteor.settings.public.kurento.screenshare;
+const {
+  baseTimeout: BASE_MEDIA_TIMEOUT,
+  maxTimeout: MAX_MEDIA_TIMEOUT,
+  maxConnectionAttempts: MAX_CONN_ATTEMPTS,
+  timeoutIncreaseFactor: TIMEOUT_INCREASE_FACTOR,
+} = MEDIA_TIMEOUTS;
 
-const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
+const HAS_DISPLAY_MEDIA = (typeof navigator.getDisplayMedia === 'function'
   || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
 
 const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf;
 
+const normalizeGetDisplayMediaError = (error) => {
+  return SCREENSHARING_ERRORS[error.name] || SCREENSHARING_ERRORS.GetDisplayMediaGenericError;
+};
+
+const getBoundGDM = () => {
+  if (typeof navigator.getDisplayMedia === 'function') {
+    return navigator.getDisplayMedia.bind(navigator);
+  } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
+    return navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
+  }
+}
+
 const getScreenStream = async () => {
   const gDMCallback = (stream) => {
+    // Some older Chromium variants choke on gDM when audio: true by NOT generating
+    // a promise rejection AND not generating a valid input screen stream, need to
+    // work around that manually for now - prlanzarin
+    if (stream == null) {
+      return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError);
+    }
+
     if (typeof stream.getVideoTracks === 'function'
-        && typeof constraints.video === 'object') {
-      stream.getVideoTracks().forEach((track) => {
-        if (typeof track.applyConstraints === 'function') {
-          track.applyConstraints(constraints.video).catch((error) => {
+      && typeof GDM_CONSTRAINTS.video === 'object') {
+      stream.getVideoTracks().forEach(track => {
+        if (typeof track.applyConstraints  === 'function') {
+          track.applyConstraints(GDM_CONSTRAINTS.video).catch(error => {
             logger.warn({
               logCode: 'screenshare_videoconstraint_failed',
               extraInfo: { errorName: error.name, errorCode: error.code },
             },
-            'Error applying screenshare video constraint');
+              'Error applying screenshare video constraint');
           });
         }
       });
     }
 
     if (typeof stream.getAudioTracks === 'function'
-        && typeof constraints.audio === 'object') {
-      stream.getAudioTracks().forEach((track) => {
-        if (typeof track.applyConstraints === 'function') {
-          track.applyConstraints(constraints.audio).catch((error) => {
+      && typeof GDM_CONSTRAINTS.audio === 'object') {
+      stream.getAudioTracks().forEach(track => {
+        if (typeof track.applyConstraints  === 'function') {
+          track.applyConstraints(GDM_CONSTRAINTS.audio).catch(error => {
             logger.warn({
               logCode: 'screenshare_audioconstraint_failed',
               extraInfo: { errorName: error.name, errorCode: error.code },
@@ -44,39 +73,81 @@ const getScreenStream = async () => {
     return Promise.resolve(stream);
   };
 
-  const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null;
+  const getDisplayMedia = getBoundGDM();
 
-  // getDisplayMedia isn't supported, generate no stream and let the legacy
-  // constraint fetcher work its way on kurento-extension.js
-  if (constraints == null) {
-    return Promise.resolve();
-  }
-  if (typeof navigator.getDisplayMedia === 'function') {
-    return navigator.getDisplayMedia(constraints)
+  if (typeof getDisplayMedia === 'function') {
+    return getDisplayMedia(GDM_CONSTRAINTS)
       .then(gDMCallback)
-      .catch((error) => {
+      .catch(error => {
+        const normalizedError = normalizeGetDisplayMediaError(error);
         logger.error({
           logCode: 'screenshare_getdisplaymedia_failed',
-          extraInfo: { errorName: error.name, errorCode: error.code },
+          extraInfo: { errorCode: normalizedError.errorCode, errorMessage: normalizedError.errorMessage },
         }, 'getDisplayMedia call failed');
-        return Promise.resolve();
-      });
-  } if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
-    return navigator.mediaDevices.getDisplayMedia(constraints)
-      .then(gDMCallback)
-      .catch((error) => {
-        logger.error({
-          logCode: 'screenshare_getdisplaymedia_failed',
-          extraInfo: { errorName: error.name, errorCode: error.code },
-        }, 'getDisplayMedia call failed');
-        return Promise.resolve();
+        return Promise.reject(normalizedError);
       });
+  } else {
+    // getDisplayMedia isn't supported, error its way out
+    return Promise.reject(SCREENSHARING_ERRORS.NotSupportedError);
   }
 };
 
+const getIceServers = (sessionToken) => {
+  return fetchWebRTCMappedStunTurnServers(sessionToken).catch(error => {
+    logger.error({
+      logCode: 'screenshare_fetchstunturninfo_error',
+      extraInfo: { error }
+    }, 'Screenshare bridge failed to fetch STUN/TURN info');
+    return getMappedFallbackStun();
+  });
+}
+
+const getNextReconnectionInterval = (oldInterval) => {
+  return Math.min(
+    TIMEOUT_INCREASE_FACTOR * oldInterval,
+    MAX_MEDIA_TIMEOUT,
+  );
+}
+
+const streamHasAudioTrack = (stream) => {
+  return stream
+    && typeof stream.getAudioTracks === 'function'
+    && stream.getAudioTracks().length >= 1;
+}
+
+const dispatchAutoplayHandlingEvent = (mediaElement) => {
+  const tagFailedEvent = new CustomEvent('screensharePlayFailed',
+    { detail: { mediaElement } });
+  window.dispatchEvent(tagFailedEvent);
+}
+
+const screenshareLoadAndPlayMediaStream = (stream, mediaElement, muted) => {
+  return loadAndPlayMediaStream(stream, mediaElement, muted).catch(error => {
+    // NotAllowedError equals autoplay issues, fire autoplay handling event.
+    // This will be handled in the screenshare react component.
+    if (error.name === 'NotAllowedError') {
+      logger.error({
+        logCode: 'screenshare_error_autoplay',
+        extraInfo: { errorName: error.name },
+      }, 'Screen share media play failed: autoplay error');
+      dispatchAutoplayHandlingEvent(mediaElement);
+    } else {
+      throw {
+        errorCode: SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode,
+        errorMessage: error.message || SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorMessage,
+      };
+    }
+  });
+}
 
 export default {
-  hasDisplayMedia,
+  HAS_DISPLAY_MEDIA,
   getConferenceBridge,
   getScreenStream,
+  getIceServers,
+  getNextReconnectionInterval,
+  streamHasAudioTrack,
+  screenshareLoadAndPlayMediaStream,
+  BASE_MEDIA_TIMEOUT,
+  MAX_CONN_ATTEMPTS,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 843e615f84c6e3b7199ac4fec54f0bda48bda949..68331d5dba1ed931663eba8e8878128343a21cc9 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -1,8 +1,8 @@
 import React, { PureComponent } from 'react';
 import cx from 'classnames';
 import { styles } from './styles.scss';
-import DesktopShare from './desktop-share/component';
 import ActionsDropdown from './actions-dropdown/container';
+import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
 import AudioControlsContainer from '../audio/audio-controls/container';
 import JoinVideoOptionsContainer from '../video-provider/video-button/container';
 import CaptionsButtonContainer from '/imports/ui/components/actions-bar/captions/container';
@@ -13,20 +13,14 @@ class ActionsBar extends PureComponent {
   render() {
     const {
       amIPresenter,
-      handleShareScreen,
-      handleUnshareScreen,
-      isVideoBroadcasting,
       amIModerator,
-      screenSharingCheck,
       enableVideo,
       isLayoutSwapped,
       toggleSwapLayout,
       handleTakePresenter,
       intl,
       isSharingVideo,
-      screenShareEndAlert,
       stopExternalVideoShare,
-      screenshareDataSavingSetting,
       isCaptionsAvailable,
       isMeteorConnected,
       isPollingEnabled,
@@ -75,15 +69,9 @@ class ActionsBar extends PureComponent {
               <JoinVideoOptionsContainer />
             )
             : null}
-          <DesktopShare {...{
-            handleShareScreen,
-            handleUnshareScreen,
-            isVideoBroadcasting,
+          <ScreenshareButtonContainer {...{
             amIPresenter,
-            screenSharingCheck,
-            screenShareEndAlert,
             isMeteorConnected,
-            screenshareDataSavingSetting,
           }}
           />
         </div>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index b5dad9d6c3318cc9fe50191075e71375af7d0273..5893a0aeb4a9b9ed4fd211b6a27593248b0de255 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -11,12 +11,8 @@ import Service from './service';
 import ExternalVideoService from '/imports/ui/components/external-video-player/service';
 import CaptionsService from '/imports/ui/components/captions/service';
 import {
-  shareScreen,
-  unshareScreen,
   isVideoBroadcasting,
-  screenShareEndAlert,
-  dataSavingSetting,
-} from '../screenshare/service';
+} from '/imports/ui/components/screenshare/service';
 
 import MediaService, {
   getSwapLayout,
@@ -31,10 +27,6 @@ export default withTracker(() => ({
   amIPresenter: Service.amIPresenter(),
   amIModerator: Service.amIModerator(),
   stopExternalVideoShare: ExternalVideoService.stopWatching,
-  handleShareScreen: onFail => shareScreen(onFail),
-  handleUnshareScreen: () => unshareScreen(),
-  isVideoBroadcasting: isVideoBroadcasting(),
-  screenSharingCheck: getFromUserSettings('bbb_enable_screen_sharing', Meteor.settings.public.kurento.enableScreensharing),
   enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo),
   isLayoutSwapped: getSwapLayout() && shouldEnableSwapLayout(),
   toggleSwapLayout: MediaService.toggleSwapLayout,
@@ -42,8 +34,6 @@ export default withTracker(() => ({
   currentSlidHasContent: PresentationService.currentSlidHasContent(),
   parseCurrentSlideContent: PresentationService.parseCurrentSlideContent,
   isSharingVideo: Service.isSharingVideo(),
-  screenShareEndAlert,
-  screenshareDataSavingSetting: dataSavingSetting(),
   isCaptionsAvailable: CaptionsService.isCaptionsAvailable(),
   isMeteorConnected: Meteor.status().connected,
   isPollingEnabled: POLLING_ENABLED,
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
deleted file mode 100755
index 498f0d50e31dfd6260df0404aac21881db6bc3dd..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import React, { memo } from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import browser from 'browser-detect';
-import Button from '/imports/ui/components/button/component';
-import logger from '/imports/startup/client/logger';
-import { notify } from '/imports/ui/services/notification';
-import cx from 'classnames';
-import Modal from '/imports/ui/components/modal/simple/component';
-import { withModalMounter } from '../../modal/service';
-import { styles } from '../styles';
-import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
-
-const propTypes = {
-  intl: PropTypes.object.isRequired,
-  amIPresenter: PropTypes.bool.isRequired,
-  handleShareScreen: PropTypes.func.isRequired,
-  handleUnshareScreen: PropTypes.func.isRequired,
-  isVideoBroadcasting: PropTypes.bool.isRequired,
-  screenSharingCheck: PropTypes.bool.isRequired,
-  screenShareEndAlert: PropTypes.func.isRequired,
-  isMeteorConnected: PropTypes.bool.isRequired,
-  screenshareDataSavingSetting: PropTypes.bool.isRequired,
-};
-
-const intlMessages = defineMessages({
-  desktopShareLabel: {
-    id: 'app.actionsBar.actionsDropdown.desktopShareLabel',
-    description: 'Desktop Share option label',
-  },
-  lockedDesktopShareLabel: {
-    id: 'app.actionsBar.actionsDropdown.lockedDesktopShareLabel',
-    description: 'Desktop locked 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',
-  },
-  genericError: {
-    id: 'app.screenshare.genericError',
-    description: 'error message for when screensharing fails with unknown error',
-  },
-  NotAllowedError: {
-    id: 'app.screenshare.notAllowed',
-    description: 'error message when screen access was not granted',
-  },
-  NotSupportedError: {
-    id: 'app.screenshare.notSupportedError',
-    description: 'error message when trying to share screen in unsafe environments',
-  },
-  screenShareNotSupported: {
-    id: 'app.media.screenshare.notSupported',
-    descriptions: 'error message when trying share screen on unsupported browsers',
-  },
-  screenShareUnavailable: {
-    id: 'app.media.screenshare.unavailable',
-    descriptions: 'title for unavailable screen share modal',
-  },
-  NotReadableError: {
-    id: 'app.screenshare.notReadableError',
-    description: 'error message when the browser failed to capture the screen',
-  },
-  1108: {
-    id: 'app.deskshare.iceConnectionStateError',
-    description: 'Error message for ice connection state failure',
-  },
-  2000: {
-    id: 'app.sfu.mediaServerConnectionError2000',
-    description: 'Error message fired when the SFU cannot connect to the media server',
-  },
-  2001: {
-    id: 'app.sfu.mediaServerOffline2001',
-    description: 'error message when SFU is offline',
-  },
-  2002: {
-    id: 'app.sfu.mediaServerNoResources2002',
-    description: 'Error message fired when the media server lacks disk, CPU or FDs',
-  },
-  2003: {
-    id: 'app.sfu.mediaServerRequestTimeout2003',
-    description: 'Error message fired when requests are timing out due to lack of resources',
-  },
-  2021: {
-    id: 'app.sfu.serverIceGatheringFailed2021',
-    description: 'Error message fired when the server cannot enact ICE gathering',
-  },
-  2022: {
-    id: 'app.sfu.serverIceStateFailed2022',
-    description: 'Error message fired when the server endpoint transitioned to a FAILED ICE state',
-  },
-  2200: {
-    id: 'app.sfu.mediaGenericError2200',
-    description: 'Error message fired when the SFU component generated a generic error',
-  },
-  2202: {
-    id: 'app.sfu.invalidSdp2202',
-    description: 'Error message fired when the clients provides an invalid SDP',
-  },
-  2203: {
-    id: 'app.sfu.noAvailableCodec2203',
-    description: 'Error message fired when the server has no available codec for the client',
-  },
-});
-
-const BROWSER_RESULTS = browser();
-const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false)
-  || (BROWSER_RESULTS && BROWSER_RESULTS.os
-    ? BROWSER_RESULTS.os.includes('Android') // mobile flag doesn't always work
-    : false);
-const IS_SAFARI = BROWSER_RESULTS.name === 'safari';
-
-const DesktopShare = ({
-  intl,
-  handleShareScreen,
-  handleUnshareScreen,
-  isVideoBroadcasting,
-  amIPresenter,
-  screenSharingCheck,
-  screenShareEndAlert,
-  isMeteorConnected,
-  screenshareDataSavingSetting,
-  mountModal,
-}) => {
-  // This is the failure callback that will be passed to the /api/screenshare/kurento.js
-  // script on the presenter's call
-  const onFail = (normalizedError) => {
-    const { errorCode, errorMessage, errorReason } = normalizedError;
-    const error = errorCode || errorMessage || errorReason;
-    // We have a properly mapped error for this. Exit screenshare and show  a toast notification
-    if (intlMessages[error]) {
-      window.kurentoExitScreenShare();
-      notify(intl.formatMessage(intlMessages[error]), 'error', 'desktop');
-    } else {
-      // Unmapped error. Log it (so we can infer what's going on), close screenSharing
-      // session and display generic error message
-      logger.error({
-        logCode: 'screenshare_default_error',
-        extraInfo: {
-          errorCode, errorMessage, errorReason,
-        },
-      }, 'Default error handler for screenshare');
-      window.kurentoExitScreenShare();
-      notify(intl.formatMessage(intlMessages.genericError), 'error', 'desktop');
-    }
-    // Don't trigger the screen share end alert if presenter click to cancel on screen share dialog
-    if (error !== 'NotAllowedError') {
-      screenShareEndAlert();
-    }
-  };
-
-  const screenshareLocked = screenshareDataSavingSetting
-    ? intlMessages.desktopShareLabel : intlMessages.lockedDesktopShareLabel;
-
-  const vLabel = isVideoBroadcasting
-    ? intlMessages.stopDesktopShareLabel : screenshareLocked;
-
-  const vDescr = isVideoBroadcasting
-    ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
-
-  const shouldAllowScreensharing = screenSharingCheck
-    && !isMobileBrowser
-    && amIPresenter;
-
-  return shouldAllowScreensharing
-    ? (
-      <Button
-        className={cx(isVideoBroadcasting || styles.btn)}
-        disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting}
-        icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
-        label={intl.formatMessage(vLabel)}
-        description={intl.formatMessage(vDescr)}
-        color={isVideoBroadcasting ? 'primary' : 'default'}
-        ghost={!isVideoBroadcasting}
-        hideLabel
-        circle
-        size="lg"
-        onClick={isVideoBroadcasting ? handleUnshareScreen : () => {
-          if (IS_SAFARI && !ScreenshareBridgeService.hasDisplayMedia) {
-            return mountModal(<Modal
-              overlayClassName={styles.overlay}
-              className={styles.modal}
-              onRequestClose={() => mountModal(null)}
-              hideBorder
-              contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)}
-            >
-              <h3 className={styles.title}>
-                {intl.formatMessage(intlMessages.screenShareUnavailable)}
-              </h3>
-              <p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p>
-                              </Modal>);
-          }
-          handleShareScreen(onFail);
-        }
-        }
-        id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
-      />
-    ) : null;
-};
-
-DesktopShare.propTypes = propTypes;
-export default withModalMounter(injectIntl(memo(DesktopShare)));
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..64dc749da3acd1d22649ee19fe1e38035063c73f
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx
@@ -0,0 +1,209 @@
+import React, { memo } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+import browser from 'browser-detect';
+import Button from '/imports/ui/components/button/component';
+import logger from '/imports/startup/client/logger';
+import { notify } from '/imports/ui/services/notification';
+import cx from 'classnames';
+import Modal from '/imports/ui/components/modal/simple/component';
+import { withModalMounter } from '../../modal/service';
+import { styles } from '../styles';
+import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service';
+import {
+  shareScreen,
+  stop,
+  screenshareHasEnded,
+  screenShareEndAlert,
+  isVideoBroadcasting,
+} from '/imports/ui/components/screenshare/service';
+import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
+
+const BROWSER_RESULTS = browser();
+const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false)
+  || (BROWSER_RESULTS && BROWSER_RESULTS.os
+    ? BROWSER_RESULTS.os.includes('Android') // mobile flag doesn't always work
+    : false);
+const IS_SAFARI = BROWSER_RESULTS.name === 'safari';
+
+const propTypes = {
+  intl: PropTypes.objectOf(Object).isRequired,
+  enabled: PropTypes.bool.isRequired,
+  amIPresenter: PropTypes.bool.isRequired,
+  isVideoBroadcasting: PropTypes.bool.isRequired,
+  isMeteorConnected: PropTypes.bool.isRequired,
+  screenshareDataSavingSetting: PropTypes.bool.isRequired,
+};
+
+const intlMessages = defineMessages({
+  desktopShareLabel: {
+    id: 'app.actionsBar.actionsDropdown.desktopShareLabel',
+    description: 'Desktop Share option label',
+  },
+  lockedDesktopShareLabel: {
+    id: 'app.actionsBar.actionsDropdown.lockedDesktopShareLabel',
+    description: 'Desktop locked 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',
+  },
+  screenShareNotSupported: {
+    id: 'app.media.screenshare.notSupported',
+    descriptions: 'error message when trying share screen on unsupported browsers',
+  },
+  screenShareUnavailable: {
+    id: 'app.media.screenshare.unavailable',
+    descriptions: 'title for unavailable screen share modal',
+  },
+  finalError: {
+    id: 'app.screenshare.screenshareFinalError',
+    description: 'Screen sharing failures with no recovery procedure',
+  },
+  retryError: {
+    id: 'app.screenshare.screenshareRetryError',
+    description: 'Screen sharing failures where a retry is recommended',
+  },
+  retryOtherEnvError: {
+    id: 'app.screenshare.screenshareRetryOtherEnvError',
+    description: 'Screen sharing failures where a retry in another environment is recommended',
+  },
+  unsupportedEnvError: {
+    id: 'app.screenshare.screenshareUnsupportedEnv',
+    description: 'Screen sharing is not supported, changing browser or device is recommended',
+  },
+  permissionError: {
+    id: 'app.screenshare.screensharePermissionError',
+    description: 'Screen sharing failure due to lack of permission',
+  },
+});
+
+const getErrorLocale = (errorCode) => {
+  switch (errorCode) {
+    // Denied getDisplayMedia permission error
+    case SCREENSHARING_ERRORS.NotAllowedError.errorCode:
+      return intlMessages.permissionError;
+    // Browser is supposed to be supported, but a browser-related error happening.
+    // Suggest retrying in another device/browser/env
+    case SCREENSHARING_ERRORS.AbortError.errorCode:
+    case SCREENSHARING_ERRORS.InvalidStateError.errorCode:
+    case SCREENSHARING_ERRORS.OverconstrainedError.errorCode:
+    case SCREENSHARING_ERRORS.TypeError.errorCode:
+    case SCREENSHARING_ERRORS.NotFoundError.errorCode:
+    case SCREENSHARING_ERRORS.NotReadableError.errorCode:
+    case SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED.errorCode:
+    case SCREENSHARING_ERRORS.SCREENSHARE_PLAY_FAILED.errorCode:
+    case SCREENSHARING_ERRORS.MEDIA_NO_AVAILABLE_CODEC.errorCode:
+    case SCREENSHARING_ERRORS.MEDIA_INVALID_SDP.errorCode:
+      return intlMessages.retryOtherEnvError;
+    // Fatal errors where a retry isn't warranted. This probably means the server
+    // is misconfigured somehow or the provider is utterly botched, so nothing
+    // the end user can do besides requesting support
+    case SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED.errorCode:
+    case SCREENSHARING_ERRORS.MEDIA_SERVER_CONNECTION_ERROR.errorCode:
+    case SCREENSHARING_ERRORS.SFU_INVALID_REQUEST.errorCode:
+      return intlMessages.finalError;
+    // Unsupported errors
+    case SCREENSHARING_ERRORS.NotSupportedError.errorCode:
+      return intlMessages.unsupportedEnvError;
+    // Fall through: everything else is an error which might be solved with a retry
+    default:
+      return intlMessages.retryError;
+  }
+}
+
+const ScreenshareButton = ({
+  intl,
+  enabled,
+  isVideoBroadcasting,
+  amIPresenter,
+  isMeteorConnected,
+  screenshareDataSavingSetting,
+  mountModal,
+}) => {
+  // This is the failure callback that will be passed to the /api/screenshare/kurento.js
+  // script on the presenter's call
+  const handleFailure = (error) => {
+    const {
+      errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode,
+      errorMessage
+    } = error;
+
+    logger.error({
+      logCode: 'screenshare_failed',
+      extraInfo: { errorCode, errorMessage },
+    }, 'Screenshare failed');
+
+    const localizedError = getErrorLocale(errorCode);
+    notify(intl.formatMessage(localizedError, { 0: errorCode }), 'error', 'desktop');
+    screenshareHasEnded();
+  };
+
+  const renderScreenshareUnavailableModal = () => {
+    return mountModal(
+      <Modal
+        overlayClassName={styles.overlay}
+        className={styles.modal}
+        onRequestClose={() => mountModal(null)}
+        hideBorder
+        contentLabel={intl.formatMessage(intlMessages.screenShareUnavailable)}
+      >
+        <h3 className={styles.title}>
+          {intl.formatMessage(intlMessages.screenShareUnavailable)}
+        </h3>
+        <p>{intl.formatMessage(intlMessages.screenShareNotSupported)}</p>
+      </Modal>
+    )
+  };
+
+  const screenshareLocked = screenshareDataSavingSetting
+    ? intlMessages.desktopShareLabel : intlMessages.lockedDesktopShareLabel;
+
+  const vLabel = isVideoBroadcasting
+    ? intlMessages.stopDesktopShareLabel : screenshareLocked;
+
+  const vDescr = isVideoBroadcasting
+    ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
+
+  const shouldAllowScreensharing = enabled
+    && !isMobileBrowser
+    && amIPresenter;
+
+  return shouldAllowScreensharing
+    ? (
+      <Button
+        className={cx(isVideoBroadcasting || styles.btn)}
+        disabled={(!isMeteorConnected && !isVideoBroadcasting) || !screenshareDataSavingSetting}
+        icon={isVideoBroadcasting ? 'desktop' : 'desktop_off'}
+        label={intl.formatMessage(vLabel)}
+        description={intl.formatMessage(vDescr)}
+        color={isVideoBroadcasting ? 'primary' : 'default'}
+        ghost={!isVideoBroadcasting}
+        hideLabel
+        circle
+        size="lg"
+        onClick={isVideoBroadcasting
+          ? screenshareHasEnded
+          : () => {
+            if (IS_SAFARI && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) {
+              renderScreenshareUnavailableModal();
+            } else {
+              shareScreen(handleFailure);
+            }
+          }
+        }
+        id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
+      />
+    ) : null;
+};
+
+ScreenshareButton.propTypes = propTypes;
+export default withModalMounter(injectIntl(memo(ScreenshareButton)));
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e100fd6f43753316c9c74142ba0849fac089a88
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/container.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import ScreenshareButton from './component';
+import getFromUserSettings from '/imports/ui/services/users-settings';
+import {
+  isVideoBroadcasting,
+  dataSavingSetting,
+} from '/imports/ui/components/screenshare/service';
+
+const ScreenshareButtonContainer = props => <ScreenshareButton {...props} />;
+
+/*
+ * All props, including the ones that are inherited from actions-bar
+ * isVideoBroadcasting,
+ * amIPresenter,
+ * screenSharingCheck,
+ * isMeteorConnected,
+ * screenshareDataSavingSetting,
+ */
+export default withModalMounter(withTracker(({ mountModal }) => ({
+  isVideoBroadcasting: isVideoBroadcasting(),
+  screenshareDataSavingSetting: dataSavingSetting(),
+  enabled: getFromUserSettings(
+    'bbb_enable_screen_sharing',
+    Meteor.settings.public.kurento.enableScreensharing
+  ),
+}))(ScreenshareButtonContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx
index fa3c77ed970a8e681c85a5a1d9faf224e064d227..0a080fa811238ae4417729b00e69409b2ea27aa6 100755
--- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx
@@ -6,6 +6,8 @@ import logger from '/imports/startup/client/logger';
 import PropTypes from 'prop-types';
 import AudioService from '../audio/service';
 import VideoService from '../video-provider/service';
+import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
+import UserListService from '/imports/ui/components/user-list/service';
 import { styles } from './styles';
 
 const intlMessages = defineMessages({
@@ -108,7 +110,7 @@ class BreakoutJoinConfirmation extends Component {
     }
 
     VideoService.exitVideo();
-    window.kurentoExitScreenShare();
+    if (UserListService.amIPresenter()) screenshareHasEnded();
     if (url === '') {
       logger.error({
         logCode: 'breakoutjoinconfirmation_redirecting_to_url',
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
index 25a0ffae7f8883dfbf56730a7c1d4411750356f5..364c1b45354155fe8106c9bcd0684e98c5d88b3e 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
@@ -7,6 +7,8 @@ import logger from '/imports/startup/client/logger';
 import { styles } from './styles';
 import BreakoutRoomContainer from './breakout-remaining-time/container';
 import VideoService from '/imports/ui/components/video-provider/service';
+import { screenshareHasEnded } from '/imports/ui/components/screenshare/service';
+import UserListService from '/imports/ui/components/user-list/service';
 
 const intlMessages = defineMessages({
   breakoutTitle: {
@@ -224,7 +226,7 @@ class BreakoutRoom extends PureComponent {
                   extraInfo: { logType: 'user_action' },
                 }, 'joining breakout room closed audio in the main room');
                 VideoService.exitVideo();
-                window.kurentoExitScreenShare();
+                if (UserListService.amIPresenter()) screenshareHasEnded();
               }
               }
               disabled={disable}
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
index 49900e02cda1bd65f38708ee062e842321109e3a..5e960f8d65be867d52dd31f4763a3d40af351a8a 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
@@ -7,9 +7,22 @@ import FullscreenButtonContainer from '../fullscreen-button/container';
 import { styles } from './styles';
 import AutoplayOverlay from '../media/autoplay-overlay/component';
 import logger from '/imports/startup/client/logger';
+import cx from 'classnames';
 import playAndRetry from '/imports/utils/mediaElementPlayRetry';
 import PollingContainer from '/imports/ui/components/polling/container';
 import { withLayoutConsumer } from '/imports/ui/components/layout/context';
+import {
+  SCREENSHARE_MEDIA_ELEMENT_NAME,
+  screenshareHasEnded,
+  screenshareHasStarted,
+  getMediaElement,
+  attachLocalPreviewStream,
+} from '/imports/ui/components/screenshare/service';
+import {
+  isStreamStateUnhealthy,
+  subscribeToStreamStateChange,
+  unsubscribeFromStreamStateChange,
+} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
 
 const intlMessages = defineMessages({
   screenShareLabel: {
@@ -33,49 +46,63 @@ class ScreenshareComponent extends React.Component {
       loaded: false,
       isFullscreen: false,
       autoplayBlocked: false,
+      isStreamHealthy: false,
     };
 
-    this.onVideoLoad = this.onVideoLoad.bind(this);
+    this.onLoadedData = this.onLoadedData.bind(this);
     this.onFullscreenChange = this.onFullscreenChange.bind(this);
     this.handleAllowAutoplay = this.handleAllowAutoplay.bind(this);
     this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
     this.failedMediaElements = [];
+    this.onStreamStateChange = this.onStreamStateChange.bind(this);
   }
 
   componentDidMount() {
-    const { presenterScreenshareHasStarted } = this.props;
-    presenterScreenshareHasStarted();
-
+    screenshareHasStarted();
     this.screenshareContainer.addEventListener('fullscreenchange', this.onFullscreenChange);
+    // Autoplay failure handling
     window.addEventListener('screensharePlayFailed', this.handlePlayElementFailed);
+    // Stream health state tracker to propagate UI changes on reconnections
+    subscribeToStreamStateChange('screenshare', this.onStreamStateChange);
+    // Attaches the local stream if it exists to serve as the local presenter preview
+    attachLocalPreviewStream(getMediaElement());
   }
 
   componentDidUpdate(prevProps) {
     const {
-      isPresenter, unshareScreen,
+      isPresenter,
     } = this.props;
     if (isPresenter && !prevProps.isPresenter) {
-      unshareScreen();
+      screenshareHasEnded();
     }
   }
 
   componentWillUnmount() {
     const {
-      presenterScreenshareHasEnded,
-      unshareScreen,
       getSwapLayout,
       shouldEnableSwapLayout,
       toggleSwapLayout,
     } = this.props;
     const layoutSwapped = getSwapLayout() && shouldEnableSwapLayout();
     if (layoutSwapped) toggleSwapLayout();
-    presenterScreenshareHasEnded();
-    unshareScreen();
+    screenshareHasEnded();
     this.screenshareContainer.removeEventListener('fullscreenchange', this.onFullscreenChange);
     window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed);
+    unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange);
+  }
+
+  onStreamStateChange (event) {
+    const { streamState } = event.detail;
+    const { isStreamHealthy } = this.state;
+
+    const newHealthState = !isStreamStateUnhealthy(streamState);
+    event.stopPropagation();
+    if (newHealthState !== isStreamHealthy) {
+      this.setState({ isStreamHealthy: newHealthState });
+    }
   }
 
-  onVideoLoad() {
+  onLoadedData() {
     this.setState({ loaded: true });
   }
 
@@ -147,12 +174,35 @@ class ScreenshareComponent extends React.Component {
     );
   }
 
-  render() {
-    const { loaded, autoplayBlocked, isFullscreen } = this.state;
+  renderAutoplayOverlay() {
     const { intl } = this.props;
 
     return (
-      [!loaded
+      <AutoplayOverlay
+        key={_.uniqueId('screenshareAutoplayOverlay')}
+        autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
+        autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
+        handleAllowAutoplay={this.handleAllowAutoplay}
+      />
+    );
+  }
+
+  render() {
+    const { loaded, autoplayBlocked, isFullscreen, isStreamHealthy } = this.state;
+    const { intl, isPresenter, isGloballyBroadcasting } = this.props;
+
+    // Conditions to render the (re)connecting spinner and the unhealthy stream
+    // grayscale:
+    // 1 - The local media tag has not received any stream data yet
+    // 2 - The user is a presenter and the stream wasn't globally broadcasted yet
+    // 3 - The media was loaded, the stream was globally broadcasted BUT the stream
+    // state transitioned to an unhealthy stream. tl;dr: screen sharing reconnection
+    const shouldRenderConnectingState = !loaded
+      || (isPresenter && !isGloballyBroadcasting)
+      || !isStreamHealthy && loaded && isGloballyBroadcasting;
+
+    return (
+      [(shouldRenderConnectingState)
         ? (
           <div
             key={_.uniqueId('screenshareArea-')}
@@ -163,29 +213,28 @@ class ScreenshareComponent extends React.Component {
         : null,
       !autoplayBlocked
         ? null
-        : (
-          <AutoplayOverlay
-            key={_.uniqueId('screenshareAutoplayOverlay')}
-            autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
-            autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
-            handleAllowAutoplay={this.handleAllowAutoplay}
-          />
-        ),
+        : (this.renderAutoplayOverlay()),
       (
         <div
           className={styles.screenshareContainer}
           key="screenshareContainer"
           ref={(ref) => { this.screenshareContainer = ref; }}
         >
+
           {isFullscreen && <PollingContainer />}
+
           {loaded && this.renderFullscreenButton()}
+
           <video
-            id="screenshareVideo"
-            key="screenshareVideo"
+            id={SCREENSHARE_MEDIA_ELEMENT_NAME}
+            key={SCREENSHARE_MEDIA_ELEMENT_NAME}
             style={{ maxHeight: '100%', width: '100%', height: '100%' }}
             playsInline
-            onLoadedData={this.onVideoLoad}
+            onLoadedData={this.onLoadedData}
             ref={(ref) => { this.videoTag = ref; }}
+            className={cx({
+              [styles.unhealthyStream]: shouldRenderConnectingState,
+            })}
             muted
           />
         </div>
@@ -199,7 +248,4 @@ export default injectIntl(withLayoutConsumer(ScreenshareComponent));
 ScreenshareComponent.propTypes = {
   intl: PropTypes.object.isRequired,
   isPresenter: PropTypes.bool.isRequired,
-  unshareScreen: PropTypes.func.isRequired,
-  presenterScreenshareHasEnded: PropTypes.func.isRequired,
-  presenterScreenshareHasStarted: PropTypes.func.isRequired,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx
index 699204b1365376479d816218d24c5029af3148be..58031bff86741a38b6a5b1a78c8570219686a4b8 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx
@@ -4,14 +4,13 @@ import Users from '/imports/api/users/';
 import Auth from '/imports/ui/services/auth';
 import MediaService, { getSwapLayout, shouldEnableSwapLayout } from '/imports/ui/components/media/service';
 import {
-  isVideoBroadcasting, presenterScreenshareHasEnded, unshareScreen,
-  presenterScreenshareHasStarted,
+  isVideoBroadcasting,
+  isGloballyBroadcasting,
 } from './service';
 import ScreenshareComponent from './component';
 
 const ScreenshareContainer = (props) => {
-  const { isVideoBroadcasting: isVB } = props;
-  if (isVB()) {
+  if (isVideoBroadcasting()) {
     return <ScreenshareComponent {...props} />;
   }
   return null;
@@ -20,11 +19,8 @@ const ScreenshareContainer = (props) => {
 export default withTracker(() => {
   const user = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } });
   return {
+    isGloballyBroadcasting: isGloballyBroadcasting(),
     isPresenter: user.presenter,
-    unshareScreen,
-    isVideoBroadcasting,
-    presenterScreenshareHasStarted,
-    presenterScreenshareHasEnded,
     getSwapLayout,
     shouldEnableSwapLayout,
     toggleSwapLayout: MediaService.toggleSwapLayout,
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
index ca9e566d8ceb3b1951b4810e1c789d1d1cf3679d..16c4552d0c3d91571b2b6c2e0ca3cc98a7a01588 100644
--- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
@@ -11,65 +11,119 @@ import UserListService from '/imports/ui/components/user-list/service';
 import AudioService from '/imports/ui/components/audio/service';
 import {Meteor} from "meteor/meteor";
 
+const SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo';
+
+let _isSharingScreen = false;
+const _sharingScreenDep = {
+  value: false,
+  tracker: new Tracker.Dependency(),
+};
+
+const isSharingScreen = () => {
+  _sharingScreenDep.tracker.depend();
+  return _sharingScreenDep.value;
+};
+
+const setSharingScreen = (isSharingScreen) => {
+  if (_sharingScreenDep.value !== isSharingScreen) {
+    _sharingScreenDep.value = isSharingScreen;
+    _sharingScreenDep.tracker.changed();
+  }
+};
+
+// A simplified, trackable version of isVideoBroadcasting that DOES NOT
+// account for the presenter's local sharing state.
+// It reflects the GLOBAL screen sharing state (akka-apps)
+const isGloballyBroadcasting = () => {
+  const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
+    { fields: { 'screenshare.stream': 1 } });
+
+  return (!screenshareEntry ? false : !!screenshareEntry.screenshare.stream);
+}
+
 // 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
 const isVideoBroadcasting = () => {
+  const sharing = isSharingScreen();
   const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
     { fields: { 'screenshare.stream': 1 } });
+  const screenIsShared = !screenshareEntry ? false : !!screenshareEntry.screenshare.stream;
+
+  if (screenIsShared && isSharingScreen) {
+    setSharingScreen(false);
+  }
+
+  return sharing || screenIsShared;
+};
+
+
+const screenshareHasAudio = () => {
+  const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
+    { fields: { 'screenshare.hasAudio': 1 } });
 
   if (!screenshareEntry) {
     return false;
   }
 
-  return !!screenshareEntry.screenshare.stream;
-};
+  return !!screenshareEntry.screenshare.hasAudio;
+}
 
-// if remote screenshare has been ended disconnect and hide the video stream
-const presenterScreenshareHasEnded = () => {
-  // references a function in the global namespace inside kurento-extension.js
-  // that we load dynamically
-  KurentoBridge.kurentoExitVideo();
+const screenshareHasEnded = () => {
+  if (isSharingScreen()) {
+    setSharingScreen(false);
+  }
+
+  KurentoBridge.stop();
 };
 
-const viewScreenshare = () => {
-  const amIPresenter = UserListService.isUserPresenter(Auth.userID);
-  if (!amIPresenter) {
-    KurentoBridge.kurentoViewScreen();
-  } else {
-    KurentoBridge.kurentoViewLocalPreview();
+const getMediaElement = () => {
+  return document.getElementById(SCREENSHARE_MEDIA_ELEMENT_NAME);
+}
+
+const attachLocalPreviewStream = (mediaElement) => {
+  const stream = KurentoBridge.gdmStream;
+  if (stream && mediaElement) {
+    // Always muted, presenter preview.
+    BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, true);
   }
-};
+}
 
-// if remote screenshare has been started connect and display the video stream
-const presenterScreenshareHasStarted = () => {
-  // WebRTC restrictions may need a capture device permission to release
-  // useful ICE candidates on recvonly/no-gUM peers
-  tryGenerateIceCandidates().then(() => {
-    viewScreenshare();
-  }).catch((error) => {
-    logger.error({
-      logCode: 'screenshare_no_valid_candidate_gum_failure',
-      extraInfo: {
-        errorName: error.name,
-        errorMessage: error.message,
-      },
-    }, `Forced gUM to release additional ICE candidates failed due to ${error.name}.`);
-    // The fallback gUM failed. Try it anyways and hope for the best.
+const screenshareHasStarted = () => {
+  // Presenter's screen preview is local, so skip
+  if (!UserListService.amIPresenter()) {
     viewScreenshare();
-  });
+  }
 };
 
-const shareScreen = (onFail) => {
+const shareScreen = async (onFail) => {
   // stop external video share if running
   const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
+
   if (meeting && meeting.externalVideoUrl) {
     stopWatching();
   }
 
-  BridgeService.getScreenStream().then((stream) => {
-    KurentoBridge.kurentoShareScreen(onFail, stream);
-  }).catch(onFail);
+  try {
+    const stream = await BridgeService.getScreenStream();
+    await KurentoBridge.share(stream, onFail);
+    setSharingScreen(true);
+  } catch (error) {
+    return onFail(error);
+  }
+};
+
+const viewScreenshare = () => {
+  const hasAudio = screenshareHasAudio();
+  KurentoBridge.view(hasAudio).catch((error) => {
+    logger.error({
+      logCode: 'screenshare_view_failed',
+      extraInfo: {
+        errorName: error.name,
+        errorMessage: error.message,
+      },
+    }, `Screenshare viewer failure`);
+  });
 };
 
 const screenShareEndAlert = () => AudioService
@@ -78,19 +132,19 @@ const screenShareEndAlert = () => AudioService
     + Meteor.settings.public.app.instanceId}`
     + '/resources/sounds/ScreenshareOff.mp3');
 
-const unshareScreen = () => {
-  KurentoBridge.kurentoExitScreenShare();
-  screenShareEndAlert();
-};
-
 const dataSavingSetting = () => Settings.dataSaving.viewScreenshare;
 
 export {
+  SCREENSHARE_MEDIA_ELEMENT_NAME,
   isVideoBroadcasting,
-  presenterScreenshareHasEnded,
-  presenterScreenshareHasStarted,
+  screenshareHasEnded,
+  screenshareHasStarted,
   shareScreen,
   screenShareEndAlert,
-  unshareScreen,
   dataSavingSetting,
+  isSharingScreen,
+  setSharingScreen,
+  getMediaElement,
+  attachLocalPreviewStream,
+  isGloballyBroadcasting,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
index 6b66e3b16ec28f2f387231d9947e3d6119b03fe8..fa54e17f32c38689d1f5b725120ad617c79f070d 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
@@ -2,10 +2,9 @@
 
 .connecting {
   @extend .connectingSpinner;
-  z-index: -1;
   background-color: transparent;
   color: var(--color-white);
-  font-size: 2.5rem * 5;
+  font-size: 2.5rem * 3;
 }
 
 .screenshareContainer{
@@ -17,3 +16,7 @@
   width: 100%;
   height: 100%;
 }
+
+.unhealthyStream {
+  filter: grayscale(50%) opacity(50%);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 6cfb414c54152e478aa69c209d50e0f18a762c82..43704ec73ab020dd9ff61cea8eb1c520d949cc7c 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -578,6 +578,10 @@ const isUserPresenter = (userId) => {
   return user ? user.presenter : false;
 };
 
+const amIPresenter = () => {
+  return isUserPresenter(Auth.userID);
+};
+
 export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => {
   const mimeType = 'text/plain';
   const userNamesObj = getUsers()
@@ -646,5 +650,6 @@ export default {
   requestUserInformation,
   focusFirstDropDownItem,
   isUserPresenter,
+  amIPresenter,
   getUsersProp,
 };
diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js
index cf6cc68d47e3dc1a6b3dae8acddb119ddb583f0a..99bd51cce4fb1e9e675221e0e6f375b9b4d8e865 100644
--- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/broker-base-errors.js
@@ -1,9 +1,12 @@
-export default SFU_BROKER_ERRORS = {
+const SFU_CLIENT_SIDE_ERRORS = {
   // 13xx errors are client-side bbb-webrtc-sfu's base broker errors
   1301: "WEBSOCKET_DISCONNECTED",
   1302: "WEBSOCKET_CONNECTION_FAILED",
   1305: "PEER_NEGOTIATION_FAILED",
   1307: "ICE_STATE_FAILED",
+};
+
+const SFU_SERVER_SIDE_ERRORS = {
   // 2xxx codes are server-side bbb-webrtc-sfu errors
   2000: "MEDIA_SERVER_CONNECTION_ERROR",
   2001: "MEDIA_SERVER_OFFLINE",
@@ -22,4 +25,12 @@ export default SFU_BROKER_ERRORS = {
   2210: "MEDIA_CONNECT_ERROR",
   2211: "MEDIA_NOT_FLOWING",
   2300: "SFU_INVALID_REQUEST",
-}
+};
+
+const SFU_BROKER_ERRORS = { ...SFU_SERVER_SIDE_ERRORS, ...SFU_CLIENT_SIDE_ERRORS };
+
+export {
+  SFU_CLIENT_SIDE_ERRORS,
+  SFU_SERVER_SIDE_ERRORS,
+  SFU_BROKER_ERRORS,
+};
diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js
new file mode 100644
index 0000000000000000000000000000000000000000..216ddf780b61be260d86e3e20936181d0596bc60
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/screenshare-broker.js
@@ -0,0 +1,230 @@
+import logger from '/imports/startup/client/logger';
+import BaseBroker from '/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker';
+
+const ON_ICE_CANDIDATE_MSG = 'iceCandidate';
+const SFU_COMPONENT_NAME = 'screenshare';
+
+class ScreenshareBroker extends BaseBroker {
+  constructor(
+    wsUrl,
+    voiceBridge,
+    userId,
+    internalMeetingId,
+    role,
+    options = {},
+  ) {
+    super(SFU_COMPONENT_NAME, wsUrl);
+    this.voiceBridge = voiceBridge;
+    this.userId = userId;
+    this.internalMeetingId = internalMeetingId;
+    this.role = role;
+    this.ws = null;
+    this.webRtcPeer = null;
+    this.hasAudio = false;
+
+    // Optional parameters are: userName, caleeName, iceServers, hasAudio
+    Object.assign(this, options);
+  }
+
+  onstreamended () {
+    // To be implemented by instantiators
+  }
+
+  share () {
+    return this.openWSConnection()
+      .then(this.startScreensharing.bind(this));
+  }
+
+  view () {
+    return this.openWSConnection()
+      .then(this.subscribeToScreenStream.bind(this));
+  }
+
+  onWSMessage (message) {
+    const parsedMessage = JSON.parse(message.data);
+
+    switch (parsedMessage.id) {
+      case 'startResponse':
+        this.processAnswer(parsedMessage);
+        break;
+      case 'playStart':
+        this.onstart();
+        this.started = true;
+        break;
+      case 'stopSharing':
+        this.stop();
+        break;
+      case 'iceCandidate':
+        this.handleIceCandidate(parsedMessage.candidate);
+        break;
+      case 'error':
+        this.handleSFUError(parsedMessage);
+        break;
+      case 'pong':
+        break;
+      default:
+        logger.debug({
+          logCode: `${this.logCodePrefix}_invalid_req`,
+          extraInfo: {
+            messageId: parsedMessage.id || 'Unknown',
+            sfuComponent: this.sfuComponent,
+            role: this.role,
+          }
+        }, `Discarded invalid SFU message`);
+    }
+  }
+
+  handleSFUError (sfuResponse) {
+    const { code, reason } = sfuResponse;
+    const error = BaseBroker.assembleError(code, reason);
+
+    logger.error({
+      logCode: `${this.logCodePrefix}_sfu_error`,
+      extraInfo: {
+        errorCode: code,
+        errorMessage: error.errorMessage,
+        role: this.role,
+        sfuComponent: this.sfuComponent,
+        started: this.started,
+      },
+    }, `Screen sharing failed in SFU`);
+    this.onerror(error);
+  }
+
+  onOfferGenerated (error, sdpOffer) {
+    if (error) {
+      logger.error({
+        logCode: `${this.logCodePrefix}_offer_failure`,
+        extraInfo: {
+          errorMessage: error.name || error.message || 'Unknown error',
+          role: this.role,
+          sfuComponent: this.sfuComponent
+        },
+      }, `Screenshare offer generation failed`);
+      // 1305: "PEER_NEGOTIATION_FAILED",
+      const normalizedError = BaseBroker.assembleError(1305);
+      return this.onerror(error);
+    }
+
+    const message = {
+      id: 'start',
+      type: this.sfuComponent,
+      role: this.role,
+      internalMeetingId: this.internalMeetingId,
+      voiceBridge: this.voiceBridge,
+      userName: this.userName,
+      callerName: this.userId,
+      sdpOffer,
+      hasAudio: !!this.hasAudio,
+    };
+
+    this.sendMessage(message);
+  }
+
+  startScreensharing () {
+    return new Promise((resolve, reject) => {
+      const options = {
+        onicecandidate: (candidate) => {
+          this.onIceCandidate(candidate, this.role);
+        },
+        videoStream: this.stream,
+      };
+
+      this.addIceServers(options);
+      this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => {
+        if (error) {
+          // 1305: "PEER_NEGOTIATION_FAILED",
+          const normalizedError = BaseBroker.assembleError(1305);
+          logger.error({
+            logCode: `${this.logCodePrefix}_peer_creation_failed`,
+            extraInfo: {
+              errorMessage: error.name || error.message || 'Unknown error',
+              errorCode: normalizedError.errorCode,
+              role: this.role,
+              sfuComponent: this.sfuComponent,
+              started: this.started,
+            },
+          }, `Screenshare peer creation failed`);
+          this.onerror(normalizedError);
+          return reject(normalizedError);
+        }
+
+        this.webRtcPeer.iceQueue = [];
+        this.webRtcPeer.generateOffer(this.onOfferGenerated.bind(this));
+
+        const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
+
+        localStream.getVideoTracks()[0].onended = () => {
+          this.webRtcPeer.peerConnection.onconnectionstatechange = null;
+          this.onstreamended();
+        };
+
+        localStream.getVideoTracks()[0].oninactive = () => {
+          this.onstreamended();
+        };
+
+        return resolve();
+      });
+
+      this.webRtcPeer.peerConnection.onconnectionstatechange = () => {
+        this.handleConnectionStateChange('screenshare');
+      };
+    });
+  }
+
+  onIceCandidate (candidate, role) {
+    const message = {
+      id: ON_ICE_CANDIDATE_MSG,
+      role,
+      type: this.sfuComponent,
+      voiceBridge: this.voiceBridge,
+      candidate,
+      callerName: this.userId,
+    };
+
+    this.sendMessage(message);
+  }
+
+  subscribeToScreenStream () {
+    return new Promise((resolve, reject) => {
+      const options = {
+        mediaConstraints: {
+          audio: !!this.hasAudio,
+        },
+        onicecandidate: (candidate) => {
+          this.onIceCandidate(candidate, this.role);
+        },
+      };
+
+      this.addIceServers(options);
+
+      this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, (error) => {
+        if (error) {
+          // 1305: "PEER_NEGOTIATION_FAILED",
+          const normalizedError = BaseBroker.assembleError(1305);
+          logger.error({
+            logCode: `${this.logCodePrefix}_peer_creation_failed`,
+            extraInfo: {
+              errorMessage: error.name || error.message || 'Unknown error',
+              errorCode: normalizedError.errorCode,
+              role: this.role,
+              sfuComponent: this.sfuComponent,
+              started: this.started,
+            },
+          }, `Screenshare peer creation failed`);
+          this.onerror(normalizedError);
+          return reject(normalizedError);
+        }
+        this.webRtcPeer.iceQueue = [];
+        this.webRtcPeer.generateOffer(this.onOfferGenerated.bind(this));
+      });
+
+      this.webRtcPeer.peerConnection.onconnectionstatechange = () => {
+        this.handleConnectionStateChange('screenshare');
+      };
+      return resolve();
+    });
+  }
+}
+
+export default ScreenshareBroker;
diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js
index dbd903148807eda6e73406c484536294afdfd9b3..e26f36af10e2f1fe12a54f1352a53e0596ec7de3 100644
--- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/sfu-base-broker.js
@@ -1,6 +1,6 @@
 import logger from '/imports/startup/client/logger';
 import { notifyStreamStateChange } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
-import SFU_BROKER_ERRORS from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors';
+import { SFU_BROKER_ERRORS } from '/imports/ui/services/bbb-webrtc-sfu/broker-base-errors';
 
 const PING_INTERVAL_MS = 15000;
 
diff --git a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js
index b4e929c4bf33b4de68f73435c5c4735d0beae47b..f45fefdfe63b5c341e79692c2c207d1ac3055ffb 100644
--- a/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js
+++ b/bigbluebutton-html5/imports/ui/services/bbb-webrtc-sfu/stream-state-service.js
@@ -37,7 +37,7 @@ export const unsubscribeFromStreamStateChange = (eventTag, callback) => {
 }
 
 export const isStreamStateUnhealthy = (streamState) => {
-  return streamState === 'disconnected' || streamState === 'failed' || streamState === 'closed';
+  return streamState === 'failed' || streamState === 'closed';
 }
 
 export const isStreamStateHealthy = (streamState) => {
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 57e5099c8cb9c5959d4024f52e8c796b57f51ea3..2e019c7a640e86a8a2357b61e3729d5931fcdd95 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -147,11 +147,15 @@ public:
       # Max timeout: used as the max camera subscribe reconnection timeout. Each
       # subscribe reattempt increases the reconnection timer up to this
       maxTimeout: 60000
-    chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc
-    chromeDefaultExtensionLink: https://chrome.google.com/webstore/detail/bigbluebutton-screenshare/akgoaoikmbmhcopjgakkcepdgdgkjfbc
-    chromeExtensionKey: KEY
-    chromeExtensionLink: LINK
     screenshare:
+      mediaTimeouts:
+        maxConnectionAttempts: 2
+        # Base screen media timeout (send|recv)
+        baseTimeout: 15000
+        # Max timeout: used as the max camera subscribe reconnection timeout. Each
+        # subscribe reattempt increases the reconnection timer up to this
+        maxTimeout: 35000
+        timeoutIncreaseFactor: 1.5
       constraints:
         video:
           frameRate:
@@ -162,10 +166,6 @@ public:
           height:
             max: 1600
         audio: false
-      chromeScreenshareSources:
-        - window
-        - screen
-      firefoxScreenshareSource: window
     # cameraProfiles is an array of:
     # - id: profile identifier
     #   name: human-readable profile name
@@ -589,7 +589,7 @@ private:
     - browser: chromeMobileIOS
       version: Infinity
     - browser: firefox
-      version: 63
+      version: 68
     - browser: firefoxMobile
       version: 68
     - browser: edge
diff --git a/bigbluebutton-html5/public/compatibility/kurento-extension.js b/bigbluebutton-html5/public/compatibility/kurento-extension.js
deleted file mode 100755
index 10395ded953b2947fcfa455fab4f626cb36d87fc..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/public/compatibility/kurento-extension.js
+++ /dev/null
@@ -1,923 +0,0 @@
-const isFirefox = typeof window.InstallTrigger !== 'undefined';
-const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
-const isChrome = !!window.chrome && !isOpera;
-const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
-const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
-const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
-  || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
-
-Kurento = function (
-  tag,
-  voiceBridge,
-  userId,
-  internalMeetingId,
-  onFail,
-  onSuccess,
-  options = {},
-) {
-  this.ws = null;
-  this.video = null;
-  this.screen = null;
-  this.webRtcPeer = null;
-  this.mediaCallback = null;
-
-  this.renderTag = tag;
-  this.voiceBridge = voiceBridge;
-  this.userId = userId;
-  this.internalMeetingId = internalMeetingId;
-
-  // Optional parameters are: userName, caleeName, chromeExtension, wsUrl, iceServers,
-  // chromeScreenshareSources, firefoxScreenshareSource, logger, stream
-
-  Object.assign(this, options);
-
-  this.SEND_ROLE = 'send';
-  this.RECV_ROLE = 'recv';
-  this.SFU_APP = 'screenshare';
-  this.ON_ICE_CANDIDATE_MSG = 'iceCandidate';
-  this.PING_INTERVAL = 15000;
-
-  window.Logger = this.logger || console;
-
-  if (this.wsUrl == null) {
-    this.defaultPath = 'bbb-webrtc-sfu';
-    this.hostName = window.location.hostname;
-    this.wsUrl = `wss://${this.hostName}/${this.defaultPath}`;
-  }
-
-  if (this.chromeScreenshareSources == null) {
-    this.chromeScreenshareSources = ['screen', 'window'];
-  }
-
-  if (this.firefoxScreenshareSource == null) {
-    this.firefoxScreenshareSource = 'window';
-  }
-
-  // Limiting max resolution to WQXGA
-  // In FireFox we force full screen share and in the case
-  // of multiple screens the total area shared becomes too large
-  this.vid_max_width = 2560;
-  this.vid_max_height = 1600;
-  this.width = window.screen.width;
-  this.height = window.screen.height;
-
-
-  this.userId = userId;
-
-  this.pingInterval = null;
-
-  // TODO FIXME we need to implement a handleError method to normalize errors
-  // generated in this script
-  if (onFail != null) {
-    this.onFail = Kurento.normalizeCallback(onFail);
-  } else {
-    const _this = this;
-    this.onFail = function () {
-      _this.logger.error('Default error handler');
-    };
-  }
-
-  if (onSuccess != null) {
-    this.onSuccess = Kurento.normalizeCallback(onSuccess);
-  } else {
-    const _this = this;
-    this.onSuccess = function () {
-      _this.logger.info('Default success handler');
-    };
-  }
-};
-
-this.KurentoManager = function () {
-  this.kurentoVideo = null;
-  this.kurentoScreenshare = null;
-  this.kurentoAudio = null;
-};
-
-KurentoManager.prototype.exitScreenShare = function () {
-  if (typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) {
-    if (this.kurentoScreenshare.logger !== null) {
-      this.kurentoScreenshare.logger.info({ logCode: 'kurentoextension_exit_screenshare_presenter' },
-        'Exiting screensharing as presenter');
-    }
-
-    if(this.kurentoScreenshare.webRtcPeer) {
-      this.kurentoScreenshare.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-    }
-
-    if (this.kurentoScreenshare.ws !== null) {
-      this.kurentoScreenshare.ws.onclose = function () {};
-      this.kurentoScreenshare.ws.close();
-    }
-
-    if (this.kurentoScreenshare.pingInterval) {
-      clearInterval(this.kurentoScreenshare.pingInterval);
-    }
-
-    this.kurentoScreenshare.dispose();
-    this.kurentoScreenshare = null;
-  }
-};
-
-KurentoManager.prototype.exitVideo = function () {
-  try {
-    if (typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
-      if(this.kurentoVideo.webRtcPeer) {
-        this.kurentoVideo.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-      }
-
-      if (this.kurentoVideo.logger !== null) {
-        this.kurentoScreenshare.logger.info({ logCode: 'kurentoextension_exit_screenshare_viewer' },
-          'Exiting screensharing as viewer');
-      }
-
-      if (this.kurentoVideo.ws !== null) {
-        this.kurentoVideo.ws.onclose = function () {};
-        this.kurentoVideo.ws.close();
-      }
-
-      if (this.kurentoVideo.pingInterval) {
-        clearInterval(this.kurentoVideo.pingInterval);
-      }
-
-      this.kurentoVideo.dispose();
-      this.kurentoVideo = null;
-    }
-  }
-  catch (err) {
-    if (this.kurentoVideo) {
-      this.kurentoVideo.dispose();
-      this.kurentoVideo = null;
-    }
-  }
-};
-
-KurentoManager.prototype.exitAudio = function () {
-  if (typeof this.kurentoAudio !== 'undefined' && this.kurentoAudio) {
-    if (this.kurentoAudio.logger !== null) {
-      this.kurentoAudio.logger.info({ logCode: 'kurentoextension_exit_listen_only' },
-        'Exiting listen only');
-    }
-
-    if (this.kurentoAudio.webRtcPeer) {
-      this.kurentoAudio.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-    }
-
-    if (this.kurentoAudio.ws !== null) {
-      this.kurentoAudio.ws.onclose = function () {};
-      this.kurentoAudio.ws.close();
-    }
-
-    if (this.kurentoAudio.pingInterval) {
-      clearInterval(this.kurentoAudio.pingInterval);
-    }
-
-    this.kurentoAudio.dispose();
-    this.kurentoAudio = null;
-  }
-};
-
-
-KurentoManager.prototype.shareScreen = function (tag) {
-  this.exitScreenShare();
-  const obj = Object.create(Kurento.prototype);
-  Kurento.apply(obj, arguments);
-  this.kurentoScreenshare = obj;
-  this.kurentoScreenshare.setScreensharing(tag);
-};
-
-KurentoManager.prototype.joinWatchVideo = function (tag) {
-  this.exitVideo();
-  const obj = Object.create(Kurento.prototype);
-  Kurento.apply(obj, arguments);
-  this.kurentoVideo = obj;
-  this.kurentoVideo.setWatchVideo(tag);
-};
-
-KurentoManager.prototype.getFirefoxScreenshareSource = function () {
-  return this.kurentoScreenshare.firefoxScreenshareSource;
-};
-
-KurentoManager.prototype.getChromeScreenshareSources = function () {
-  return this.kurentoScreenshare.chromeScreenshareSources;
-};
-
-KurentoManager.prototype.getChromeExtensionKey = function () {
-  return this.kurentoScreenshare.chromeExtension;
-};
-
-
-Kurento.prototype.setScreensharing = function (tag) {
-  this.mediaCallback = this.startScreensharing.bind(this);
-  this.create(tag);
-};
-
-Kurento.prototype.create = function (tag) {
-  this.setRenderTag(tag);
-  this.init();
-};
-
-Kurento.prototype.downscaleResolution = function (oldWidth, oldHeight) {
-  const factorWidth = this.vid_max_width / oldWidth;
-  const factorHeight = this.vid_max_height / oldHeight;
-  let width,
-    height;
-
-  if (factorWidth < factorHeight) {
-    width = Math.trunc(oldWidth * factorWidth);
-    height = Math.trunc(oldHeight * factorWidth);
-  } else {
-    width = Math.trunc(oldWidth * factorHeight);
-    height = Math.trunc(oldHeight * factorHeight);
-  }
-
-  return { width, height };
-};
-
-Kurento.prototype.init = function () {
-  const self = this;
-  if ('WebSocket' in window) {
-    this.ws = new WebSocket(this.wsUrl);
-
-    this.ws.onmessage = this.onWSMessage.bind(this);
-    this.ws.onclose = () => {
-      kurentoManager.exitScreenShare();
-      this.logger.error({ logCode: 'kurentoextension_websocket_close' },
-        'WebSocket connection to SFU closed unexpectedly, screenshare/listen only will drop');
-      self.onFail('Websocket connection closed');
-    };
-    this.ws.onerror = (error) => {
-      kurentoManager.exitScreenShare();
-      this.logger.error({
-        logCode: 'kurentoextension_websocket_error',
-        extraInfo: { errorMessage: error.name || error.message || 'Unknown error' }
-      }, 'Error in the WebSocket connection to SFU, screenshare/listen only will drop');
-      self.onFail('Websocket connection error');
-    };
-    this.ws.onopen = function () {
-      self.pingInterval = setInterval(self.ping.bind(self), self.PING_INTERVAL);
-      self.mediaCallback();
-    };
-  } else {
-    this.logger.info({ logCode: 'kurentoextension_websocket_unsupported'},
-      'Browser does not support websockets');
-  }
-};
-
-Kurento.prototype.onWSMessage = function (message) {
-  const parsedMessage = JSON.parse(message.data);
-  switch (parsedMessage.id) {
-    case 'startResponse':
-      this.startResponse(parsedMessage);
-      break;
-    case 'stopSharing':
-      kurentoManager.exitScreenShare();
-      break;
-    case 'iceCandidate':
-      this.handleIceCandidate(parsedMessage.candidate);
-      break;
-    case 'webRTCAudioSuccess':
-      this.onSuccess(parsedMessage.success);
-      break;
-    case 'webRTCAudioError':
-    case 'error':
-      this.handleSFUError(parsedMessage);
-      break;
-    case 'pong':
-      break;
-    default:
-      this.logger.error({
-        logCode: 'kurentoextension_unrecognized_sfu_message',
-        extraInfo: { sfuResponse: parsedMessage }
-      }, `Unrecognized SFU message: ${parsedMessage.id}`);
-  }
-};
-
-Kurento.prototype.setRenderTag = function (tag) {
-  this.renderTag = tag;
-};
-
-Kurento.prototype.processIceQueue = function () {
-  const peer = this.webRtcPeer;
-  while (peer.iceQueue.length) {
-    const candidate = peer.iceQueue.shift();
-    peer.addIceCandidate(candidate, (error) => {
-      if (error) {
-        // Just log the error. We can't be sure if a candidate failure on add is
-        // fatal or not, so that's why we have a timeout set up for negotiations and
-        // listeners for ICE state transitioning to failures, so we won't act on it here
-        this.logger.error({
-          logCode: 'kurentoextension_addicecandidate_error',
-          extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-        }, `Adding ICE candidate failed due to ${error.message}`);
-      }
-    });
-  }
-}
-
-Kurento.prototype.handleIceCandidate = function (candidate) {
-  const peer = this.webRtcPeer;
-  if (peer.negotiated) {
-    peer.addIceCandidate(candidate, (error) => {
-      if (error) {
-        // Just log the error. We can't be sure if a candidate failure on add is
-        // fatal or not, so that's why we have a timeout set up for negotiations and
-        // listeners for ICE state transitioning to failures, so we won't act on it here
-        this.logger.error({
-          logCode: 'kurentoextension_addicecandidate_error',
-          extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-        }, `Adding ICE candidate failed due to ${error.message}`);
-      }
-    });
-  } else {
-    // ICE candidates are queued until a SDP answer has been processed.
-    // This was done due to a long term iOS/Safari quirk where it'd
-    // fail if candidates were added before the offer/answer cycle was completed.
-    // IT STILL HAPPENS - prlanzarin sept 2019
-    peer.iceQueue.push(candidate);
-  }
-}
-
-Kurento.prototype.startResponse = function (message) {
-  if (message.response !== 'accepted') {
-    this.handleSFUError(message);
-  } else {
-    this.logger.info({
-      logCode: 'kurentoextension_start_success',
-      extraInfo: { sfuResponse: message }
-    }, `Start request accepted for ${message.type}`);
-
-    this.webRtcPeer.processAnswer(message.sdpAnswer, (error) => {
-      if (error) {
-        this.logger.error({
-          logCode: 'kurentoextension_peerconnection_processanswer_error',
-          extraInfo: {
-            errorMessage: error.name || error.message || 'Unknown error',
-          },
-        }, `Processing SDP answer from SFU for failed due to ${error.message}`);
-
-        return this.onFail(error);
-      }
-
-      this.logger.info({
-        logCode: 'kurentoextension_process_answer',
-      }, `Answer processed with success`);
-
-      // Mark the peer as negotiated and flush the ICE queue
-      this.webRtcPeer.negotiated = true;
-      this.processIceQueue();
-      // audio calls gets their success callback in a subsequent step (@webRTCAudioSuccess)
-      // due to legacy messaging which I don't intend to break now - prlanzarin
-      if (message.type === 'screenshare') {
-        this.onSuccess()
-      }
-    });
-  }
-};
-
-Kurento.prototype.handleSFUError = function (sfuResponse) {
-  const { type, code, reason, role } = sfuResponse;
-  switch (type) {
-    case 'screenshare':
-      this.logger.error({
-        logCode: 'kurentoextension_screenshare_start_rejected',
-        extraInfo: { sfuResponse }
-      }, `SFU screenshare rejected by SFU with error ${code} = ${reason}`);
-
-      if (role === this.SEND_ROLE) {
-        kurentoManager.exitScreenShare();
-      } else if (role === this.RECV_ROLE) {
-        kurentoManager.exitVideo();
-      }
-      break;
-    case 'audio':
-      this.logger.error({
-        logCode: 'kurentoextension_listenonly_start_rejected',
-        extraInfo: { sfuResponse }
-      }, `SFU listen only rejected by SFU with error ${code} = ${reason}`);
-
-      kurentoManager.exitAudio();
-      break;
-  }
-
-  this.onFail( { code, reason } );
-};
-
-Kurento.prototype.onOfferPresenter = function (error, offerSdp) {
-  const self = this;
-
-  if (error) {
-    this.logger.error({
-      logCode: 'kurentoextension_screenshare_presenter_offer_failure',
-      extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-    }, `Failed to generate peer connection offer for screenshare presenter with error ${error.message}`);
-    this.onFail(error);
-    return;
-  }
-
-  const message = {
-    id: 'start',
-    type: this.SFU_APP,
-    role: this.SEND_ROLE,
-    internalMeetingId: self.internalMeetingId,
-    voiceBridge: self.voiceBridge,
-    callerName: self.userId,
-    sdpOffer: offerSdp,
-    vh: this.height,
-    vw: this.width,
-    userName: self.userName,
-  };
-
-  this.logger.info({
-    logCode: 'kurentoextension_screenshare_request_start_presenter' ,
-    extraInfo: { sfuRequest: message },
-  }, `Screenshare presenter offer generated. Sending start request to SFU`);
-
-  this.sendMessage(message);
-};
-
-
-Kurento.prototype.startScreensharing = function () {
-  if (window.chrome) {
-    if (this.chromeExtension == null && !hasDisplayMedia) {
-      this.logger.error({ logCode: "kurentoextension_screenshare_noextensionkey" },
-        'Screenshare hasnt got a Chrome extension key configured',
-      );
-      // TODO error handling here
-      this.onFail();
-      return;
-    }
-  }
-
-  const options = {
-    localVideo: document.getElementById(this.renderTag),
-    onicecandidate: (candidate) => {
-      this.onIceCandidate(candidate, this.SEND_ROLE);
-    },
-    sendSource: 'desktop',
-    videoStream: this.stream || undefined,
-  };
-
-  let resolution;
-  this.logger.debug({ logCode: 'kurentoextension_screenshare_screen_dimensions'},
-    `Screenshare screen dimensions are ${this.width} x ${this.height}`);
-  if (this.width > this.vid_max_width || this.height > this.vid_max_height) {
-    resolution = this.downscaleResolution(this.width, this.height);
-    this.width = resolution.width;
-    this.height = resolution.height;
-    this.logger.info({ logCode: 'kurentoextension_screenshare_track_resize' },
-      `Screenshare track dimensions have been resized to ${this.width} x ${this.height}`);
-  }
-
-  this.addIceServers(this.iceServers, options);
-
-  this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => {
-    if (error) {
-      this.logger.error({
-        logCode: 'kurentoextension_screenshare_peerconnection_create_error',
-        extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-      }, `WebRTC peer constructor for screenshare (presenter) failed due to ${error.message}`);
-      this.onFail(error);
-      return kurentoManager.exitScreenShare();
-    }
-
-    this.webRtcPeer.iceQueue = [];
-    this.webRtcPeer.generateOffer(this.onOfferPresenter.bind(this));
-
-    const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
-    const _this = this;
-    localStream.getVideoTracks()[0].onended = function () {
-      _this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-      return kurentoManager.exitScreenShare();
-    };
-
-    localStream.getVideoTracks()[0].oninactive = function () {
-      return kurentoManager.exitScreenShare();
-    };
-  });
-  this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => {
-    if (this.webRtcPeer) {
-      const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState;
-      if (iceConnectionState === 'failed' || iceConnectionState === 'closed') {
-        this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-        this.logger.error({
-          logCode: 'kurentoextension_screenshare_presenter_ice_failed',
-          extraInfo: { iceConnectionState }
-        }, `WebRTC peer for screenshare presenter failed due to ICE transitioning to ${iceConnectionState}`);
-        this.onFail({ message: 'iceConnectionStateError', code: 1108 });
-      }
-    }
-  };
-};
-
-Kurento.prototype.onIceCandidate = function (candidate, role) {
-  const self = this;
-  this.logger.debug({
-    logCode: 'kurentoextension_screenshare_client_candidate',
-    extraInfo: { candidate }
-  }, `Screenshare client-side candidate generated: ${JSON.stringify(candidate)}`);
-
-  const message = {
-    id: this.ON_ICE_CANDIDATE_MSG,
-    role,
-    type: this.SFU_APP,
-    voiceBridge: self.voiceBridge,
-    candidate,
-    callerName: self.userId,
-  };
-
-  this.sendMessage(message);
-};
-
-Kurento.prototype.setWatchVideo = function (tag) {
-  this.useVideo = true;
-  this.useCamera = 'none';
-  this.useMic = 'none';
-  this.mediaCallback = this.viewer;
-  this.create(tag);
-};
-
-Kurento.prototype.viewer = function () {
-  const self = this;
-  if (!this.webRtcPeer) {
-    const options = {
-      mediaConstraints: {
-        audio: false,
-      },
-      onicecandidate: (candidate) => {
-        this.onIceCandidate(candidate, this.RECV_ROLE);
-      },
-    };
-
-    this.addIceServers(this.iceServers, options);
-
-    self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) {
-      if (error) {
-        return self.onFail(error);
-      }
-      self.webRtcPeer.iceQueue = [];
-      this.generateOffer(self.onOfferViewer.bind(self));
-    });
-    self.webRtcPeer.peerConnection.oniceconnectionstatechange = () => {
-      if (this.webRtcPeer) {
-        const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState;
-        if (iceConnectionState === 'failed' || iceConnectionState === 'closed') {
-          this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-          this.logger.error({
-            logCode: 'kurentoextension_screenshare_viewer_ice_failed',
-            extraInfo: { iceConnectionState }
-          }, `WebRTC peer for screenshare viewer failed due to ICE transitioning to ${iceConnectionState}`);
-          this.onFail({ message: 'iceConnectionStateError', code: 1108 });
-        }
-      }
-    };
-  }
-};
-
-Kurento.prototype.onOfferViewer = function (error, offerSdp) {
-  const self = this;
-  if (error) {
-    this.logger.error({
-      logCode: 'kurentoextension_screenshare_viewer_offer_failure',
-      extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-    }, `Failed to generate peer connection offer for screenshare viewer with error ${error.message}`);
-
-    return this.onFail(error);
-  }
-
-  const message = {
-    id: 'start',
-    type: this.SFU_APP,
-    role: this.RECV_ROLE,
-    internalMeetingId: self.internalMeetingId,
-    voiceBridge: self.voiceBridge,
-    callerName: self.userId,
-    sdpOffer: offerSdp,
-    userName: self.userName,
-  };
-
-  this.logger.info({
-    logCode: 'kurentoextension_screenshare_request_start_viewer',
-    extraInfo: { sfuRequest: message },
-  }, `Screenshare viewer offer generated. Sending start request to SFU`);
-
-  this.sendMessage(message);
-};
-
-KurentoManager.prototype.joinAudio = function (tag) {
-  this.exitAudio();
-  const obj = Object.create(Kurento.prototype);
-  Kurento.apply(obj, arguments);
-  this.kurentoAudio = obj;
-  this.kurentoAudio.setAudio(tag);
-};
-
-Kurento.prototype.setAudio = function (tag) {
-  this.mediaCallback = this.listenOnly.bind(this);
-  this.create(tag);
-};
-
-Kurento.prototype.listenOnly = function () {
-  if (!this.webRtcPeer) {
-    const options = {
-      onicecandidate : this.onListenOnlyIceCandidate.bind(this),
-      mediaConstraints: {
-        audio: true,
-        video: false,
-      },
-    };
-
-    this.addIceServers(this.iceServers, options);
-
-    this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, (error) => {
-      if (error) {
-        return this.onFail(error);
-      }
-
-      this.webRtcPeer.iceQueue = [];
-      this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => {
-        if (this.webRtcPeer) {
-          const iceConnectionState = this.webRtcPeer.peerConnection.iceConnectionState;
-
-          if (iceConnectionState === 'failed' || iceConnectionState === 'closed') {
-            this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
-            this.logger.error({
-              logCode: 'kurentoextension_listenonly_ice_failed',
-              extraInfo: { iceConnectionState }
-            }, `WebRTC peer for listen only failed due to ICE transitioning to ${iceConnectionState}`);
-            this.onFail({
-              errorCode: 1007,
-              errorMessage: `ICE negotiation failed. Current state - ${iceConnectionState}`,
-            });
-          }
-        }
-      }
-
-      this.webRtcPeer.generateOffer(this.onOfferListenOnly.bind(this));
-    });
-  }
-};
-
-Kurento.prototype.onListenOnlyIceCandidate = function (candidate) {
-  const self = this;
-  this.logger.debug({
-    logCode: 'kurentoextension_listenonly_client_candidate',
-    extraInfo: { candidate }
-  }, `Listen only client-side candidate generated: ${JSON.stringify(candidate)}`);
-
-  const message = {
-    id: this.ON_ICE_CANDIDATE_MSG,
-    type: 'audio',
-    role: 'viewer',
-    voiceBridge: self.voiceBridge,
-    candidate,
-  };
-  this.sendMessage(message);
-};
-
-Kurento.prototype.onOfferListenOnly = function (error, offerSdp) {
-  const self = this;
-  if (error) {
-    this.logger.error({
-      logCode: 'kurentoextension_listenonly_offer_failure',
-      extraInfo: { errorMessage: error.name || error.message || 'Unknown error' },
-    }, `Failed to generate peer connection offer for listen only with error ${error.message}`);
-
-    return this.onFail(error);
-  }
-
-  const message = {
-    id: 'start',
-    type: 'audio',
-    role: 'viewer',
-    voiceBridge: self.voiceBridge,
-    caleeName: self.caleeName,
-    sdpOffer: offerSdp,
-    userId: self.userId,
-    userName: self.userName,
-    internalMeetingId: self.internalMeetingId,
-  };
-
-  this.logger.info({
-    logCode: 'kurentoextension_listenonly_request_start',
-    extraInfo: { sfuRequest: message },
-  }, "Listen only offer generated. Sending start request to SFU");
-  this.sendMessage(message);
-};
-
-Kurento.prototype.pauseTrack = function (message) {
-  const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
-  const track = localStream.getVideoTracks()[0];
-
-  if (track) {
-    track.enabled = false;
-  }
-};
-
-Kurento.prototype.resumeTrack = function (message) {
-  const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
-  const track = localStream.getVideoTracks()[0];
-
-  if (track) {
-    track.enabled = true;
-  }
-};
-
-Kurento.prototype.addIceServers = function (iceServers, options) {
-  if (iceServers && iceServers.length > 0) {
-    this.logger.debug({
-      logCode: 'kurentoextension_add_iceservers',
-      extraInfo: { iceServers }
-    }, `Injecting ICE servers into peer creation`);
-
-    options.configuration = {};
-    options.configuration.iceServers = iceServers;
-  }
-};
-
-Kurento.prototype.stop = function () {
-  // if (this.webRtcPeer) {
-  //  var message = {
-  //    id : 'stop',
-  //    type : 'screenshare',
-  //    voiceBridge: kurentoHandler.voiceBridge
-  //  }
-  //  kurentoHandler.sendMessage(message);
-  //  kurentoHandler.disposeScreenShare();
-  // }
-};
-
-Kurento.prototype.dispose = function () {
-  if (this.webRtcPeer) {
-    this.webRtcPeer.dispose();
-    this.webRtcPeer = null;
-  }
-};
-
-Kurento.prototype.ping = function () {
-  const message = {
-    id: 'ping',
-  };
-  this.sendMessage(message);
-};
-
-Kurento.prototype.sendMessage = function (message) {
-  const jsonMessage = JSON.stringify(message);
-  this.ws.send(jsonMessage);
-};
-
-Kurento.normalizeCallback = function (callback) {
-  if (typeof callback === 'function') {
-    return callback;
-  }
-  return function (args) {
-    document.getElementById('BigBlueButton')[callback](args);
-  };
-};
-
-
-/* Global methods */
-
-// this function explains how to use above methods/objects
-window.getScreenConstraints = function (sendSource, callback) {
-  let screenConstraints = { video: {}, audio: false };
-
-  // Limiting FPS to a range of 5-10 (5 ideal)
-  screenConstraints.video.frameRate = { ideal: 5, max: 10 };
-
-  screenConstraints.video.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
-  screenConstraints.video.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
-
-  const getChromeScreenConstraints = function (extensionKey) {
-    return new Promise((resolve, reject) => {
-      chrome.runtime.sendMessage(
-        extensionKey,
-        {
-          getStream: true,
-          sources: kurentoManager.getChromeScreenshareSources(),
-        },
-        (response) => {
-          resolve(response);
-        }
-      );
-    });
-  };
-
-  const getDisplayMediaConstraints = function () {
-    // The fine-grained constraints (e.g.: frameRate) are supposed to go into
-    // the MediaStream because getDisplayMedia does not support them,
-    // so they're passed differently
-    kurentoManager.kurentoScreenshare.extensionInstalled = true;
-    optionalConstraints.width = { max: kurentoManager.kurentoScreenshare.vid_max_width };
-    optionalConstraints.height = { max: kurentoManager.kurentoScreenshare.vid_max_height };
-    optionalConstraints.frameRate = { ideal: 5, max: 10 };
-
-    let gDPConstraints = {
-      video: true,
-      optional: optionalConstraints
-    };
-
-    return gDPConstraints;
-  };
-
-  const optionalConstraints = [
-    { googCpuOveruseDetection: true },
-    { googCpuOveruseEncodeUsage: true },
-    { googCpuUnderuseThreshold: 55 },
-    { googCpuOveruseThreshold: 100 },
-    { googPayloadPadding: true },
-    { googScreencastMinBitrate: 600 },
-    { googHighStartBitrate: true },
-    { googHighBitrate: true },
-    { googVeryHighBitrate: true },
-  ];
-
-  if (isElectron) {
-    const sourceId = ipcRenderer.sendSync('screen-chooseSync');
-    kurentoManager.kurentoScreenshare.extensionInstalled = true;
-
-    // this statement sets gets 'sourceId" and sets "chromeMediaSourceId"
-    screenConstraints.video.chromeMediaSource = { exact: [sendSource] };
-    screenConstraints.video.chromeMediaSourceId = sourceId;
-    screenConstraints.optional = optionalConstraints;
-
-    return callback(null, screenConstraints);
-  }
-
-  if (isChrome) {
-    if (!hasDisplayMedia) {
-      const extensionKey = kurentoManager.getChromeExtensionKey();
-      getChromeScreenConstraints(extensionKey).then((constraints) => {
-        if (!constraints) {
-          document.dispatchEvent(new Event('installChromeExtension'));
-          return;
-        }
-
-        const sourceId = constraints.streamId;
-
-        kurentoManager.kurentoScreenshare.extensionInstalled = true;
-
-        // Re-wrap the video constraints into the mandatory object (latest adapter)
-        screenConstraints.video = {};
-        screenConstraints.video.mandatory = {};
-        screenConstraints.video.mandatory.maxFrameRate = 10;
-        screenConstraints.video.mandatory.maxHeight = kurentoManager.kurentoScreenshare.vid_max_height;
-        screenConstraints.video.mandatory.maxWidth = kurentoManager.kurentoScreenshare.vid_max_width;
-        screenConstraints.video.mandatory.chromeMediaSource = sendSource;
-        screenConstraints.video.mandatory.chromeMediaSourceId = sourceId;
-        screenConstraints.optional = optionalConstraints;
-
-        return callback(null, screenConstraints);
-      });
-    } else {
-      return callback(null, getDisplayMediaConstraints());
-    }
-  }
-
-  if (isFirefox) {
-    const firefoxScreenshareSource = kurentoManager.getFirefoxScreenshareSource();
-    screenConstraints.video.mediaSource = firefoxScreenshareSource;
-    return callback(null, screenConstraints);
-  }
-
-  // Falls back to getDisplayMedia if the browser supports it
-  if (hasDisplayMedia) {
-    return callback(null, getDisplayMediaConstraints());
-  }
-};
-
-window.kurentoInitialize = function () {
-  if (window.kurentoManager == null || window.KurentoManager === undefined) {
-    window.kurentoManager = new KurentoManager();
-  }
-};
-
-window.kurentoShareScreen = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.shareScreen.apply(window.kurentoManager, arguments);
-};
-
-
-window.kurentoExitScreenShare = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.exitScreenShare();
-};
-
-window.kurentoWatchVideo = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments);
-};
-
-window.kurentoExitVideo = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.exitVideo();
-};
-
-window.kurentoJoinAudio = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.joinAudio.apply(window.kurentoManager, arguments);
-};
-
-window.kurentoExitAudio = function () {
-  window.kurentoInitialize();
-  window.kurentoManager.exitAudio();
-};
diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json
index 6b6499f4fc6c649a99f175fab6ecea6cb9382d1f..421fd097c7c4044f165e91a616dcb5f4d3490ac7 100755
--- a/bigbluebutton-html5/public/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -128,10 +128,11 @@
     "app.media.screenshare.notSupported": "Screensharing is not supported in this browser.",
     "app.media.screenshare.autoplayBlockedDesc": "We need your permission to show you the presenter's screen.",
     "app.media.screenshare.autoplayAllowLabel": "View shared screen",
-    "app.screenshare.notAllowed": "Error: Permission to access screen wasn't granted.",
-    "app.screenshare.notSupportedError": "Error: Screensharing is allowed only on safe (SSL) domains",
-    "app.screenshare.notReadableError": "Error: There was a failure while trying to capture your screen",
-    "app.screenshare.genericError": "Error: An error has occurred with screensharing, please try again",
+    "app.screenshare.screenshareFinalError": "Code {0}. Could not share the screen.",
+    "app.screenshare.screenshareRetryError": "Code {0}. Try sharing the screen again.",
+    "app.screenshare.screenshareRetryOtherEnvError": "Code {0}. Could not share the screen. Try again using a different browser or device.",
+    "app.screenshare.screenshareUnsupportedEnv": "Code {0}. Browser is not supported. Try again using a different browser or device.",
+    "app.screenshare.screensharePermissionError": "Code {0}. Permission to capture the screen needs to be granted.",
     "app.meeting.ended": "This session has ended",
     "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
     "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
@@ -664,7 +665,6 @@
     "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues",
     "app.fullscreenButton.label": "Make {0} fullscreen",
     "app.fullscreenUndoButton.label": "Undo {0} fullscreen",
-    "app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)",
     "app.sfu.mediaServerConnectionError2000": "Unable to connect to media server (error 2000)",
     "app.sfu.mediaServerOffline2001": "Media server is offline. Please try again later (error 2001)",
     "app.sfu.mediaServerNoResources2002": "Media server has no available resources (error 2002)",