diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html
index 477638ae336c7159ba5778f4bd6bf64a4a1e924f..d9628be3ca15bfd8d470bb4250b123594f8df72d 100755
--- a/bigbluebutton-html5/client/main.html
+++ b/bigbluebutton-html5/client/main.html
@@ -67,7 +67,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/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
index 2f9ac06072b253b157bb9675bdfa35f6df5fee15..93ed27748b3a932196f575f0cd9c229dc7b8ee95 100755
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -1,240 +1,259 @@
 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 } from '/imports/ui/components/screenshare/service';
 
 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 BRIDGE_NAME = 'kurento'
 const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
+const SEND_ROLE = 'send';
+const RECV_ROLE = 'recv';
 
-const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY;
+const errorCodeMap = {
+  1301: 1101,
+  1302: 1102,
+  1305: 1105,
+  1307: 1108, // This should be 1107, but I'm preserving the existing locales - prlanzarin
+}
 
-const getUserId = () => Auth.userID;
+const mapErrorCode = (error) => {
+  const { errorCode } = error;
+  const mappedErrorCode = errorCodeMap[errorCode];
+  if (errorCode == null || mappedErrorCode == null) return error;
+  error.errorCode = mappedErrorCode;
+  return error;
+}
 
-const getMeetingId = () => Auth.meetingID;
+export default class KurentoScreenshareBridge {
+  constructor() {
+    this.role;
+    this.broker;
+    this._gdmStream;
+    this.connectionAttempts = 0;
+    this.reconnecting = false;
+    this.reconnectionTimeout;
+    this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
+  }
 
-const getUsername = () => Auth.fullname;
+  get gdmStream() {
+    return this._gdmStream;
+  }
 
-const getSessionToken = () => Auth.sessionToken;
+  set gdmStream(stream) {
+    this._gdmStream = stream;
+  }
 
-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';
+  outboundStreamReconnect() {
+    const currentRestartIntervalMs = this.restartIntervalMs;
+    const stream = this.gdmStream;
+
+    logger.warn({
+      logCode: 'screenshare_presenter_reconnect'
+    }, `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: {
+          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}`);
-    }
-    return normalizedError;
+  inboundStreamReconnect() {
+    const currentRestartIntervalMs = this.restartIntervalMs;
+
+    logger.warn({
+      logCode: 'screenshare_viewer_reconnect',
+    }, `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(stream, this.onerror).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: {
+          errorMessage: error.errorMessage,
+          reconnecting: this.reconnecting,
+          role: this.role,
+          bridge: BRIDGE_NAME
+        },
+      }, 'Screensharing reconnect failed');
+    });
   }
 
-  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}`);
+  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 playElement(screenshareMediaElement) {
-    const mediaTagPlayed = () => {
-      logger.info({
-        logCode: 'screenshare_media_play_success',
-      }, 'Screenshare media played successfully');
-    };
+  maxConnectionAttemptsReached () {
+    return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
+  }
 
-    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();
+  scheduleReconnect () {
+    if (this.reconnectionTimeout == null) {
+      this.reconnectionTimeout = setTimeout(
+        this.handleConnectionTimeoutExpiry.bind(this),
+        this.restartIntervalMs
+      );
     }
-  };
+  }
+
+  clearReconnectionTimeout () {
+    this.reconnecting = false;
+    this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
 
-  static screenshareElementLoadAndPlay(stream, element, muted) {
-    element.muted = muted;
-    element.pause();
-    element.srcObject = stream;
-    KurentoScreenshareBridge.playElement(element);
+    if (this.reconnectionTimeout) {
+      clearTimeout(this.reconnectionTimeout);
+      this.reconnectionTimeout = null;
+    }
   }
 
-  kurentoViewLocalPreview() {
-    const screenshareMediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
-    const { webRtcPeer } = window.kurentoManager.kurentoScreenshare;
+  handleViewerStart() {
+    const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
 
-    if (webRtcPeer) {
-      const stream = webRtcPeer.getLocalStream();
-      KurentoScreenshareBridge.screenshareElementLoadAndPlay(stream, screenshareMediaElement, true);
+    if (mediaElement && this.broker && this.broker.webRtcPeer) {
+      const stream = this.broker.webRtcPeer.getRemoteStream();
+      BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio);
     }
+
+    this.clearReconnectionTimeout();
   }
 
-  async kurentoViewScreen(hasAudio) {
-    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(),
-        hasAudio,
-      };
-
-      const onFail = (error) => {
-        KurentoScreenshareBridge.handleViewerFailure(error, started);
-      };
-
-      // 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,
-            !hasAudio,
-          );
-        }
-      };
-
-      window.kurentoWatchVideo(
-        SCREENSHARE_VIDEO_TAG,
-        BridgeService.getConferenceBridge(),
-        getUserId(),
-        getMeetingId(),
-        onFail,
-        onSuccess,
-        options,
-        hasAudio,
-      );
+  handleBrokerFailure(error) {
+    mapErrorCode(error);
+    BridgeService.handleViewerFailure(error, this.broker.started);
+    // 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();
     }
   }
 
