diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java
index 59521552c3041b30ef9e527d1635b3d614919fd8..da93ef6b915b503a3b6bf9954e1b2c1c6194dd4e 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/User.java
@@ -151,7 +151,9 @@ public class User {
 	}
 	
 	public void addStream(String stream) {
-		streams.add(stream);
+		if (!streams.contains(stream)) {
+			streams.add(stream);
+		}
 	}
 	
 	public void removeStream(String stream) {
diff --git a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js
index ae16937c170634002d6785ac8564d538dbb34433..a8f37165c68dcbe99d0bd92c6aaca3ba6c686585 100755
--- a/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js
+++ b/bigbluebutton-html5/imports/api/annotations/server/handlers/whiteboardSend.js
@@ -5,12 +5,14 @@ import Metrics from '/imports/startup/server/metrics';
 
 const { queueMetrics } = Meteor.settings.private.redis.metrics;
 
-const ANNOTATION_PROCCESS_INTERVAL = 60;
+const {
+  annotationsQueueProcessInterval: ANNOTATION_PROCESS_INTERVAL,
+} = Meteor.settings.public.whiteboard;
 
 let annotationsQueue = {};
 let annotationsRecieverIsRunning = false;
 
-const proccess = () => {
+const process = () => {
   if (!Object.keys(annotationsQueue).length) {
     annotationsRecieverIsRunning = false;
     return;
@@ -24,7 +26,7 @@ const proccess = () => {
   });
   annotationsQueue = {};
 
-  Meteor.setTimeout(proccess, ANNOTATION_PROCCESS_INTERVAL);
+  Meteor.setTimeout(process, ANNOTATION_PROCESS_INTERVAL);
 };
 
 export default function handleWhiteboardSend({ header, body }, meetingId) {
@@ -45,7 +47,7 @@ export default function handleWhiteboardSend({ header, body }, meetingId) {
   if (queueMetrics) {
     Metrics.setAnnotationQueueLength(meetingId, annotationsQueue[meetingId].length);
   }
-  if (!annotationsRecieverIsRunning) proccess();
+  if (!annotationsRecieverIsRunning) process();
 
   return addAnnotation(meetingId, whiteboardId, userId, annotation);
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 7b81fd76066848806a326788e3ea96646e902a3b..3b223e395aff4c47bb95bd0a73eb32d1ce130e14 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -17,6 +17,7 @@ import { Tracker } from 'meteor/tracker';
 import VoiceCallStates from '/imports/api/voice-call-states';
 import CallStateOptions from '/imports/api/voice-call-states/utils/callStates';
 import Auth from '/imports/ui/services/auth';
+import Settings from '/imports/ui/services/settings';
 
 const MEDIA = Meteor.settings.public.media;
 const MEDIA_TAG = MEDIA.mediaTag;
@@ -36,6 +37,8 @@ const BRIDGE_NAME = 'sip';
 const WEBSOCKET_KEEP_ALIVE_INTERVAL = MEDIA.websocketKeepAliveInterval || 0;
 const WEBSOCKET_KEEP_ALIVE_DEBOUNCE = MEDIA.websocketKeepAliveDebounce || 10;
 const TRACE_SIP = MEDIA.traceSip || false;
+const AUDIO_MICROPHONE_CONSTRAINTS = Meteor.settings.public.app.defaultSettings
+  .application.microphoneConstraints;
 
 const getAudioSessionNumber = () => {
   let currItem = parseInt(sessionStorage.getItem(AUDIO_SESSION_NUM_KEY), 10);
@@ -578,16 +581,24 @@ class SIPSession {
 
       const target = SIP.UserAgent.makeURI(`sip:${callExtension}@${hostname}`);
 
-      const audioDeviceConstraint = this.inputDeviceId
-        ? { deviceId: { exact: this.inputDeviceId } }
-        : true;
+      const userSettingsConstraints = Settings.application.microphoneConstraints;
+      const audioDeviceConstraints = userSettingsConstraints
+        || AUDIO_MICROPHONE_CONSTRAINTS || {};
+
+      const matchConstraints = this.filterSupportedConstraints(
+        audioDeviceConstraints,
+      );
+
+      if (this.inputDeviceId) {
+        matchConstraints.deviceId = { exact: this.inputDeviceId };
+      }
 
       const inviterOptions = {
         sessionDescriptionHandlerOptions: {
           constraints: {
             audio: isListenOnly
               ? false
-              : audioDeviceConstraint,
+              : matchConstraints,
             video: false,
           },
           iceGatheringTimeout: ICE_GATHERING_TIMEOUT,
@@ -932,6 +943,88 @@ class SIPSession {
       resolve();
     });
   }
+
+  /**
+   * Filter constraints set in audioDeviceConstraints, based on
+   * constants supported by browser. This avoids setting a constraint
+   * unsupported by browser. In currently safari version (13+), for example,
+   * setting an unsupported constraint crashes the audio.
+   * @param  {Object} audioDeviceConstraints Constraints to be set
+   * see: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
+   * @return {Object}                        A new Object of the same type as
+   * input, containing only the supported constraints.
+   */
+  filterSupportedConstraints(audioDeviceConstraints) {
+    try {
+      const matchConstraints = {};
+      const supportedConstraints = navigator
+        .mediaDevices.getSupportedConstraints() || {};
+      Object.entries(audioDeviceConstraints).forEach(
+        ([constraintName, constraintValue]) => {
+          if (supportedConstraints[constraintName]) {
+            matchConstraints[constraintName] = constraintValue;
+          }
+        }
+      );
+
+      return matchConstraints;
+    } catch (error) {
+      logger.error({
+        logCode: 'sipjs_unsupported_audio_constraint_error',
+        extraInfo: {
+          callerIdName: this.user.callerIdName,
+        },
+      }, 'SIP.js unsupported constraint error');
+      return {};
+    }
+  }
+
+  /**
+   * Update audio constraints for current local MediaStream (microphone)
+   * @param  {Object}  constraints MediaTrackConstraints object. See:
+   * https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
+   * @return {Promise}             A Promise for this process
+   */
+  async updateAudioConstraints(constraints) {
+    try {
+      if (typeof constraints !== 'object') return;
+
+      logger.info({
+        logCode: 'sipjs_update_audio_constraint',
+        extraInfo: {
+          callerIdName: this.user.callerIdName,
+        },
+      }, 'SIP.js updating audio constraint');
+
+      const matchConstraints = this.filterSupportedConstraints(constraints);
+
+      //Chromium bug - see: https://bugs.chromium.org/p/chromium/issues/detail?id=796964&q=applyConstraints&can=2
+      if (browser().name === 'chrome') {
+        matchConstraints.deviceId = this.inputDeviceId;
+
+        const stream = await navigator.mediaDevices.getUserMedia(
+          { audio: matchConstraints },
+        );
+
+        this.currentSession.sessionDescriptionHandler
+          .setLocalMediaStream(stream);
+      } else {
+        const { localMediaStream } = this.currentSession
+          .sessionDescriptionHandler;
+
+        localMediaStream.getAudioTracks().forEach(
+          track => track.applyConstraints(matchConstraints),
+        );
+      }
+    } catch (error) {
+      logger.error({
+        logCode: 'sipjs_audio_constraint_error',
+        extraInfo: {
+          callerIdName: this.user.callerIdName,
+        },
+      }, 'SIP.js failed to update audio constraint');
+    }
+  }
 }
 
 export default class SIPBridge extends BaseAudioBridge {
@@ -1082,4 +1175,8 @@ export default class SIPBridge extends BaseAudioBridge {
 
     return this.media.outputDeviceId || value;
   }
+
+  async updateAudioConstraints(constraints) {
+    return this.activeSession.updateAudioConstraints(constraints);
+  }
 }
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js
index 494fb95677c42a168b47e09bc532ee0266e2adcf..86dd9d60681499ea52ad77e07f3f8cf44605798c 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/startTyping.js
@@ -15,7 +15,7 @@ export default function startTyping(meetingId, userId, chatId) {
     userId,
   };
 
-  const user = Users.findOne(selector);
+  const user = Users.findOne(selector, { fields: { name: 1, role: 1 } });
 
   const modifier = {
     meetingId,
diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js
index 42dd7e2e7fed6fc970ed7a8526ceadad47ff251b..c9637bdd74b5f26601900803a3ce80c470e0e3e4 100644
--- a/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js
+++ b/bigbluebutton-html5/imports/api/users/server/handlers/validateAuthToken.js
@@ -4,6 +4,7 @@ import Users from '/imports/api/users';
 import userJoin from './userJoin';
 import pendingAuthenticationsStore from '../store/pendingAuthentications';
 import createDummyUser from '../modifiers/createDummyUser';
+import ClientConnections from '/imports/startup/server/ClientConnections';
 
 import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
 import { ValidationStates } from '/imports/api/auth-token-validation';
@@ -75,6 +76,7 @@ export default function handleValidateAuthToken({ body }, meetingId) {
           createDummyUser(meetingId, userId, authToken);
         }
 
+        ClientConnections.add(sessionId, methodInvocationObject.connection);
         upsertValidationState(meetingId, userId, ValidationStates.VALIDATED, methodInvocationObject.connection.id);
 
         /* End of logic migrated from validateAuthToken */
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js
index a075d36662f02270aad7bb58b6538929d0c8ac27..36bcd600aba0d1acd160888b0248163c7696af06 100755
--- a/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeaving.js
@@ -4,6 +4,7 @@ import RedisPubSub from '/imports/startup/server/redis';
 import Logger from '/imports/startup/server/logger';
 import AuthTokenValidation from '/imports/api/auth-token-validation';
 import Users from '/imports/api/users';
+import ClientConnections from '/imports/startup/server/ClientConnections';
 
 export default function userLeaving(meetingId, userId, connectionId) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
@@ -40,6 +41,8 @@ export default function userLeaving(meetingId, userId, connectionId) {
     sessionId: meetingId,
   };
 
+  ClientConnections.removeClientConnection(`${meetingId}--${userId}`, connectionId);
+
   Logger.info(`User '${userId}' is leaving meeting '${meetingId}'`);
   return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
 }
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js
index 38e31316ac795c70f46fd8f980c59e1f15b90504..c2ed519acceeee65e83f8ada074071f47f7669a8 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/userLeftMeeting.js
@@ -17,18 +17,8 @@ export default function userLeftMeeting() { // TODO-- spread the code to method/
 
     if (numberAffected) {
       Logger.info(`user left id=${requesterUserId} meeting=${meetingId}`);
+      ClientConnections.removeClientConnection(this.userId, this.connection.id);
     }
-    ClientConnections.removeClientConnection(this.userId, this.connection.id);
-
-    Users.update(
-      selector,
-      {
-        $set: {
-          loggedOut: true,
-        },
-      },
-      cb,
-    );
   } catch (err) {
     Logger.error(`leaving dummy user to collection: ${err}`);
   }
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js
index a53690907ab9900ff4d74413e55e16b9487ccc1a..e3ca329a3a6f3ee10c9a5d705f8603758fc09b7f 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/validateAuthToken.js
@@ -1,18 +1,18 @@
 import { Meteor } from 'meteor/meteor';
 import RedisPubSub from '/imports/startup/server/redis';
 import Logger from '/imports/startup/server/logger';
-import ClientConnections from '/imports/startup/server/ClientConnections';
 import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
 import { ValidationStates } from '/imports/api/auth-token-validation';
 import pendingAuthenticationsStore from '../store/pendingAuthentications';
 import BannedUsers from '../store/bannedUsers';
-import Users from '/imports/api/users';
 
 export default function validateAuthToken(meetingId, requesterUserId, requesterToken, externalId) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
   const EVENT_NAME = 'ValidateAuthTokenReqMsg';
 
+  Logger.debug('ValidateAuthToken method called', { meetingId, requesterUserId, requesterToken, externalId });
+
   // Check if externalId is banned from the meeting
   if (externalId) {
     if (BannedUsers.has(meetingId, externalId)) {
@@ -21,24 +21,7 @@ export default function validateAuthToken(meetingId, requesterUserId, requesterT
     }
   }
 
-  // Prevent users who have left or been ejected to use the same sessionToken again.
-  const isUserInvalid = Users.findOne({
-    meetingId,
-    userId: requesterUserId,
-    authToken: requesterToken,
-    $or: [{ ejected: true }, { loggedOut: true }],
-  });
-
-  if (isUserInvalid) {
-    Logger.warn(`An invalid sessionToken tried to validateAuthToken meetingId=${meetingId} authToken=${requesterToken}`);
-    return {
-      invalid: true,
-      reason: `User has an invalid sessionToken due to ${isUserInvalid.ejected ? 'ejection' : 'log out'}`,
-      error_type: `invalid_session_token_due_to_${isUserInvalid.ejected ? 'eject' : 'log_out'}`,
-    };
-  }
-
-  ClientConnections.add(`${meetingId}--${requesterUserId}`, this.connection);
+  if (!meetingId) return false;
 
   // Store reference of methodInvocationObject ( to postpone the connection userId definition )
   pendingAuthenticationsStore.add(meetingId, requesterUserId, requesterToken, this);
diff --git a/bigbluebutton-html5/imports/api/users/server/publishers.js b/bigbluebutton-html5/imports/api/users/server/publishers.js
index 55e4acbb89e4780c4d085d6e6e425be279019a26..3cdba628ad98a2acb5b51e1a211f3e2e946e851b 100644
--- a/bigbluebutton-html5/imports/api/users/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/users/server/publishers.js
@@ -18,6 +18,7 @@ function currentUser() {
   const selector = {
     meetingId,
     userId: requesterUserId,
+    intId: { $exists: true }
   };
 
   const options = {
@@ -56,6 +57,7 @@ function users(role) {
     $or: [
       { meetingId },
     ],
+    intId: { $exists: true }
   };
 
   const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });
diff --git a/bigbluebutton-html5/imports/startup/server/ClientConnections.js b/bigbluebutton-html5/imports/startup/server/ClientConnections.js
index 5e4ca6bdfabc837f2796b1eb00bbc260ac3dbd7a..9bb837f4ae1900d86e027bd407397cbcfbd57311 100644
--- a/bigbluebutton-html5/imports/startup/server/ClientConnections.js
+++ b/bigbluebutton-html5/imports/startup/server/ClientConnections.js
@@ -1,6 +1,10 @@
 import Logger from './logger';
 import userLeaving from '/imports/api/users/server/methods/userLeaving';
 import { extractCredentials } from '/imports/api/common/server/helpers';
+import AuthTokenValidation from '/imports/api/auth-token-validation';
+import Users from '/imports/api/users';
+
+const { enabled, syncInterval } = Meteor.settings.public.syncUsersWithConnectionManager;
 
 class ClientConnections {
   constructor() {
@@ -11,10 +15,15 @@ class ClientConnections {
       this.print();
     }, 30000);
 
-    // setTimeout(() => {
-    //   this.syncConnectionsWithServer();
-    // }, 10000);
+    if (enabled) {
+      const syncConnections = Meteor.bindEnvironment(() => {
+        this.syncConnectionsWithServer();
+      });
 
+      setInterval(() => {
+        syncConnections();
+      }, syncInterval);
+    }
   }
 
   add(sessionId, connection) {
@@ -29,6 +38,13 @@ class ClientConnections {
 
     const { meetingId, requesterUserId: userId } = extractCredentials(sessionId);
 
+    if (!meetingId) {
+      Logger.error('Error on add new client connection. sessionId=${sessionId} connection=${connection.id}',
+        { logCode: 'client_connections_add_error_meeting_id_null', extraInfo: { meetingId, userId } }
+      );
+      return false;
+    }
+
     if (!this.exists(meetingId)) {
       Logger.info(`Meeting not found in connections: meetingId=${meetingId}`);
       this.createMeetingConnections(meetingId);
@@ -92,7 +108,7 @@ class ClientConnections {
     Logger.info(`Removing connectionId for user. sessionId=${sessionId} connectionId=${connectionId}`);
     const { meetingId, requesterUserId: userId } = extractCredentials(sessionId);
 
-    const meetingConnections = this.connections.get(meetingId)
+    const meetingConnections = this.connections.get(meetingId);
 
     if (meetingConnections?.has(userId)) {
       const filteredConnections = meetingConnections.get(userId).filter(c => c !== connectionId);
@@ -109,7 +125,38 @@ class ClientConnections {
   }
 
   syncConnectionsWithServer() {
-    console.error('syncConnectionsWithServer', Array.from(Meteor.server.sessions.keys()), Meteor.server);
+    Logger.info('Syncing ClientConnections with server');
+    const activeConnections = Array.from(Meteor.server.sessions.keys());
+
+    Logger.debug(`Found ${activeConnections.length} active connections in server`);
+
+    const onlineUsers = AuthTokenValidation
+      .find(
+        { connectionId: { $in: activeConnections } },
+        { fields: { meetingId: 1, userId: 1 } }
+      )
+      .fetch();
+
+    const onlineUsersId = onlineUsers.map(({ userId }) => userId);
+
+    const usersQuery = { userId: { $nin: onlineUsersId } };
+
+    const userWithoutConnectionIds = Users.find(usersQuery, { fields: { meetingId: 1, userId: 1 } }).fetch();
+
+    const removedUsersWithoutConnection = Users.remove(usersQuery);
+
+    if (removedUsersWithoutConnection) {
+      Logger.info(`Removed ${removedUsersWithoutConnection} users that are not connected`);
+      Logger.info(`Clearing connections`);
+      try {
+        userWithoutConnectionIds
+          .forEach(({ meetingId, userId }) => {
+            this.removeClientConnection(`${meetingId}--${userId}`);
+          });
+      } catch (err) {
+        Logger.error('Error on sync ClientConnections', err);
+      }
+    }
   }
 
 }
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx
index 6b1aba372275309aa7e4ee43b40067e67b6bf5dc..4680459f112995d253b1e3a2242b53b697981515 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx
@@ -107,7 +107,7 @@ class QuickPollDropdown extends Component {
     const parsedSlide = parseCurrentSlideContent(
       intl.formatMessage(intlMessages.yesOptionLabel),
       intl.formatMessage(intlMessages.noOptionLabel),
-      intl.formatMessage(intlMessages.abstentionOptionLabel)
+      intl.formatMessage(intlMessages.abstentionOptionLabel),
       intl.formatMessage(intlMessages.trueOptionLabel),
       intl.formatMessage(intlMessages.falseOptionLabel),
     );
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index 864af9d23d3f866faf44a4d77e3cd973d4d94459..26e295ae635171a5854f81c8b675324877f4cc81 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -11,6 +11,7 @@ import VideoPreviewContainer from '/imports/ui/components/video-preview/containe
 import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
 import Service from './service';
 import AudioModalContainer from './audio-modal/container';
+import Settings from '/imports/ui/services/settings';
 
 const APP_CONFIG = Meteor.settings.public.app;
 const KURENTO_CONFIG = Meteor.settings.public.kurento;
@@ -105,6 +106,7 @@ const messages = {
 };
 
 export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ mountModal, intl, userLocks }) => {
+  const { microphoneConstraints } = Settings.application;
   const autoJoin = getFromUserSettings('bbb_auto_join_audio', APP_CONFIG.autoJoin);
   const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
   const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam);
@@ -117,12 +119,14 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
     if (userWebcam) return resolve();
     mountModal(<VideoPreviewContainer resolve={resolve} />);
   });
-  if (userMic
-    && Service.isConnected()
-    && !Service.isListenOnly()
-    && !Service.isMuted()) {
-    Service.toggleMuteMicrophone();
-    notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on');
+
+  if (Service.isConnected() && !Service.isListenOnly()) {
+    Service.updateAudioConstraints(microphoneConstraints);
+
+    if (userMic && !Service.isMuted()) {
+      Service.toggleMuteMicrophone();
+      notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'audio_on');
+    }
   }
 
   Breakouts.find().observeChanges({
diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js
index b0ae6e1002c5179032034ea4aa697777766d62c3..3ae8226bd2dc441eff8ca0b3f2f60890803eabad 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/service.js
@@ -1,6 +1,6 @@
 import Users from '/imports/api/users';
 import Auth from '/imports/ui/services/auth';
-import { debounce } from 'lodash';
+import { debounce, throttle } from 'lodash';
 import AudioManager from '/imports/ui/services/audio-manager';
 import Meetings from '/imports/api/meetings';
 import { makeCall } from '/imports/ui/services/api';
@@ -8,6 +8,7 @@ import VoiceUsers from '/imports/api/voice-users';
 import logger from '/imports/startup/client/logger';
 
 const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
 
 const init = (messages, intl) => {
   AudioManager.setAudioMessages(messages, intl);
@@ -40,11 +41,11 @@ const isVoiceUser = () => {
     { fields: { joined: 1 } });
   return voiceUser ? voiceUser.joined : false;
 };
-const toggleMuteMicrophone = () => {
+
+const toggleMuteMicrophone = throttle(() => {
   const user = VoiceUsers.findOne({
     meetingId: Auth.meetingID, intId: Auth.userID,
   }, { fields: { muted: 1 } });
-
   if (user.muted) {
     logger.info({
       logCode: 'audiomanager_unmute_audio',
@@ -58,7 +59,7 @@ const toggleMuteMicrophone = () => {
     }, 'microphone muted by user');
     makeCall('toggleVoice');
   }
-};
+}, TOGGLE_MUTE_THROTTLE_TIME);
 
 
 export default {
@@ -89,4 +90,6 @@ export default {
   autoplayBlocked: () => AudioManager.autoplayBlocked,
   handleAllowAutoplay: () => AudioManager.handleAllowAutoplay(),
   playAlertSound: url => AudioManager.playAlertSound(url),
+  updateAudioConstraints:
+    constraints => AudioManager.updateAudioConstraints(constraints),
 };
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx
index 7628a11661a4fdc409f57a7f5057a009d9a18d39..8ab1028c8a5993f9f6508b7ccea97c745f094420 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/container.jsx
@@ -1,10 +1,12 @@
 import React, { PureComponent } from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
+import _ from 'lodash';
 import { makeCall } from '/imports/ui/services/api';
 import ChatForm from './component';
 import ChatService from '../service';
 
 const CHAT_CONFIG = Meteor.settings.public.chat;
+const START_TYPING_THROTTLE_INTERVAL = 2000;
 
 class ChatContainer extends PureComponent {
   render() {
@@ -25,7 +27,7 @@ export default withTracker(() => {
   const stopUserTyping = () => makeCall('stopUserTyping');
 
   return {
-    startUserTyping,
+    startUserTyping: _.throttle(startUserTyping, START_TYPING_THROTTLE_INTERVAL),
     stopUserTyping,
     UnsentMessagesCollection: ChatService.UnsentMessagesCollection,
     minMessageLength: CHAT_CONFIG.min_message_length,
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
index d9cc1f2ab72c88985e12cb2e9e82bafc0a485d5e..3c7360106a2b698d50e322b7654d3a1f6afed21a 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-form/styles.scss
@@ -139,7 +139,7 @@
 
   > span {
     display: block;
-    width: 100%;
+    margin-right: 0.05rem;
     line-height: var(--font-size-md);
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx
index ec0309527bd78a1699321d88ec16964b53af810c..36fcd5d036735402e447d82793b06ca821b3b8b9 100644
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/talking-indicator/container.jsx
@@ -10,6 +10,7 @@ import Service from './service';
 
 const APP_CONFIG = Meteor.settings.public.app;
 const { enableTalkingIndicator } = APP_CONFIG;
+const TALKING_INDICATOR_MUTE_INTERVAL = 500;
 
 const TalkingIndicatorContainer = (props) => {
   if (!enableTalkingIndicator) return null;
@@ -47,7 +48,7 @@ export default withTracker(() => {
     }
   }
 
-  const muteUser = (id) => {
+  const muteUser = debounce((id) => {
     const user = VoiceUsers.findOne({ meetingId, voiceUserId: id }, {
       fields: {
         muted: 1,
@@ -55,11 +56,11 @@ export default withTracker(() => {
     });
     if (user.muted) return;
     makeCall('toggleVoice', id);
-  };
+  }, TALKING_INDICATOR_MUTE_INTERVAL, { leading: true, trailing: false });
 
   return {
     talkers,
-    muteUser: id => debounce(muteUser(id), 500, { leading: true, trailing: false }),
+    muteUser,
     openPanel: Session.get('openPanel'),
     isBreakoutRoom: meetingIsBreakout(),
   };
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js
index 08a173fff33e78518af7cd3224dc7299c37672cc..3f53a8a61731f575efee5a71812f3304853503be 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/service.js
+++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js
@@ -16,7 +16,7 @@ const downloadPresentationUri = (podId) => {
     return null;
   }
 
-  const presentationFileName =  currentPresentation.id + '.' + currentPresentation.name.split('.').pop();
+  const presentationFileName = `${currentPresentation.id}.${currentPresentation.name.split('.').pop()}`;
 
   const uri = `https://${window.document.location.hostname}/bigbluebutton/presentation/download/`
     + `${currentPresentation.meetingId}/${currentPresentation.id}`
diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
index f92a87a31c2772b77c855ba10098e15a47abcd20..f043f7c8b4cbc0403fd206ff9eeae64ceb1463f3 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
@@ -74,6 +74,7 @@ const propTypes = {
     fallbackLocale: PropTypes.string,
     fontSize: PropTypes.string,
     locale: PropTypes.string,
+    microphoneConstraints: PropTypes.objectOf(Object),
   }).isRequired,
   updateSettings: PropTypes.func.isRequired,
   availableLocales: PropTypes.objectOf(PropTypes.array).isRequired,
diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
index d91cacee4324f7b2d7d36c9a9cb2de8c7bf284e2..911a02c0fcb7e8762e5f3b95d479fafcd7e890fc 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
@@ -17,6 +17,10 @@ const intlMessages = defineMessages({
     id: 'app.submenu.application.animationsLabel',
     description: 'animations label',
   },
+  audioFilterLabel: {
+    id: 'app.submenu.application.audioFilterLabel',
+    description: 'audio filters label',
+  },
   fontSizeControlLabel: {
     id: 'app.submenu.application.fontSizeControlLabel',
     description: 'label for font size ontrol',
@@ -76,6 +80,8 @@ class ApplicationMenu extends BaseMenu {
         '18px',
         '20px',
       ],
+      audioFilterEnabled: ApplicationMenu.isAudioFilterEnabled(props
+        .settings.microphoneConstraints),
     };
   }
 
@@ -118,6 +124,49 @@ class ApplicationMenu extends BaseMenu {
     });
   }
 
+  static isAudioFilterEnabled(_constraints) {
+    if (typeof _constraints === 'undefined') return true;
+
+    const _isConstraintEnabled = (constraintValue) => {
+      switch (typeof constraintValue) {
+        case 'boolean':
+          return constraintValue;
+        case 'string':
+          return constraintValue === 'true';
+        case 'object':
+          return !!(constraintValue.exact || constraintValue.ideal);
+        default:
+          return false;
+      }
+    };
+
+    let isAnyFilterEnabled = true;
+
+    const constraints = _constraints && (typeof _constraints.advanced === 'object')
+      ? _constraints.advanced
+      : _constraints || {};
+
+    isAnyFilterEnabled = Object.values(constraints).find(
+      constraintValue => _isConstraintEnabled(constraintValue),
+    );
+
+    return isAnyFilterEnabled;
+  }
+
+  handleAudioFilterChange() {
+    const _audioFilterEnabled = !ApplicationMenu.isAudioFilterEnabled(this
+      .state.settings.microphoneConstraints);
+    const _newConstraints = {
+      autoGainControl: _audioFilterEnabled,
+      echoCancellation: _audioFilterEnabled,
+      noiseSuppression: _audioFilterEnabled,
+    };
+
+    const obj = this.state;
+    obj.settings.microphoneConstraints = _newConstraints;
+    this.handleUpdateSettings(this.state.settings, obj.settings);
+  }
+
   handleUpdateFontSize(size) {
     const obj = this.state;
     obj.settings.fontSize = size;
@@ -208,6 +257,25 @@ class ApplicationMenu extends BaseMenu {
             </div>
           </div>
 
+          <div className={styles.row}>
+            <div className={styles.col} aria-hidden="true">
+              <div className={styles.formElement}>
+                <label className={styles.label}>
+                  {intl.formatMessage(intlMessages.audioFilterLabel)}
+                </label>
+              </div>
+            </div>
+            <div className={styles.col}>
+              <div className={cx(styles.formElement, styles.pullContentRight)}>
+                <Toggle
+                  icons={false}
+                  defaultChecked={this.state.audioFilterEnabled}
+                  onChange={() => this.handleAudioFilterChange()}
+                  ariaLabel={intl.formatMessage(intlMessages.audioFilterLabel)}
+                />
+              </div>
+            </div>
+          </div>
           <div className={styles.row}>
             <div className={styles.col} aria-hidden="true">
               <div className={styles.formElement}>
@@ -249,6 +317,27 @@ class ApplicationMenu extends BaseMenu {
               </span>
             </div>
           </div>
+
+          <div className={styles.row}>
+            <div className={styles.col} aria-hidden="true">
+              <div className={styles.formElement}>
+                <label className={styles.label}>
+                  {intl.formatMessage(intlMessages.audioFilterLabel)}
+                </label>
+              </div>
+            </div>
+            <div className={styles.col}>
+              <div className={cx(styles.formElement, styles.pullContentRight)}>
+                <Toggle
+                  icons={false}
+                  defaultChecked={this.state.audioFilterEnabled}
+                  onChange={() => this.handleAudioFilterChange()}
+                  ariaLabel={intl.formatMessage(intlMessages.audioFilterLabel)}
+                />
+              </div>
+            </div>
+          </div>
+
           <hr className={styles.separator} />
           <div className={styles.row}>
             <div className={styles.col}>
@@ -303,3 +392,4 @@ class ApplicationMenu extends BaseMenu {
 }
 
 export default injectIntl(ApplicationMenu);
+
diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
index 18cfe74df2e996ee53a513298109540b3ea9cee4..67ea6939228b9339b9facfc99d5adf72c9ee9290 100755
--- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
@@ -7,12 +7,14 @@ import Button from '/imports/ui/components/button/component';
 // import { notify } from '/imports/ui/services/notification';
 import logger from '/imports/startup/client/logger';
 import Modal from '/imports/ui/components/modal/simple/component';
+import Service from './service';
 import browser from 'browser-detect';
 import VideoService from '../video-provider/service';
 import cx from 'classnames';
 import { styles } from './styles';
 
 const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
+const GUM_TIMEOUT = Meteor.settings.public.kurento.gUMTimeout;
 
 const VIEW_STATES = {
   finding: 'finding',
@@ -118,6 +120,22 @@ const intlMessages = defineMessages({
     id: 'app.video.permissionError',
     description: 'Error message for webcam permission',
   },
+  AbortError: {
+    id: 'app.video.abortError',
+    description: 'Some problem occurred which prevented the device from being used',
+  },
+  OverconstrainedError: {
+    id: 'app.video.overconstrainedError',
+    description: 'No candidate devices which met the criteria requested',
+  },
+  SecurityError: {
+    id: 'app.video.securityError',
+    description: 'Media support is disabled on the Document',
+  },
+  TypeError: {
+    id: 'app.video.typeError',
+    description: 'List of constraints specified is empty, or has all constraints set to false',
+  },
   NotFoundError: {
     id: 'app.video.notFoundError',
     description: 'error message when can not get webcam video',
@@ -134,6 +152,10 @@ const intlMessages = defineMessages({
     id: 'app.video.notReadableError',
     description: 'error message When the webcam is being used by other software',
   },
+  TimeoutError: {
+    id: 'app.video.timeoutError',
+    description: 'error message when promise did not return',
+  },
   iOSError: {
     id: 'app.audioModal.iOSBrowser',
     description: 'Audio/Video Not supported warning',
@@ -146,54 +168,13 @@ const intlMessages = defineMessages({
     id: 'app.audioModal.iOSErrorRecommendation',
     description: 'Audio/Video recommended action',
   },
+  genericError: {
+    id: 'app.video.genericError',
+    description: 'error message for when the webcam sharing fails with unknown error',
+  },
 });
 
 class VideoPreview extends Component {
-  static handleGUMError(error) {
-    // logger.error(error);
-    // logger.error(error.id);
-    // logger.error(error.name);
-    // console.log(error);
-    // console.log(error.name)
-    // console.log(error.message)
-
-    // let convertedError;
-
-    /* switch (error.name) {
-      case 'SourceUnavailableError':
-      case 'NotReadableError':
-        // hardware failure with the device
-        // NotReadableError: Could not start video source
-        break;
-      case 'NotAllowedError':
-        // media was disallowed
-        // NotAllowedError: Permission denied
-        convertedError = intlMessages.NotAllowedError;
-        break;
-      case 'AbortError':
-        // generic error occured
-        // AbortError: Starting video failed (FF when there's a hardware failure)
-        break;
-      case 'NotFoundError':
-        // no webcam found
-        // NotFoundError: The object can not be found here.
-        // NotFoundError: Requested device not found
-        convertedError = intlMessages.NotFoundError;
-        break;
-      case 'SecurityError':
-        // user media support is disabled on the document
-        break;
-      case 'TypeError':
-        // issue with constraints or maybe Chrome with HTTP
-        break;
-      default:
-        // default error message handling
-        break;
-    } */
-
-    return `${error.name}: ${error.message}`;
-  }
-
   constructor(props) {
     super(props);
 
@@ -240,12 +221,20 @@ class VideoPreview extends Component {
     // skipped then we get devices with no labels
     if (hasMediaDevices) {
       try {
-        navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'user' } })
+        let firstAllowedDeviceId;
+
+        const constraints = {
+          audio: false,
+          video: {
+            facingMode: 'user',
+          },
+        };
+
+        Service.promiseTimeout(GUM_TIMEOUT, navigator.mediaDevices.getUserMedia(constraints))
           .then((stream) => {
             if (!this._isMounted) return;
             this.deviceStream = stream;
             // try and get the deviceId for the initial stream
-            let firstAllowedDeviceId;
             if (stream.getVideoTracks) {
               const videoTracks = stream.getVideoTracks();
               if (videoTracks.length > 0 && videoTracks[0].getSettings) {
@@ -253,7 +242,9 @@ class VideoPreview extends Component {
                 firstAllowedDeviceId = trackSettings.deviceId;
               }
             }
-
+          }).catch((error) => {
+            this.handleDeviceError('initial_device', error, 'getting initial device');
+          }).finally(() => {
             navigator.mediaDevices.enumerateDevices().then((devices) => {
               const webcams = [];
               let initialDeviceId;
@@ -297,43 +288,11 @@ class VideoPreview extends Component {
                 });
               }
             }).catch((error) => {
-              logger.warn({
-                logCode: 'video_preview_enumerate_error',
-                extraInfo: {
-                  errorName: error.name,
-                  errorMessage: error.message,
-                },
-              }, 'Error enumerating devices');
-              this.setState({
-                viewState: VIEW_STATES.error,
-                deviceError: VideoPreview.handleGUMError(error),
-              });
-            });
-          }).catch((error) => {
-          logger.warn({
-            logCode: 'video_preview_initial_device_error',
-            extraInfo: {
-              errorName: error.name,
-              errorMessage: error.message,
-            },
-          }, 'Error getting initial device');
-          this.setState({
-            viewState: VIEW_STATES.error,
-            deviceError: VideoPreview.handleGUMError(error),
+              this.handleDeviceError('enumerate', error, 'enumerating devices');
           });
         });
       } catch (error) {
-        logger.warn({
-          logCode: 'video_preview_grabbing_error',
-          extraInfo: {
-            errorName: error.name,
-            errorMessage: error.message,
-          },
-        }, 'Error grabbing initial video stream');
-        this.setState({
-          viewState: VIEW_STATES.error,
-          deviceError: VideoPreview.handleGUMError(error),
-        });
+        this.handleDeviceError('grabbing', error, 'grabbing initial video stream');
       }
     } else {
       // TODO: Add an error message when media is globablly disabled
@@ -409,6 +368,51 @@ class VideoPreview extends Component {
     if (resolve) resolve();
   }
 
+  handlePreviewError(logCode, error, description) {
+    logger.warn({
+      logCode: `video_preview_${logCode}_error`,
+      extraInfo: {
+        errorName: error.name,
+        errorMessage: error.message,
+      },
+    }, `Error ${description}`);
+    this.setState({
+      previewError: this.handleGUMError(error),
+    });
+  }
+
+  handleDeviceError(logCode, error, description) {
+    logger.warn({
+      logCode: `video_preview_${logCode}_error`,
+      extraInfo: {
+        errorName: error.name,
+        errorMessage: error.message,
+      },
+    }, `Error ${description}`);
+    this.setState({
+      viewState: VIEW_STATES.error,
+      deviceError: this.handleGUMError(error),
+    });
+  }
+
+  handleGUMError(error) {
+    const { intl } = this.props;
+
+    logger.error({
+      logCode: 'video_preview_gum_failure',
+      extraInfo: {
+        errorName: error.name, errorMessage: error.message,
+      },
+    }, 'getUserMedia failed in video-preview');
+
+    if (intlMessages[error.name]) {
+      return intl.formatMessage(intlMessages[error.name]);
+    }
+
+    return intl.formatMessage(intlMessages.genericError,
+      { 0: `${error.name}: ${error.message}` });
+  }
+
   displayInitialPreview(deviceId) {
     const { changeWebcam } = this.props;
     const availableProfiles = CAMERA_PROFILES.filter(p => !p.hidden);
@@ -441,7 +445,7 @@ class VideoPreview extends Component {
     }
     this.deviceStream = null;
 
-    return navigator.mediaDevices.getUserMedia(constraints);
+    return Service.promiseTimeout(GUM_TIMEOUT, navigator.mediaDevices.getUserMedia(constraints));
   }
 
   displayPreview(deviceId, profile) {
@@ -467,14 +471,7 @@ class VideoPreview extends Component {
       this.video.srcObject = stream;
       this.deviceStream = stream;
     }).catch((error) => {
-      logger.warn({
-        logCode: 'video_preview_do_gum_preview_error',
-        extraInfo: {
-          errorName: error.name,
-          errorMessage: error.message,
-        },
-      }, 'Error displaying final selection.');
-      this.setState({ previewError: VideoPreview.handleGUMError(error) });
+      this.handlePreviewError('do_gum_preview', error, 'displaying final selection');
     });
   }
 
@@ -547,7 +544,7 @@ class VideoPreview extends Component {
                <label className={styles.label} htmlFor="setQuality">
                  {intl.formatMessage(intlMessages.qualityLabel)}
                </label>
-              { availableProfiles && availableProfiles.length > 0
+              {availableProfiles && availableProfiles.length > 0
                 ? (
                   <select
                     id="setQuality"
@@ -565,7 +562,8 @@ class VideoPreview extends Component {
                         <option key={profile.id} value={profile.id}>
                           {`${label}`}
                         </option>
-                      )})}
+                      )
+                    })}
                   </select>
                 )
                 : (
diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/service.js b/bigbluebutton-html5/imports/ui/components/video-preview/service.js
index 54125aa91f0093e20624545586f19370a723dc09..a5d92d6cb05918e8a56ef77bdb0d1fc6701d754e 100755
--- a/bigbluebutton-html5/imports/ui/components/video-preview/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-preview/service.js
@@ -1,4 +1,25 @@
+const promiseTimeout = (ms, promise) => {
+  const timeout = new Promise((resolve, reject) => {
+    const id = setTimeout(() => {
+      clearTimeout(id);
+
+      const error = {
+        name: 'TimeoutError',
+        message: 'Promise did not return',
+      };
+
+      reject(error);
+    }, ms);
+  });
+
+  return Promise.race([
+    promise,
+    timeout,
+  ]);
+};
+
 export default {
+  promiseTimeout,
   changeWebcam: (deviceId) => {
     Session.set('WebcamDeviceId', deviceId);
   },
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
index f71407d1e07daeb435fb03731c92ecec3bba4c94..e1d61fa0ff3da42ff2bab0dea124c85d98fbe3a8 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
@@ -68,8 +68,6 @@ class VideoService {
     const BROWSER_RESULTS = browser();
     this.isMobile = BROWSER_RESULTS.mobile || BROWSER_RESULTS.os.includes('Android');
     this.isSafari = BROWSER_RESULTS.name === 'safari';
-    this.pageChangeLocked = false;
-
     this.numberOfDevices = 0;
 
     this.record = null;
@@ -167,11 +165,28 @@ class VideoService {
         meetingId: Auth.meetingID,
         userId: Auth.userID,
       }, { fields: { stream: 1 } },
-    ).fetch().length;
-    this.sendUserUnshareWebcam(cameraId);
-    if (streams < 2) {
-      // If the user had less than 2 streams, set as a full disconnection
+    ).fetch();
+
+    const hasTargetStream = streams.some(s => s.stream === cameraId);
+    const hasOtherStream = streams.some(s => s.stream !== cameraId);
+
+    // Check if the target (cameraId) stream exists in the remote collection.
+    // If it does, means it was successfully shared. So do the full stop procedure.
+    if (hasTargetStream) {
+      this.sendUserUnshareWebcam(cameraId);
+    }
+
+    if (!hasOtherStream) {
+      // There's no other remote stream, meaning (OR)
+      // a) This was effectively the last webcam being unshared
+      // b) This was a connecting stream timing out (not effectively shared)
+      // For both cases, we clean everything up.
       this.exitedVideo();
+    } else {
+      // It was not the last webcam the user had successfully shared,
+      // nor was cameraId present in the server collection.
+      // Hence it's a connecting stream (not effectively shared) which timed out
+      this.stopConnectingStream();
     }
   }
 
@@ -333,6 +348,11 @@ class VideoService {
     return { streams: paginatedStreams, totalNumberOfStreams: mappedStreams.length };
   }
 
+  stopConnectingStream () {
+    this.deviceId = null;
+    this.isConnecting = false;
+  }
+
   getConnectingStream(streams) {
     let connectingStream;
 
@@ -347,8 +367,7 @@ class VideoService {
           };
         } else {
           // Connecting stream is already stored at database
-          this.deviceId = null;
-          this.isConnecting = false;
+          this.stopConnectingStream();
         }
       } else {
         logger.error({
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx
index 4043f183ad226d473ac4675d4a6a90cd975fe4e1..5b44dcb42a6daa78766f9b5de4b7dd252666093e 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx
@@ -74,7 +74,7 @@ const JoinVideoButton = ({
     <Button
       label={label}
       data-test="joinVideo"
-      className={cx(styles.btn, hasVideoStream || styles.btn)}
+      className={cx(hasVideoStream || styles.btn)}
       onClick={handleOnClick}
       hideLabel
       color={hasVideoStream ? 'primary' : 'default'}
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
index ac1340fb3ecdf3b09e336eb24b8aaf696a1c139e..c4cef8f638b4eb9905b328678f97c828e8a52954 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx
@@ -77,6 +77,8 @@ const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItem
 };
 
 const ASPECT_RATIO = 4 / 3;
+const ACTION_NAME_FOCUS = 'focus';
+const ACTION_NAME_MIRROR = 'mirror';
 
 class VideoList extends Component {
   constructor(props) {
@@ -308,6 +310,7 @@ class VideoList extends Component {
       const isFocusedIntlKey = !isFocused ? 'focus' : 'unfocus';
       const isMirrored = this.cameraIsMirrored(cameraId);
       let actions = [{
+        actionName: ACTION_NAME_MIRROR,
         label: intl.formatMessage(intlMessages['mirrorLabel']),
         description: intl.formatMessage(intlMessages['mirrorDesc']),
         onClick: () => this.mirrorCamera(cameraId),
@@ -315,6 +318,7 @@ class VideoList extends Component {
 
       if (numOfStreams > 2) {
         actions.push({
+          actionName: ACTION_NAME_FOCUS,
           label: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Label`]),
           description: intl.formatMessage(intlMessages[`${isFocusedIntlKey}Desc`]),
           onClick: () => this.handleVideoFocus(cameraId),
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
index 798d99f43fb7a379f4d70c093008bc34d05b6da5..c83a8da8810303334ce57352dc43e241a27233d7 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
@@ -111,7 +111,7 @@ class VideoListItem extends Component {
     return _.compact([
       <DropdownListTitle className={styles.hiddenDesktop} key="name">{name}</DropdownListTitle>,
       <DropdownListSeparator className={styles.hiddenDesktop} key="sep" />,
-      ...actions.map(action => (<DropdownListItem key={cameraId} {...action} />)),
+      ...actions.map(action => (<DropdownListItem key={`${cameraId}-${action.actionName}`} {...action} />)),
     ]);
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx
index efb4d7a281ac069eb49a278e2df81045fb276c2b..8f04eeba79c9e7511fc97518c78d80799c29a807 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/cursor-listener/component.jsx
@@ -2,9 +2,10 @@ import React, { Component } from 'react';
 import { throttle } from 'lodash';
 import PropTypes from 'prop-types';
 
+const { cursorInterval: CURSOR_INTERVAL } = Meteor.settings.public.whiteboard;
+
 // maximum value of z-index to prevent other things from overlapping
 const MAX_Z_INDEX = (2 ** 31) - 1;
-const CURSOR_INTERVAL = 40;
 
 export default class CursorListener extends Component {
   static touchCenterPoint(touches) {
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
index f791cb6a98dd7c9f5f5bd8fd8c3fe33689b79a7a..8a96c794062799b1f986497d71e09814d2edee15 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import {Meteor} from "meteor/meteor";
+import { Meteor } from 'meteor/meteor';
 
 const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
 const DRAW_START = ANNOTATION_CONFIG.status.start;
@@ -51,6 +51,7 @@ export default class TextDrawListener extends Component {
     // Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
     this.hasBeenTouchedRecently = false;
 
+    this.handleClick = this.handleClick.bind(this);
     this.handleMouseDown = this.handleMouseDown.bind(this);
     this.handleMouseMove = this.handleMouseMove.bind(this);
     this.handleMouseUp = this.handleMouseUp.bind(this);
@@ -65,9 +66,9 @@ export default class TextDrawListener extends Component {
 
   componentDidMount() {
     window.addEventListener('beforeunload', this.sendLastMessage);
+    window.addEventListener('click', this.handleClick);
   }
 
-
   // If the activeId suddenly became empty - this means the shape was deleted
   // While the user was drawing it. So we are resetting the state.
   componentWillReceiveProps(nextProps) {
@@ -113,11 +114,17 @@ export default class TextDrawListener extends Component {
 
   componentWillUnmount() {
     window.removeEventListener('beforeunload', this.sendLastMessage);
+    window.removeEventListener('click', this.handleClick);
     // sending the last message on componentDidUnmount
     // for example in case when you switched a tool while drawing text shape
     this.sendLastMessage();
   }
 
+  handleClick() {
+    const { isWritingText } = this.state;
+    if (isWritingText) this.sendLastMessage();
+  }
+
   // checks if the input textarea is focused or not, and if not - moves focus there
   // returns false if text area wasn't focused
   // returns true if textarea was focused
@@ -510,8 +517,9 @@ export default class TextDrawListener extends Component {
     } = this.state;
 
     const { contextMenuHandler } = actions;
-
-    const baseName = Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId;
+    const { settings } = Meteor;
+    const { public: _public } = settings;
+    const baseName = _public.app.cdn + _public.app.basename + _public.app.instanceId;
     const textDrawStyle = {
       width: '100%',
       height: '100%',
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 26801f82b824983a479a7b221ca16b55206a0be0..d7c1a2f425367c64f1ec797a40566743eae3803e 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -635,6 +635,10 @@ class AudioManager {
 
     return audioAlert.play();
   }
+
+  async updateAudioConstraints(constraints) {
+    await this.bridge.updateAudioConstraints(constraints);
+  }
 }
 
 const audioManager = new AudioManager();
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 12d27d5696f1db96960f4e2161209af6a9944244..c9a21301f3ba09a215ed4d9b08d1b26e103e0d5d 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -56,6 +56,25 @@ public:
         raiseHandPushAlerts: true
         fallbackLocale: en
         overrideLocale: null
+        #Audio constraints for microphone. Use this to control browser's
+        #filters, such as AGC (Auto Gain Control) , Echo Cancellation,
+        #Noise Supression, etc.
+        #For more deails, see:
+        # https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
+        #Currently, google chrome sets {ideal: true} for autoGainControl,
+        #echoCancellation and noiseSuppression, if not set.
+        #The accepted value for each constraint is an object of type
+        #https://developer.mozilla.org/en-US/docs/Web/API/ConstrainBoolean
+        #These values are used as initial constraints for every new participant,
+        #and can be changed by user in: Settings > Application > Microphone
+        #Audio Filters.
+        # microphoneConstraints:
+        #   autoGainControl:
+        #     ideal: true
+        #   echoCancellation:
+        #     ideal: true
+        #   noiseSuppression:
+        #     ideal: true
       audio:
         inputDeviceId: undefined
         outputDeviceId: undefined
@@ -111,10 +130,12 @@ public:
     # Valid for video-provider. Time (ms) before its WS connection times out
     # and tries to reconnect.
     wsConnectionTimeout: 4000
+    # Time in milis to wait for the browser to return a gUM call (used in video-preview)
+    gUMTimeout: 20000
     cameraTimeouts:
       # Base camera timeout: used as the camera *sharing* timeout and
       # as the minimum camera subscribe reconnection timeout
-      baseTimeout: 15000
+      baseTimeout: 30000
       # Max timeout: used as the max camera subscribe reconnection timeout. Each
       # subscribe reattempt increases the reconnection timer up to this
       maxTimeout: 60000
@@ -253,9 +274,9 @@ public:
       mobilePageSizes:
         moderator: 2
         viewer: 2
-  pingPong:
-    clearUsersInSeconds: 180
-    pongTimeInSeconds: 15
+  syncUsersWithConnectionManager:
+    enabled: false
+    syncInterval: 60000
   allowOutsideCommands:
     toggleRecording: false
     toggleSelfVoice: false
@@ -324,6 +345,8 @@ public:
     #user activates microphone.
     iceGatheringTimeout: 5000
     sipjsHackViaWs: false
+    # Mute/umute toggle throttle time
+    toggleMuteThrottleTime: 300
     #Websocket keepAlive interval (seconds). You may set this to prevent
     #websocket disconnection in some environments. When set, BBB will send
     #'\r\n\r\n' string through SIP.js's websocket. If not set, default value
@@ -401,6 +424,8 @@ public:
     role_moderator: MODERATOR
     role_viewer: VIEWER
   whiteboard:
+    annotationsQueueProcessInterval: 60
+    cursorInterval: 40
     annotations:
       status:
         start: DRAW_START
@@ -539,6 +564,7 @@ private:
     leak:
       enabled: false
     heapdump:
+      thresholdMb: 1024
       enabled: false
       heapdumpFolderPath: HEAPDUMP_FOLDER
       heapdumpIntervalMs: 3600000
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index f5314096e525dbaef585fd526a625a29385af3f0..b28f48fd22f5ee59c56d7463d139ac688a9f3687 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -309,6 +309,7 @@
     "app.screenshare.screenShareLabel" : "Screen share",
     "app.submenu.application.applicationSectionTitle": "Application",
     "app.submenu.application.animationsLabel": "Animations",
+    "app.submenu.application.audioFilterLabel": "Audio Filters for Microphone",
     "app.submenu.application.fontSizeControlLabel": "Font size",
     "app.submenu.application.increaseFontBtnLabel": "Increase application font size",
     "app.submenu.application.decreaseFontBtnLabel": "Decrease application font size",
@@ -609,10 +610,16 @@
     "app.video.iceConnectionStateError": "Connection failure (ICE error 1107)",
     "app.video.permissionError": "Error on sharing webcam.  Please check permissions",
     "app.video.sharingError": "Error on sharing webcam",
+    "app.video.abortError": "Some problem occurred which prevented the device from being used",
+    "app.video.overconstrainedError": "No candidate devices which met the criteria requested",
+    "app.video.securityError": "Media support is disabled on the Document",
+    "app.video.typeError": "List of constraints specified is empty, or has all constraints set to false",
     "app.video.notFoundError": "Could not find webcam. Please make sure it's connected",
     "app.video.notAllowed": "Missing permission for share webcam, please make sure your browser permissions",
     "app.video.notSupportedError": "Can share webcam video only with safe sources, make sure your SSL certificate is valid",
     "app.video.notReadableError": "Could not get webcam video. Please make sure another program is not using the webcam ",
+    "app.video.timeoutError": "Browser did not respond in time.",
+    "app.video.genericError": "An unknown error has occurred with the device (Error {0})",
     "app.video.mediaFlowTimeout1020": "Media could not reach the server (error 1020)",
     "app.video.suggestWebcamLock": "Enforce lock setting to viewers webcams?",
     "app.video.suggestWebcamLockReason": "(this will improve the stability of the meeting)",
@@ -739,4 +746,4 @@
     "app.debugWindow.form.button.copy": "Copy",
     "app.debugWindow.form.enableAutoarrangeLayoutLabel": "Enable Auto Arrange Layout",
     "app.debugWindow.form.enableAutoarrangeLayoutDescription": "(it will be disabled if you drag or resize the webcams area)"
-}
\ No newline at end of file
+}
diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json
index a50ffdf759f12f9698e981183ac03a8c408b553d..2017603dad8e60c53886e5dad5ff3e5c6e750f4d 100644
--- a/bigbluebutton-html5/private/locales/pt_BR.json
+++ b/bigbluebutton-html5/private/locales/pt_BR.json
@@ -283,6 +283,7 @@
     "app.screenshare.screenShareLabel" : "Compartilhamento de tela",
     "app.submenu.application.applicationSectionTitle": "Aplicação",
     "app.submenu.application.animationsLabel": "Animações",
+    "app.submenu.application.audioFilterLabel": "Filtros de áudio para o microfone",
     "app.submenu.application.fontSizeControlLabel": "Tamanho da fonte",
     "app.submenu.application.increaseFontBtnLabel": "Aumentar o tamanho da fonte da aplicação",
     "app.submenu.application.decreaseFontBtnLabel": "Diminuir o tamanho da fonte da aplicação",