-  kurentoExitVideo() {
-    window.kurentoExitVideo();
+  async view(hasAudio = false) {
+    this.role = RECV_ROLE;
+    const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
+    const options = {
+      iceServers,
+      userName: Auth.fullname,
+      hasAudio,
+    };
+
+    this.broker = new ScreenshareBroker(
+      Auth.authenticateURL(SFU_URL),
+      BridgeService.getConferenceBridge(),
+      Auth.userID,
+      Auth.meetingID,
+      this.role,
+      options,
+    );
+
+    this.broker.onstart = this.handleViewerStart.bind(this);
+    this.broker.onerror = this.handleBrokerFailure.bind(this);
+    this.broker.onstreamended = this.stop.bind(this);
+    return this.broker.view().finally(this.scheduleReconnect.bind(this));
   }
 
-  async kurentoShareScreen(onFail, stream) {
-    let iceServers = [];
-    try {
-      iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
-    } catch (error) {
-      logger.error({ logCode: 'screenshare_presenter_fetchstunturninfo_error' },
-
-        'Screenshare bridge failed to fetch STUN/TURN info, using default');
-      iceServers = getMappedFallbackStun();
-    } finally {
-      const hasAudioTrack = stream.getAudioTracks().length >= 1;
-      const options = {
-        wsUrl: Auth.authenticateURL(SFU_URL),
-        chromeExtension: CHROME_EXTENSION_KEY,
-        chromeScreenshareSources: CHROME_SCREENSHARE_SOURCES,
-        firefoxScreenshareSource: FIREFOX_SCREENSHARE_SOURCE,
-        iceServers,
-        logger,
-        userName: getUsername(),
-        hasAudio: hasAudioTrack,
-      };
-
-      let started = false;
-
-      const failureCallback = (error) => {
-        const normalizedError = KurentoScreenshareBridge.handlePresenterFailure(error, started);
-        onFail(normalizedError);
-      };
-
-      const successCallback = () => {
-        started = true;
-        logger.info({
-          logCode: 'screenshare_presenter_start_success',
-        }, 'Screenshare presenter started succesfully');
-      };
-
-      options.stream = stream || undefined;
-
-      window.kurentoShareScreen(
-        SCREENSHARE_VIDEO_TAG,
-        BridgeService.getConferenceBridge(),
-        getUserId(),
-        getMeetingId(),
-        failureCallback,
-        successCallback,
-        options,
-      );
-    }
+  handlePresenterStart() {
+    logger.info({
+      logCode: 'screenshare_presenter_start_success',
+    }, 'Screenshare presenter started succesfully');
+    this.clearReconnectionTimeout();
+    this.reconnecting = false;
+    this.connectionAttempts = 0;
   }
 
-  kurentoExitScreenShare() {
-    window.kurentoExitScreenShare();
+  async share(stream, onFailure) {
+    this.onerror = onFailure;
+    this.connectionAttempts += 1;
+    this.role = SEND_ROLE;
+    this.gdmStream = stream;
+
+    const onerror = (error) => {
+      mapErrorCode(error);
+      const normalizedError = BridgeService.handlePresenterFailure(error, this.broker.started);
+
+      // Gracious mid call reconnects aren't yet implemented, so stop it.
+      if (this.broker.started) {
+        return onFailure(normalizedError);
+      }
+
+      // Otherwise, sharing attempts have a finite amount of attempts for it
+      // to work (configurable). If expired, error out.
+      if (this.maxConnectionAttemptsReached()) {
+        this.clearReconnectionTimeout();
+        this.connectionAttempts = 0;
+        return onFailure({
+          errorCode: 1120,
+          errorMessage: `MAX_CONNECTION_ATTEMPTS_REACHED`,
+        });
+      }
+    };
+
+    const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
+    const options = {
+      iceServers,
+      userName: Auth.fullname,
+      stream,
+      hasAudio: BridgeService.streamHasAudioTrack(stream),
+    };
+
+    this.broker = new ScreenshareBroker(
+      Auth.authenticateURL(SFU_URL),
+      BridgeService.getConferenceBridge(),
+      Auth.userID,
+      Auth.meetingID,
+      this.role,
+      options,
+    );
+
+    this.broker.onstart = this.handlePresenterStart.bind(this);
+    this.broker.onerror = onerror.bind(this);
+    this.broker.onstreamended = this.stop.bind(this);
+    return this.broker.share().finally(this.scheduleReconnect.bind(this));
+  };
+
+  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 e2565e434cbffcb6e33bb70e95bbddeec2b08b56..1183fd81047dc9fe273a22ee82d56ccfab4e8442 100644
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/service.js
@@ -1,10 +1,20 @@
 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';
 
 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'
   || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
 
@@ -12,8 +22,19 @@ const getConferenceBridge = () => Meetings.findOne().voiceProp.voiceConf;
 
 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({
+        errorMessage: 'NotSupportedError',
+        errorName: 'NotSupportedError',
+        errorCode: 9,
+      });
+    }
+
     if (typeof stream.getVideoTracks === 'function'
-        && typeof constraints.video === 'object') {
+      && typeof constraints.video === 'object') {
       stream.getVideoTracks().forEach(track => {
         if (typeof track.applyConstraints  === 'function') {
           track.applyConstraints(constraints.video).catch(error => {
@@ -21,14 +42,14 @@ const getScreenStream = async () => {
               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') {
+      && typeof constraints.audio === 'object') {
       stream.getAudioTracks().forEach(track => {
         if (typeof track.applyConstraints  === 'function') {
           track.applyConstraints(constraints.audio).catch(error => {
@@ -46,11 +67,9 @@ const getScreenStream = async () => {
 
   const constraints = hasDisplayMedia ? GDM_CONSTRAINTS : null;
 
-  // 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();
-  } else  {
+  if (hasDisplayMedia) {
+    // The double checks here is to detect whether gDM is in navigator or mediaDevices
+    // because it can be on either of them depending on the browser+version
     if (typeof navigator.getDisplayMedia === 'function') {
       return navigator.getDisplayMedia(constraints)
         .then(gDMCallback)
@@ -59,7 +78,7 @@ const getScreenStream = async () => {
             logCode: 'screenshare_getdisplaymedia_failed',
             extraInfo: { errorName: error.name, errorCode: error.code },
           }, 'getDisplayMedia call failed');
-          return Promise.resolve();
+          return Promise.reject({ errorCode: error.code, errorMessage: error.name || error.message });
         });
     } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
       return navigator.mediaDevices.getDisplayMedia(constraints)
@@ -69,15 +88,119 @@ const getScreenStream = async () => {
             logCode: 'screenshare_getdisplaymedia_failed',
             extraInfo: { errorName: error.name, errorCode: error.code },
           }, 'getDisplayMedia call failed');
-          return Promise.resolve();
+          return Promise.reject({ errorCode: error.code, errorMessage: error.name || error.message });
         });
     }
+  } else {
+    // getDisplayMedia isn't supported, error its way out
+    return Promise.reject({
+      errorMessage: 'NotSupportedError',
+      errorName: 'NotSupportedError',
+      errorCode: 9,
+    });
+  }
+}
+
+const normalizeError = (error = {}) => {
+  const errorMessage = error.errorMessage || error.name || error.message || error.reason || 'Unknown error';
+  const errorCode = error.errorCode || error.code || undefined;
+  const errorReason = error.reason || error.id || 'Undefined reason';
+
+  return { errorMessage, errorCode, errorReason };
+}
+
+const handlePresenterFailure = (error, started = false) => {
+  const normalizedError = 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}`);
+  }
+  return normalizedError;
+}
+
+const handleViewerFailure = (error, started = false) => {
+  const normalizedError = 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}`);
   }
+  return normalizedError;
+}
+
+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 {
+      const normalizedError = {
+        errorCode: 1104,
+        errorMessage: error.message || 'SCREENSHARE_PLAY_FAILED',
+      };
+      throw normalizedError;
+    }
+  });
+}
 
 export default {
   hasDisplayMedia,
   getConferenceBridge,
   getScreenStream,
+  normalizeError,
+  handlePresenterFailure,
+  handleViewerFailure,
+  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 2708acbea7e346a8746a9c314d3a1244cd2e41f7..f287a25085eafa0c1be800de565dbc67eb438f8f 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/component';
+import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
 import QuickPollDropdown from './quick-poll-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
 import JoinVideoOptionsContainer from '../video-provider/video-button/container';
@@ -13,11 +13,7 @@ class ActionsBar extends PureComponent {
   render() {
     const {
       amIPresenter,
-      handleShareScreen,
-      handleUnshareScreen,
-      isVideoBroadcasting,
       amIModerator,
-      screenSharingCheck,
       enableVideo,
       isLayoutSwapped,
       toggleSwapLayout,
@@ -26,9 +22,7 @@ class ActionsBar extends PureComponent {
       currentSlidHasContent,
       parseCurrentSlideContent,
       isSharingVideo,
-      screenShareEndAlert,
       stopExternalVideoShare,
-      screenshareDataSavingSetting,
       isCaptionsAvailable,
       isMeteorConnected,
       isPollingEnabled,
@@ -83,15 +77,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 f75cf3d683bbec1cff4d92ddc7d4fc99f8ed7c89..70788aa6c1ed5780e9796978c780b9ed6d6843d4 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,
@@ -30,10 +26,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,
@@ -41,8 +33,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/screenshare/component.jsx
similarity index 82%
rename from bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
rename to bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx
index 09b26906588339d577d71b950335ed10e403a4a4..c5ef076ecb585a9db27eb6f00fbbfe58c44164a0 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx
@@ -10,15 +10,20 @@ 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';
+
 
 const propTypes = {
   intl: intlShape.isRequired,
+  enabled: PropTypes.bool.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,
 };
@@ -72,6 +77,10 @@ const intlMessages = defineMessages({
     id: 'app.deskshare.iceConnectionStateError',
     description: 'Error message for ice connection state failure',
   },
+  1120: {
+    id: 'app.deskshare.mediaFlowTimeout',
+    description: 'Error message for screenshare media flow timeout',
+  },
   2000: {
     id: 'app.sfu.mediaServerConnectionError2000',
     description: 'Error message fired when the SFU cannot connect to the media server',
@@ -117,14 +126,12 @@ const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false)
     : false);
 const IS_SAFARI = BROWSER_RESULTS.name === 'safari';
 
-const DesktopShare = ({
+
+const ScreenshareButton = ({
   intl,
-  handleShareScreen,
-  handleUnshareScreen,
+  enabled,
   isVideoBroadcasting,
   amIPresenter,
-  screenSharingCheck,
-  screenShareEndAlert,
   isMeteorConnected,
   screenshareDataSavingSetting,
   mountModal,
@@ -136,7 +143,7 @@ const DesktopShare = ({
     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();
+      screenshareHasEnded();
       notify(intl.formatMessage(intlMessages[error]), 'error', 'desktop');
     } else {
       // Unmapped error. Log it (so we can infer what's going on), close screenSharing
@@ -147,7 +154,7 @@ const DesktopShare = ({
           errorCode, errorMessage, errorReason,
         },
       }, 'Default error handler for screenshare');
-      window.kurentoExitScreenShare();
+      screenshareHasEnded();
       notify(intl.formatMessage(intlMessages.genericError), 'error', 'desktop');
     }
     // Don't trigger the screen share end alert if presenter click to cancel on screen share dialog
@@ -156,6 +163,23 @@ const DesktopShare = ({
     }
   };
 
+  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;
 
@@ -165,7 +189,7 @@ const DesktopShare = ({
   const vDescr = isVideoBroadcasting
     ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
 
-  const shouldAllowScreensharing = screenSharingCheck
+  const shouldAllowScreensharing = enabled
     && !isMobileBrowser
     && amIPresenter;
 
@@ -182,28 +206,20 @@ const DesktopShare = ({
         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>);
+        onClick={isVideoBroadcasting
+          ? screenshareHasEnded
+          : () => {
+            if (IS_SAFARI && !ScreenshareBridgeService.hasDisplayMedia) {
+              renderScreenshareUnavailableModal();
+            } else {
+              shareScreen(onFail);
+            }
           }
-          handleShareScreen(onFail);
-        }
         }
         id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
       />
     ) : null;
 };
 
-DesktopShare.propTypes = propTypes;
-export default withModalMounter(injectIntl(memo(DesktopShare)));
+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/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
index 26abe0eae01b0afcf3cefa48f0ddd7d749cfd64a..cdf759790f3c530d569b1b897d2860367a72cd19 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
@@ -7,7 +7,20 @@ 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 {
+  SCREENSHARE_MEDIA_ELEMENT_NAME,
+  screenshareHasEnded,
+  screenshareHasStarted,
+  getMediaElement,
+  attachLocalPreviewStream,
+} from '/imports/ui/components/screenshare/service';
+import {
+  isStreamStateHealthy,
+  subscribeToStreamStateChange,
+  unsubscribeFromStreamStateChange,
+} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
 
 const intlMessages = defineMessages({
   screenShareLabel: {
@@ -31,49 +44,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());
   }
 
   componentWillReceiveProps(nextProps) {
     const {
-      isPresenter, unshareScreen,
+      isPresenter,
     } = this.props;
     if (isPresenter && !nextProps.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 = isStreamStateHealthy(streamState);
+    event.stopPropagation();
+    if (newHealthState !== isStreamHealthy) {
+      this.setState({ isStreamHealthy: newHealthState });
+    }
   }
 
-  onVideoLoad() {
+  onLoadedData() {
     this.setState({ loaded: true });
   }
 
@@ -143,12 +170,26 @@ class ScreenshareComponent extends React.Component {
     );
   }
 
+  renderAutoplayOverlay() {
+    const { intl } = this.props;
+
+    return (
+      <AutoplayOverlay
+        key={_.uniqueId('screenshareAutoplayOverlay')}
+        autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
+        autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
+        handleAllowAutoplay={this.handleAllowAutoplay}
+      />
+    );
+  }
+
   render() {
-    const { loaded, autoplayBlocked } = this.state;
+    const { loaded, autoplayBlocked, isStreamHealthy } = this.state;
     const { intl } = this.props;
+    const shouldRenderReconnect = !isStreamHealthy && loaded;
 
     return (
-      [!loaded
+      [(!loaded || shouldRenderReconnect)
         ? (
           <div
             key={_.uniqueId('screenshareArea-')}
@@ -158,28 +199,26 @@ 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; }}
         >
+
           {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]: !isStreamHealthy,
+            })}
             muted
           />
         </div>
@@ -193,7 +232,4 @@ export default injectIntl(ScreenshareComponent);
 ScreenshareComponent.propTypes = {
   intl: intlShape.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..60502e3981812d593fec1570ce892287b6aceafb 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx
@@ -4,14 +4,12 @@ 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,
 } from './service';
 import ScreenshareComponent from './component';
 
 const ScreenshareContainer = (props) => {
-  const { isVideoBroadcasting: isVB } = props;
-  if (isVB()) {
+  if (isVideoBroadcasting()) {
     return <ScreenshareComponent {...props} />;
   }
   return null;
@@ -21,10 +19,6 @@ export default withTracker(() => {
   const user = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } });
   return {
     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 6ec077badc455928ef77b55077c0b279b49828e2..c22da4fdeb302eefbe298582686b68b10a3fa002 100644
--- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
@@ -10,35 +10,42 @@ import Auth from '/imports/ui/services/auth';
 import UserListService from '/imports/ui/components/user-list/service';
 import AudioService from '/imports/ui/components/audio/service';
 
+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();
+  }
+};
+
 // 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 (!screenshareEntry) {
-    return false;
+  if (screenIsShared && isSharingScreen) {
+    setSharingScreen(false);
   }
 
-  return !!screenshareEntry.screenshare.stream;
+  return sharing || screenIsShared;
 };
 
-// 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 viewScreenshare = (hasAudio) => {
-  const amIPresenter = UserListService.isUserPresenter(Auth.userID);
-  if (!amIPresenter) {
-    KurentoBridge.kurentoViewScreen(hasAudio);
-  } else {
-    KurentoBridge.kurentoViewLocalPreview();
-  }
-};
 
 const screenshareHasAudio = () => {
   const screenshareEntry = Screenshare.findOne({ meetingId: Auth.meetingID },
@@ -51,37 +58,66 @@ const screenshareHasAudio = () => {
   return !!screenshareEntry.screenshare.hasAudio;
 }
 
-// if remote screenshare has been started connect and display the video stream
-const presenterScreenshareHasStarted = () => {
-  const hasAudio = screenshareHasAudio();
+const screenshareHasEnded = () => {
+  const amIPresenter = UserListService.isUserPresenter(Auth.userID);
 
-  // WebRTC restrictions may need a capture device permission to release
-  // useful ICE candidates on recvonly/no-gUM peers
-  tryGenerateIceCandidates().then(() => {
-    viewScreenshare(hasAudio);
-  }).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.
-    viewScreenshare(hasAudio);
-  });
+  if (amIPresenter) {
+    setSharingScreen(false);
+  }
+
+  KurentoBridge.stop();
+  screenShareEndAlert();
 };
 
-const shareScreen = (onFail) => {
+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);
+  }
+}
+
+const screenshareHasStarted = () => {
+  const amIPresenter = UserListService.isUserPresenter(Auth.userID);
+
+  // Presenter's screen preview is local, so skip
+  if (!amIPresenter) {
+    viewScreenshare();
+  }
+};
+
+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
@@ -89,19 +125,18 @@ const screenShareEndAlert = () => AudioService
     + Meteor.settings.public.app.basename}`
     + '/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,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
index aff8c3d1932b53bfe58544b3a3dc44a37bb7942b..a9ca8486628d78b4e1edd4a3f26c6eab13d80db4 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/styles.scss
@@ -3,10 +3,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{
@@ -18,3 +17,7 @@
   width: 100%;
   height: 100%;
 }
+
+.unhealthyStream {
+  filter: grayscale(50%) opacity(50%);
+}
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 62c69a3f6aa3fae15ab8fa27f9339c75ad666bfc..fa5ac6653775838e751ef045851f38fb6155ce3e 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -110,6 +110,14 @@ public:
     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:
@@ -120,10 +128,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
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 7bac8d4e733f71f1b8f49cde347c5f10e2a32e1d..b460ccea07c8c782c211c4c8a9186165cc20570b 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -593,6 +593,7 @@
     "app.video.clientDisconnected": "Webcam cannot be shared due to connection issues",
     "app.fullscreenButton.label": "Make {0} fullscreen",
     "app.deskshare.iceConnectionStateError": "Connection failed when sharing screen (ICE error 1108)",
+    "app.deskshare.mediaFlowTimeout": "Media could not reach the server (error 1120)",
     "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)",
diff --git a/bigbluebutton-html5/public/compatibility/kurento-extension.js b/bigbluebutton-html5/public/compatibility/kurento-extension.js
deleted file mode 100755
index d5e0f51da465651fc41f72fcef3dfe6714f900be..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/public/compatibility/kurento-extension.js
+++ /dev/null
@@ -1,924 +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, hasAudio
-
-  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,
-    hasAudio: !!this.hasAudio,
-  };
-
-  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: !!self.hasAudio,
-      },
-      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();
-};