diff --git a/bbb-voice-conference/config/freeswitch/conf/autoload_configs/conference.conf.xml b/bbb-voice-conference/config/freeswitch/conf/autoload_configs/conference.conf.xml
index 739511f0a8ed5fc51c73cad6b857d5ed487e30b9..fa37e9452fd48657eabad78c4c8358d4d28b7271 100644
--- a/bbb-voice-conference/config/freeswitch/conf/autoload_configs/conference.conf.xml
+++ b/bbb-voice-conference/config/freeswitch/conf/autoload_configs/conference.conf.xml
@@ -216,6 +216,7 @@
       <param name="caller-id-number" value="$${outbound_caller_id}"/>
       <!-- param name="comfort-noise" value="true"/ -->
       <param name="comfort-noise" value="1400"/>
+      <param name="video-auto-floor-msec" value="2000"/>
 
       <!-- <param name="conference-flags" value="video-floor-only|rfc-4579|livearray-sync|auto-3d-position|minimize-video-encoding"/> -->
 
diff --git a/bbb-voice-conference/config/freeswitch/conf/autoload_configs/logfile.conf.xml b/bbb-voice-conference/config/freeswitch/conf/autoload_configs/logfile.conf.xml
index 670bae957f9ec66c1c49dab4db3c899e3a385b9f..abbd2f737af48e5004023eea799d9b72ce37e9e3 100644
--- a/bbb-voice-conference/config/freeswitch/conf/autoload_configs/logfile.conf.xml
+++ b/bbb-voice-conference/config/freeswitch/conf/autoload_configs/logfile.conf.xml
@@ -9,10 +9,10 @@
         <!-- File to log to -->
 	<!--<param name="logfile" value="/var/log/freeswitch.log"/>-->
         <!-- At this length in bytes rotate the log file (0 for never) -->
-        <param name="rollover" value="1048576000"/>
+        <param name="rollover" value="104857600"/>
 		<!-- Maximum number of log files to keep before wrapping -->
 		<!-- If this parameter is enabled, the log filenames will not include a date stamp -->
-		<param name="maximum-rotate" value="32"/>
+		<param name="maximum-rotate" value="10"/>
         <!-- Prefix all log lines by the session's uuid  -->
         <param name="uuid" value="true" />
       </settings>
@@ -22,7 +22,7 @@
 	     value is one or more of debug,info,notice,warning,err,crit,alert,all
 	     Please see comments in console.conf.xml for more information
 	-->
-	<map name="all" value="console,debug,info,notice,warning,err,crit,alert"/>
+	<map name="all" value="info,notice,warning,err,crit,alert"/>
       </mappings>
     </profile>
   </profiles>
diff --git a/bigbluebutton-html5/.eslintrc.js b/bigbluebutton-html5/.eslintrc.js
index a43e3c5f16931c9653447587abf9cfef215b8dc4..b93b3575a49bb8e24ac18da4470a96be92138116 100644
--- a/bigbluebutton-html5/.eslintrc.js
+++ b/bigbluebutton-html5/.eslintrc.js
@@ -24,6 +24,7 @@ module.exports = {
     'react/prop-types': 1,
     'jsx-a11y/no-access-key': 0,
     'react/jsx-props-no-spreading': 'off',
+    'max-classes-per-file': ['error', 2],
   },
   globals: {
     browser: 'writable',
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index ac0d4a93cfb3183aa12c3aec82ce547604004a14..ca41fc8f87cbace78d7b33e51e0749257983d686 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -119,28 +119,69 @@ class SIPSession {
     return matchConstraints;
   }
 
-  setInputStream(stream) {
-    if (!this.currentSession
-      || !this.currentSession.sessionDescriptionHandler
-    ) return;
+  /**
+   * Set the input stream for the peer that represents the current session.
+   * Internally, this will call the sender's replaceTrack function.
+   * @param  {MediaStream}  stream The MediaStream object to be used as input
+   *                               stream
+   * @return {Promise}            A Promise that is resolved with the
+   *                              MediaStream object that was set.
+   */
+  async setInputStream(stream) {
+    try {
+      if (!this.currentSession
+        || !this.currentSession.sessionDescriptionHandler
+      ) return null;
 
+      await this.currentSession.sessionDescriptionHandler
+        .setLocalMediaStream(stream);
 
-    this.currentSession.sessionDescriptionHandler.setLocalMediaStream(stream);
+      return stream;
+    } catch (error) {
+      logger.warn({
+        logCode: 'sip_js_setinputstream_error',
+        extraInfo: {
+          errorCode: error.code,
+          errorMessage: error.message,
+          callerIdName: this.user.callerIdName,
+        },
+      }, 'Failed to set input stream (mic)');
+      return null;
+    }
   }
 
+  /**
+   * Change the input device with the given deviceId, without renegotiating
+   * peer.
+   * A new MediaStream object is created for the given deviceId. This object
+   * is returned by the resolved promise.
+   * @param  {String}  deviceId The id of the device to be set as input
+   * @return {Promise}          A promise that is resolved with the MediaStream
+   *                            object after changing the input device.
+   */
+  async liveChangeInputDevice(deviceId) {
+    try {
+      this.inputDeviceId = deviceId;
 
-  liveChangeInputDevice(deviceId) {
-    this.inputDeviceId = deviceId;
-
-    const constraints = {
-      audio: this.getAudioConstraints(),
-    };
+      const constraints = {
+        audio: this.getAudioConstraints(),
+      };
 
-    this.inputStream.getAudioTracks().forEach(t => t.stop());
+      this.inputStream.getAudioTracks().forEach((t) => t.stop());
 
-    return navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
-      this.setInputStream(stream);
-    });
+      return await navigator.mediaDevices.getUserMedia(constraints)
+        .then(this.setInputStream.bind(this));
+    } catch (error) {
+      logger.warn({
+        logCode: 'sip_js_livechangeinputdevice_error',
+        extraInfo: {
+          errorCode: error.code,
+          errorMessage: error.message,
+          callerIdName: this.user.callerIdName,
+        },
+      }, 'Failed to change input device (mic)');
+      return null;
+    }
   }
 
   get inputDeviceId() {
@@ -149,7 +190,7 @@ class SIPSession {
 
       if (stream) {
         const track = stream.getAudioTracks().find(
-          t => t.getSettings().deviceId,
+          (t) => t.getSettings().deviceId,
         );
 
         if (track && (typeof track.getSettings === 'function')) {
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 5186879e1cdfc392bccfdd07b968e7c9d21452bb..8af4e57b2869a482c963684a97735a3cc954eff8 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -19,7 +19,6 @@ import { notify } from '/imports/ui/services/notification';
 import deviceInfo from '/imports/utils/deviceInfo';
 import { invalidateCookie } from '/imports/ui/components/audio/audio-modal/service';
 import getFromUserSettings from '/imports/ui/services/users-settings';
-import LayoutManagerComponent from '/imports/ui/components/layout/layout-manager/component';
 import LayoutManagerContainer from '/imports/ui/components/layout/layout-manager/container';
 import { withLayoutContext } from '/imports/ui/components/layout/context';
 import VideoService from '/imports/ui/components/video-provider/service';
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index 0d2565a0f557816453c6c92c90e69ee317a9cb28..26c232b3f462c5dfa1d131a01b73f5e4039c08aa 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -21,7 +21,9 @@ import MediaService, {
 const ActionsBarContainer = (props) => {
   const usingUsersContext = useContext(UsersContext);
   const { users } = usingUsersContext;
-  const currentUser = users[Auth.meetingID][Auth.userID];
+
+  const currentUser = { userId: Auth.userID, emoji: users[Auth.meetingID][Auth.userID].emoji };
+
   return (
     <ActionsBar {
       ...{
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
index 8c40bf140b98bddf73366468fcff18b0522cc57d..9b9c082da510cd6e7a67173866b1b761c5e64f60 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -30,6 +30,7 @@ const intlMessages = defineMessages({
 });
 
 const propTypes = {
+  shortcuts: PropTypes.objectOf(PropTypes.string).isRequired,
   processToggleMuteFromOutside: PropTypes.func.isRequired,
   handleToggleMuteMicrophone: PropTypes.func.isRequired,
   handleJoinAudio: PropTypes.func.isRequired,
@@ -88,8 +89,11 @@ class AudioControls extends PureComponent {
     );
   }
 
-  static renderLeaveButtonWithLiveStreamSelector() {
-    return (<InputStreamLiveSelectorContainer />);
+  static renderLeaveButtonWithLiveStreamSelector(props) {
+    const { handleLeaveAudio } = props;
+    return (
+      <InputStreamLiveSelectorContainer {...{ handleLeaveAudio }} />
+    );
   }
 
   renderLeaveButtonWithoutLiveStreamSelector() {
@@ -151,7 +155,8 @@ class AudioControls extends PureComponent {
 
     if (inAudio) {
       if (_enableDynamicDeviceSelection) {
-        return AudioControls.renderLeaveButtonWithLiveStreamSelector();
+        return AudioControls.renderLeaveButtonWithLiveStreamSelector(this
+          .props);
       }
 
       return this.renderLeaveButtonWithoutLiveStreamSelector();
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
index f3c993f1a8a60454bf93c8ac96e0df8a22cfcda8..aed2c2f38397c51b9940cf5a97c5e0c94acf7e93 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -11,14 +11,23 @@ import Storage from '/imports/ui/services/storage/session';
 import getFromUserSettings from '/imports/ui/services/users-settings';
 import AudioControls from './component';
 import AudioModalContainer from '../audio-modal/container';
-import { invalidateCookie } from '../audio-modal/service';
+import {
+  setUserSelectedMicrophone,
+  setUserSelectedListenOnly,
+} from '../audio-modal/service';
+
 import Service from '../service';
 import AppService from '/imports/ui/components/app/service';
 
 const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
 const APP_CONFIG = Meteor.settings.public.app;
 
-const AudioControlsContainer = props => <AudioControls {...props} />;
+const AudioControlsContainer = (props) => {
+  const {
+    users, lockSettings, userLocks, children, ...newProps
+  } = props;
+  return <AudioControls {...newProps} />;
+};
 
 const processToggleMuteFromOutside = (e) => {
   switch (e.data) {
@@ -46,7 +55,8 @@ const handleLeaveAudio = () => {
   const meetingIsBreakout = AppService.meetingIsBreakout();
 
   if (!meetingIsBreakout) {
-    invalidateCookie('joinedAudio');
+    setUserSelectedMicrophone(false);
+    setUserSelectedListenOnly(false);
   }
 
   const skipOnFistJoin = getFromUserSettings('bbb_skip_check_audio_on_first_join', APP_CONFIG.skipCheckOnJoin);
@@ -88,7 +98,7 @@ export default withUsersConsumer(lockContextContainer(withModalMounter(withTrack
   }
 
   return ({
-    processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg),
+    processToggleMuteFromOutside: (arg) => processToggleMuteFromOutside(arg),
     showMute: isConnected() && !isListenOnly() && !isEchoTest() && !userLocks.userMic,
     muted: isConnected() && !isListenOnly() && isMuted(),
     inAudio: isConnected() && !isEchoTest(),
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx
index 535588a3e45efb9e705cb96e9dcaac60e56ad456..94c2186c4e8f7f98ebae4fdbcf50446ef77d3524 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/component.jsx
@@ -50,7 +50,7 @@ const intlMessages = defineMessages({
 
 const propTypes = {
   liveChangeInputDevice: PropTypes.func.isRequired,
-  exitAudio: PropTypes.func.isRequired,
+  handleLeaveAudio: PropTypes.func.isRequired,
   liveChangeOutputDevice: PropTypes.func.isRequired,
   intl: PropTypes.shape({
     formatMessage: PropTypes.func.isRequired,
@@ -267,7 +267,7 @@ class InputStreamLiveSelector extends Component {
 
     const {
       liveChangeInputDevice,
-      exitAudio,
+      handleLeaveAudio,
       liveChangeOutputDevice,
       intl,
       shortcuts,
@@ -299,7 +299,7 @@ class InputStreamLiveSelector extends Component {
           key="leaveAudioButtonKey"
           className={styles.stopButton}
           label={intl.formatMessage(intlMessages.leaveAudio)}
-          onClick={() => exitAudio()}
+          onClick={() => handleLeaveAudio()}
           accessKey={shortcuts.leaveaudio}
         />,
       ]);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/container.jsx
index 4c0c04c0ed989a21fc23275bcce2d162ecd7deea..86e4dca851a9b428f04922eefab6ab71c6e0a6c3 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/input-stream-live-selector/container.jsx
@@ -11,14 +11,12 @@ class InputStreamLiveSelectorContainer extends PureComponent {
   }
 }
 
-export default withTracker(() => {
-  return {
-    isAudioConnected: Service.isConnected(),
-    isListenOnly: Service.isListenOnly(),
-    currentInputDeviceId: Service.inputDeviceId(),
-    currentOutputDeviceId: Service.outputDeviceId(),
-    liveChangeInputDevice: Service.liveChangeInputDevice,
-    liveChangeOutputDevice: Service.changeOutputDevice,
-    exitAudio: Service.exitAudio,
-  };
-})(InputStreamLiveSelectorContainer);
+export default withTracker(({ handleLeaveAudio }) => ({
+  isAudioConnected: Service.isConnected(),
+  isListenOnly: Service.isListenOnly(),
+  currentInputDeviceId: Service.inputDeviceId(),
+  currentOutputDeviceId: Service.outputDeviceId(),
+  liveChangeInputDevice: Service.liveChangeInputDevice,
+  liveChangeOutputDevice: Service.changeOutputDevice,
+  handleLeaveAudio,
+}))(InputStreamLiveSelectorContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
index 24b8cff217babd4aa96d9ad983ef769f554d6147..b0b0488af0e4607b55d0948aee47c0b3f0190293 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
@@ -15,13 +15,11 @@ import {
   closeModal,
   joinListenOnly,
   leaveEchoTest,
-  getcookieData,
 } from './service';
 import Storage from '/imports/ui/services/storage/session';
 import Service from '../service';
 
-const AudioModalContainer = props => <AudioModal {...props} />;
-
+const AudioModalContainer = (props) => <AudioModal {...props} />;
 
 const APP_CONFIG = Meteor.settings.public.app;
 
@@ -50,7 +48,6 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
   }
 
   const meetingIsBreakout = AppService.meetingIsBreakout();
-  const { joinedAudio } = getcookieData();
 
   const joinFullAudioImmediately = (autoJoin && (skipCheck || skipCheckOnJoin && !getEchoTest))
     || (skipCheck || skipCheckOnJoin && !getEchoTest);
@@ -61,14 +58,15 @@ export default lockContextContainer(withModalMounter(withTracker(({ userLocks })
   const { isChrome, isIe } = browserInfo;
 
   return ({
-    joinedAudio,
     meetingIsBreakout,
     closeModal,
-    joinMicrophone: skipEchoTest => joinMicrophone(skipEchoTest || skipCheck || skipCheckOnJoin),
+    joinMicrophone: (skipEchoTest) => joinMicrophone(skipEchoTest || skipCheck || skipCheckOnJoin),
     joinListenOnly,
     leaveEchoTest,
-    changeInputDevice: inputDeviceId => Service.changeInputDevice(inputDeviceId),
-    changeOutputDevice: outputDeviceId => Service.changeOutputDevice(outputDeviceId),
+    changeInputDevice: (inputDeviceId) => Service
+      .changeInputDevice(inputDeviceId),
+    changeOutputDevice: (outputDeviceId) => Service
+      .changeOutputDevice(outputDeviceId),
     joinEchoTest: () => Service.joinEchoTest(),
     exitAudio: () => Service.exitAudio(),
     isConnecting: Service.isConnecting(),
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js
index afb7d098ec406b2fcbb0316863c7eea7c89987e9..738f2ef7ffa9be7d4cf5c74067dbbc61bcaec4b7 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js
@@ -1,39 +1,29 @@
 import { showModal } from '/imports/ui/components/modal/service';
 import Service from '../service';
-import AppService from '/imports/ui/components/app/service';
+import Storage from '/imports/ui/services/storage/session';
 
-export const getcookieData = () => {
-  const cookiesString = document.cookie;
-  const cookies = cookiesString.split(';');
-  const cookiesKeyValue = cookies.reduce((acc, value) => {
-    if (!value) return acc;
-    const splitValue = value.trim().split('=');
-    return {
-      ...acc,
-      [splitValue[0]]: splitValue[1],
-    };
-  }, {});
-  return cookiesKeyValue;
-};
+const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone';
+const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly';
 
-export const setCookieData = (name, value, daysToExpire = 1) => {
-  const date = new Date();
-  date.setDate(date.getDate() + daysToExpire);
-  document.cookie = `${name}=${value};expires=${date.toUTCString()}`;
-};
+export const setUserSelectedMicrophone = (value) => (
+  Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, !!value)
+);
 
-export const invalidateCookie = (name) => {
-  const cookies = getcookieData();
-  if (cookies[name]) {
-    // set the expires date to current date invalid the cookie
-    setCookieData(name, false, 0);
-    return true;
-  }
-  return false;
-};
+export const setUserSelectedListenOnly = (value) => (
+  Storage.setItem(CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY, !!value)
+);
+
+export const didUserSelectedMicrophone = () => (
+  !!Storage.getItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY)
+);
+
+export const didUserSelectedListenOnly = () => (
+  !!Storage.getItem(CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY)
+);
 
-export const joinMicrophone = (skipEchoTest = false, changeInputDevice = false) => {
-  const meetingIsBreakout = AppService.meetingIsBreakout();
+export const joinMicrophone = (skipEchoTest = false) => {
+  Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, true);
+  Storage.setItem(CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY, false);
 
   const call = new Promise((resolve, reject) => {
     if (skipEchoTest) {
@@ -45,17 +35,6 @@ export const joinMicrophone = (skipEchoTest = false, changeInputDevice = false)
   });
 
   return call.then(() => {
-    const { inputAudioId } = getcookieData();
-
-    if (changeInputDevice && inputAudioId) {
-      Service.changeInputDevice(inputAudioId);
-    }
-
-    if (!meetingIsBreakout) {
-      const inputDeviceId = Service.inputDeviceId();
-      setCookieData('joinedAudio', true, 1);
-      setCookieData('inputAudioId', inputDeviceId, 1);
-    }
     showModal(null);
   }).catch((error) => {
     throw error;
@@ -63,6 +42,9 @@ export const joinMicrophone = (skipEchoTest = false, changeInputDevice = false)
 };
 
 export const joinListenOnly = () => {
+  Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, false);
+  Storage.setItem(CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY, true);
+
   const call = new Promise((resolve) => {
     Service.joinListenOnly().then(() => {
       // Autoplay block wasn't triggered. Close the modal. If autoplay was
@@ -96,4 +78,6 @@ export default {
   closeModal,
   joinListenOnly,
   leaveEchoTest,
+  didUserSelectedMicrophone,
+  didUserSelectedListenOnly,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index d2d48b1df6c9b0c94228b29f96c0b9d55ec8cf41..81f37ef27e2a0557583776dbbbd7dc0a78df6703 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -1,4 +1,5 @@
 import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
 import { withTracker } from 'meteor/react-meteor-data';
 import { Session } from 'meteor/session';
 import { withModalMounter } from '/imports/ui/components/modal/service';
@@ -10,7 +11,13 @@ import { notify } from '/imports/ui/services/notification';
 import getFromUserSettings from '/imports/ui/services/users-settings';
 import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
 import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
-import { getcookieData, joinMicrophone } from '/imports/ui/components/audio/audio-modal/service';
+import {
+  joinMicrophone,
+  joinListenOnly,
+  didUserSelectedMicrophone,
+  didUserSelectedListenOnly,
+} from '/imports/ui/components/audio/audio-modal/service';
+
 import Service from './service';
 import AudioModalContainer from './audio-modal/container';
 import Settings from '/imports/ui/services/settings';
@@ -73,20 +80,54 @@ class AudioContainer extends PureComponent {
   }
 
   componentDidMount() {
-    const { meetingIsBreakout, joinedAudio } = this.props;
+    const { meetingIsBreakout } = this.props;
+
     this.init();
-    if (meetingIsBreakout && joinedAudio) {
-      joinMicrophone(true, true);
+
+    if (meetingIsBreakout) {
+      this.joinAudio();
     }
   }
 
   componentDidUpdate(prevProps) {
-    const { hasBreakoutRooms, joinedAudio } = this.props;
+    if (this.userIsReturningFromBreakoutRoom(prevProps)) {
+      this.joinAudio();
+    }
+  }
+
+  /**
+   * Helper function to determine wheter user is returning from breakout room
+   * to main room.
+   * @param  {[Object} prevProps prevProps param from componentDidUpdate
+   * @return {boolean}           True if user is returning from breakout room
+   *                             to main room. False, otherwise.
+   */
+  userIsReturningFromBreakoutRoom(prevProps) {
+    const { hasBreakoutRooms } = this.props;
     const { hasBreakoutRooms: hadBreakoutRooms } = prevProps;
-    if (hadBreakoutRooms && !hasBreakoutRooms && joinedAudio
-      && !Service.isConnected()) {
-      joinMicrophone(true, true);
+    return hadBreakoutRooms && !hasBreakoutRooms;
+  }
+
+  /**
+   * Helper function that join (or not) user in audio. If user previously
+   * selected microphone, it will automatically join mic (without audio modal).
+   * If user previously selected listen only option in audio modal, then it will
+   * automatically join listen only.
+   */
+  joinAudio() {
+    if (Service.isConnected()) return;
+
+    const {
+      userSelectedMicrophone,
+      userSelectedListenOnly,
+    } = this.props;
+
+    if (userSelectedMicrophone) {
+      joinMicrophone(true);
+      return;
     }
+
+    if (userSelectedListenOnly) joinListenOnly();
   }
 
   render() {
@@ -126,7 +167,9 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
   const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
   const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam);
   const { userWebcam, userMic } = userLocks;
-  const { joinedAudio } = getcookieData();
+
+  const userSelectedMicrophone = didUserSelectedMicrophone();
+  const userSelectedListenOnly = didUserSelectedListenOnly();
   const meetingIsBreakout = AppService.meetingIsBreakout();
   const hasBreakoutRooms = AppService.getBreakoutRooms().length > 0;
   const openAudioModal = () => new Promise((resolve) => {
@@ -152,7 +195,13 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
       // if the user joined a breakout room, the main room's audio was
       // programmatically dropped to avoid interference. On breakout end,
       // offer to rejoin main room audio only if the user is not in audio already
-      if (Service.isUsingAudio() || joinedAudio) {
+      if (Service.isUsingAudio()
+        || userSelectedMicrophone
+        || userSelectedListenOnly) {
+        if (enableVideo && autoShareWebcam) {
+          openVideoPreviewModal();
+        }
+
         return;
       }
       setTimeout(() => openAudioModal().then(() => {
@@ -166,7 +215,8 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
   return {
     hasBreakoutRooms,
     meetingIsBreakout,
-    joinedAudio,
+    userSelectedMicrophone,
+    userSelectedListenOnly,
     init: () => {
       Service.init(messages, intl);
       const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo);
@@ -180,10 +230,20 @@ export default lockContextContainer(withModalMounter(injectIntl(withTracker(({ m
       Session.set('audioModalIsOpen', true);
       if (enableVideo && autoShareWebcam) {
         openAudioModal().then(() => { openVideoPreviewModal(); didMountAutoJoin = true; });
-      } else if (!(joinedAudio && meetingIsBreakout)) {
+      } else if (!(
+        userSelectedMicrophone
+        && userSelectedListenOnly
+        && meetingIsBreakout)) {
         openAudioModal();
         didMountAutoJoin = true;
       }
     },
   };
 })(AudioContainer))));
+
+AudioContainer.propTypes = {
+  hasBreakoutRooms: PropTypes.bool.isRequired,
+  meetingIsBreakout: PropTypes.bool.isRequired,
+  userSelectedListenOnly: PropTypes.bool.isRequired,
+  userSelectedMicrophone: PropTypes.bool.isRequired,
+};
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index 3bb841646c32c8ed32fe3bb0a92dc8352cc6e407..ce751faeaaa5c92a84a198144370423ccce175fb 100755
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -107,9 +107,7 @@
   display: inline-block;
   cursor: pointer;
 
-  &:-moz-focusring {
-    outline: none;
-  }
+
 
   &:focus,
   &:hover {
@@ -120,6 +118,12 @@
     outline-style: solid;
   }
 
+  &:-moz-focusring {
+    outline-color: transparent;
+    outline-offset: var(--border-radius);
+  }
+
+
   &:active {
     &:focus {
       span:first-of-type::before {
diff --git a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
index 134bfc6f753da79d13d8846b73579e724bacfde7..08449297ad36781c089193c10beaaea031915c7c 100644
--- a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
+++ b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
@@ -328,6 +328,7 @@ const reducer = (state, action) => {
             timeWindowIds.forEach((timeWindowId)=> {
               const timeWindow = messages[timeWindowId];
               if (timeWindow.messageType === MESSAGE_TYPES.STREAM) {
+                chat.unreadTimeWindows.delete(timeWindowId);
                 delete newState[chatId][group][timeWindowId];
               }
             });
diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx
index b659cd52dc4d3f846aa4290f59cc2ba56ebe874f..a9921d00ca5f3f1608962851fca8946da9ae2c4a 100644
--- a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx
@@ -396,6 +396,10 @@ class VideoPlayer extends Component {
     this.setState({ playing: true });
 
     this.handleFirstPlay();
+
+    if (!isPresenter && !playing) {
+      this.setState({ playing: false });
+    }
   }
 
   handleOnPause() {
@@ -410,6 +414,10 @@ class VideoPlayer extends Component {
     this.setState({ playing: false });
 
     this.handleFirstPlay();
+
+    if (!isPresenter && playing) {
+      this.setState({ playing: true });
+    }
   }
 
   render() {
diff --git a/bigbluebutton-html5/imports/ui/components/layout/context.jsx b/bigbluebutton-html5/imports/ui/components/layout/context.jsx
index 0cd9feda2bda3e5a383a7129408d9e7542fe8376..e250b3dc17869e1d09f24715fa1a836e2f5ec041 100644
--- a/bigbluebutton-html5/imports/ui/components/layout/context.jsx
+++ b/bigbluebutton-html5/imports/ui/components/layout/context.jsx
@@ -1,8 +1,6 @@
 import React, { createContext, useReducer, useEffect } from 'react';
 import Storage from '/imports/ui/services/storage/session';
 
-const { webcamsDefaultPlacement } = Meteor.settings.public.layout;
-
 export const LayoutContext = createContext();
 
 const initialState = {
@@ -50,7 +48,7 @@ const initialState = {
   },
   webcamsAreaUserSetsHeight: 0,
   webcamsAreaUserSetsWidth: 0,
-  webcamsPlacement: webcamsDefaultPlacement || 'top',
+  webcamsPlacement: 'top',
   presentationAreaSize: {
     width: 0,
     height: 0,
diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/container.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/container.jsx
index c38735a33e9a61f1fe8e3f42d80be4a7959ef564..2136bbc5fae43275d23d1190741d0d7c8309c901 100644
--- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/container.jsx
@@ -1,7 +1,5 @@
 import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
-import Auth from '/imports/ui/services/auth';
-import Screenshare from '/imports/api/screenshare';
 import { isVideoBroadcasting } from '/imports/ui/components/screenshare/service';
 import LayoutManagerComponent from '/imports/ui/components/layout/layout-manager/component';
 
diff --git a/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
index 4c68acf9a63a1dedfc08253b60f2b4706e7066d2..667476dcac2ae3039efa9afeb9cf6d47bbdc6746 100644
--- a/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/modal/random-user/component.jsx
@@ -107,7 +107,6 @@ class RandomUserSelect extends Component {
       intl,
       mountModal,
       numAvailableViewers,
-      randomUserReq,
       currentUser,
       clearRandomlySelectedUser,
       mappedRandomlySelectedUsers,
diff --git a/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx b/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx
index d86f24d0ad9e7f48dc515b1b2cdfa955cf6062ad..3a5d03ff4487930faba45ab60899137b835e9243 100644
--- a/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/muted-alert/component.jsx
@@ -62,16 +62,27 @@ class MutedAlert extends Component {
     this._isMounted = false;
     if (this.speechEvents) this.speechEvents.stop();
     if (this.inputStream) {
-      this.inputStream.getTracks().forEach(t => t.stop());
+      this.inputStream.getTracks().forEach((t) => t.stop());
     }
     this.resetTimer();
   }
 
   cloneMediaStream() {
     if (this.inputStream) return;
-    const { inputStream, muted } = this.props;
-    if (inputStream && !muted) this.inputStream = inputStream.clone();
+    const { inputStream } = this.props;
+
+    if (inputStream) {
+      this.inputStream = inputStream.clone();
+      this.enableInputStreamAudioTracks(this.inputStream);
+    }
+  }
+
+  /* eslint-disable no-param-reassign */
+  enableInputStreamAudioTracks() {
+    if (!this.inputStream) return;
+    this.inputStream.getAudioTracks().forEach((t) => { t.enabled = true; });
   }
+  /* eslint-enable no-param-reassign */
 
   resetTimer() {
     if (this.timer) clearTimeout(this.timer);
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
index 01a7590c93a6a9804ddaae4af8610a12faa9ba45..fd4887a7cf7726f2a4cf34ad5a58d1903e6bceb5 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx
@@ -102,7 +102,6 @@ class NavBar extends Component {
               : <Icon iconName="left_arrow" className={styles.arrowLeft} />
             }
             <Button
-              data-test="userListToggleButton"
               onClick={NavBar.handleToggleUserList}
               ghost
               circle
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx
index 9efb966b8d552a025b27390b9a74096387f64f61..9a588bbeb7a896069f42d2d03eeaa01e311113b0 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/container.jsx
@@ -15,9 +15,14 @@ import WhiteboardService from '/imports/ui/components/whiteboard/service';
 const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
 
 const PresentationAreaContainer = ({ presentationPodIds, mountPresentationArea, ...props }) => {
+  const { layoutSwapped, podId } = props;
+
   const usingUsersContext = useContext(UsersContext);
   const { users } = usingUsersContext;
   const currentUser = users[Auth.meetingID][Auth.userID];
+
+  const userIsPresenter = (podId === 'DEFAULT_PRESENTATION_POD') ? currentUser.presenter : props.isPresenter;
+
   return mountPresentationArea
     && (
       <PresentationArea
@@ -25,6 +30,7 @@ const PresentationAreaContainer = ({ presentationPodIds, mountPresentationArea,
         ...{
           ...props,
           isViewer: currentUser.role === ROLE_VIEWER,
+          userIsPresenter: userIsPresenter && !layoutSwapped,
         }
         }
       />
@@ -81,7 +87,7 @@ export default withTracker(({ podId }) => {
     currentSlide,
     slidePosition,
     downloadPresentationUri: PresentationAreaService.downloadPresentationUri(podId),
-    userIsPresenter: PresentationAreaService.isPresenter(podId) && !layoutSwapped,
+    isPresenter: PresentationAreaService.isPresenter(podId),
     multiUser: WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
       && !layoutSwapped,
     presentationIsDownloadable,
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/download-presentation-button/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/download-presentation-button/component.jsx
index 6c096167d1414cef07854d7fc4346021ebba2241..7ec31b72ba748ac387a64f78b7624e285b812b0b 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/download-presentation-button/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/download-presentation-button/component.jsx
@@ -29,7 +29,7 @@ const DownloadPresentationButton = ({
     <Button
       color="default"
       icon="template_download"
-      size="sm"
+      size="md"
       onClick={handleDownloadPresentation}
       label={intl.formatMessage(intlMessages.downloadPresentationButton)}
       hideLabel
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
index b9a3ab428d55c5fc90041a5938a8ab5750c9a536..11a749cb172d4589220746f04ba141e33d6741eb 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx
@@ -242,7 +242,7 @@ class PresentationUploader extends Component {
   }
 
   componentDidUpdate(prevProps) {
-    const { selectedToBeNextCurrent, isOpen, presentations: propPresentations } = this.props;
+    const { isOpen, presentations: propPresentations } = this.props;
     const { presentations } = this.state;
 
     // cleared local presetation state errors and set to presentations available on the server
@@ -356,10 +356,6 @@ class PresentationUploader extends Component {
     const hasError = item.conversion.error || item.upload.error;
     const isProcessing = (isUploading || isConverting) && !hasError;
 
-    const {
-      intl, selectedToBeNextCurrent,
-    } = this.props;
-
     const itemClassName = {
       [styles.done]: !isProcessing && !hasError,
       [styles.err]: hasError,
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js
index 879283f90d8f911e8031c533021089264a019a8c..b04cab4388bb181a3eb595e74d4d1c94104c97b6 100755
--- a/bigbluebutton-html5/imports/ui/components/presentation/service.js
+++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js
@@ -1,7 +1,6 @@
 import PresentationPods from '/imports/api/presentation-pods';
 import Presentations from '/imports/api/presentations';
 import { Slides, SlidePositions } from '/imports/api/slides';
-import Users from '/imports/api/users';
 import Auth from '/imports/ui/services/auth';
 
 const getCurrentPresentation = podId => Presentations.findOne({
@@ -163,25 +162,16 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue,
 
 const isPresenter = (podId) => {
   // a main presenter in the meeting always owns a default pod
-  if (podId === 'DEFAULT_PRESENTATION_POD') {
-    const options = {
-      filter: {
-        presenter: 1,
-      },
+  if (podId !== 'DEFAULT_PRESENTATION_POD') {
+    // if a pod is not default, then we check whether this user owns a current pod
+    const selector = {
+      meetingId: Auth.meetingID,
+      podId,
     };
-    const currentUser = Users.findOne({
-      userId: Auth.userID,
-    }, options);
-    return currentUser ? currentUser.presenter : false;
+    const pod = PresentationPods.findOne(selector);
+    return pod.currentPresenterId === Auth.userID;
   }
-
-  // if a pod is not default, then we check whether this user owns a current pod
-  const selector = {
-    meetingId: Auth.meetingID,
-    podId,
-  };
-  const pod = PresentationPods.findOne(selector);
-  return pod.currentPresenterId === Auth.userID;
+  return true;
 };
 
 export default {
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
index 5e960f8d65be867d52dd31f4763a3d40af351a8a..030da381cf89b543260c065b07cc192e9d11d2fd 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
@@ -189,7 +189,7 @@ class ScreenshareComponent extends React.Component {
 
   render() {
     const { loaded, autoplayBlocked, isFullscreen, isStreamHealthy } = this.state;
-    const { intl, isPresenter, isGloballyBroadcasting } = this.props;
+    const { isPresenter, isGloballyBroadcasting } = this.props;
 
     // Conditions to render the (re)connecting spinner and the unhealthy stream
     // grayscale:
diff --git a/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx b/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
index 163eb897b33ea0bdfdf505d404f6ec708c13eb04..eea62234a2c20972eb8228df71a3034b2654d3b5 100644
--- a/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/status-notifier/component.jsx
@@ -20,7 +20,11 @@ const messages = defineMessages({
   },
   raisedHandDesc: {
     id: 'app.statusNotifier.raisedHandDesc',
-    description: 'label for user with raised hands',
+    description: 'label for multiple users with raised hands',
+  },
+  raisedHandDescOneUser: {
+    id: 'app.statusNotifier.raisedHandDescOneUser',
+    description: 'label for a single user with raised hand',
   },
   and: {
     id: 'app.statusNotifier.and',
@@ -110,7 +114,9 @@ class StatusNotifier extends Component {
         break;
     }
 
-    return intl.formatMessage(messages.raisedHandDesc, { 0: formattedNames });
+    const raisedHandMessageString
+        = length === 1 ? messages.raisedHandDescOneUser : messages.raisedHandDesc;
+    return intl.formatMessage(raisedHandMessageString, { 0: formattedNames });
   }
 
   raisedHandAvatars() {
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/container.jsx
index caf09b7f7137cb815e8d6efe3732836c6c1cd6d7..5a0211b63b80a18f825e412a8b202d29bedf9620 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/container.jsx
@@ -12,9 +12,14 @@ const CLOSED_CHAT_LIST_KEY = 'closedChatList';
 const UserContentContainer = (props) => {
   const usingUsersContext = useContext(UsersContext);
   const { users } = usingUsersContext;
-  const currentUser = users[Auth.meetingID][Auth.userID];
+  const currentUser = {
+    userId: Auth.userID,
+    presenter: users[Auth.meetingID][Auth.userID].presenter,
+    locked: users[Auth.meetingID][Auth.userID].locked,
+    role: users[Auth.meetingID][Auth.userID].role,
+  };
   return (<UserContent {...props} currentUser={currentUser} />);
-}
+};
 
 export default withTracker(() => ({
   pollIsOpen: Session.equals('isPollOpen', true),
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
index 2dc93d645a0aae8c2eaa5454cf63a2f7b122c03d..fa8143c72eda4467ca1d83ef0ef32bc8137eca26 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
@@ -45,7 +45,6 @@ const {
 } = Meteor.settings.public.kurento.cameraSortingModes;
 
 const TOKEN = '_';
-const ENABLE_PAGINATION_SESSION_VAR = 'enablePagination';
 
 class VideoService {
   constructor() {
diff --git a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
index bc5791c572a2aed78bbe4736c9678127d836f81a..92a0306e46a2d770eb67329345ef16453a6b533d 100644
--- a/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/waiting-users/styles.scss
@@ -96,8 +96,7 @@
   display: flex;
   flex-flow: row;
   flex-direction: row;
-  align-items: center; 
-  justify-content: space-between;
+  align-items: center;
   border-radius: 5px;
   cursor: pointer;
   :global(.animationsEnabled) & {
@@ -132,14 +131,15 @@
 
 .userContentContainer {
   display: flex;
+  flex: 1;
+  overflow: hidden;
   align-items: center;
-  width: 64%;
   flex-direction: row;
 }
 
 .button {
   font-weight: 400;
-  
+
   &:focus {
     background-color: var(--list-item-bg-hover) !important;
     box-shadow: inset 0 0 0 var(--border-size) var(--item-focus-border), inset 1px 0 0 1px var(--item-focus-border) ;
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index ddbd8b0d4d6aefd9b511790622fece0af8c8ee93..3c4324488bd2ae27019e3d544b0de160ea4bd8fa 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -67,6 +67,9 @@ class AudioManager {
     this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
     this.monitor = this.monitor.bind(this);
 
+    this._inputStream = null;
+    this._inputStreamTracker = new Tracker.Dependency();
+
     this.BREAKOUT_AUDIO_TRANSFER_STATES = BREAKOUT_AUDIO_TRANSFER_STATES;
   }
 
@@ -335,6 +338,7 @@ class AudioManager {
       window.parent.postMessage({ response: 'joinedAudio' }, '*');
       this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO));
       logger.info({ logCode: 'audio_joined' }, 'Audio Joined');
+      this.inputStream = (this.bridge ? this.bridge.inputStream : null);
       if (STATS.enabled) this.monitor();
       this.audioEventHandler({
         name: 'started',
@@ -357,7 +361,8 @@ class AudioManager {
     this.failedMediaElements = [];
 
     if (this.inputStream) {
-      this.inputStream.getTracks().forEach(track => track.stop());
+      this.inputStream.getTracks().forEach((track) => track.stop());
+      this.inputStream = null;
       this.inputDevice = { id: 'default' };
     }
 
@@ -498,11 +503,13 @@ class AudioManager {
   }
 
   liveChangeInputDevice(deviceId) {
-    const handleChangeInputDeviceSuccess = (inputDevice) => {
-      this.inputDevice = inputDevice;
-      return Promise.resolve(inputDevice);
-    };
-    this.bridge.liveChangeInputDevice(deviceId).then(handleChangeInputDeviceSuccess);
+    // we force stream to be null, so MutedAlert will deallocate it and
+    // a new one will be created for the new stream
+    this.inputStream = null;
+    this.bridge.liveChangeInputDevice(deviceId).then((stream) => {
+      this.setSenderTrackEnabled(!this.isMuted);
+      this.inputStream = stream;
+    });
   }
 
   async changeOutputDevice(deviceId, isLive) {
@@ -517,8 +524,19 @@ class AudioManager {
   }
 
   get inputStream() {
-    this._inputDevice.tracker.depend();
-    return (this.bridge ? this.bridge.inputStream : null);
+    this._inputStreamTracker.depend();
+    return this._inputStream;
+  }
+
+  set inputStream(stream) {
+    // We store reactive information about input stream
+    // because mutedalert component needs to track when it changes
+    // and then update hark with the new value for inputStream
+    if (this._inputStream !== stream) {
+      this._inputStreamTracker.changed();
+    }
+
+    this._inputStream = stream;
   }
 
   get inputDevice() {
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index e174c88df77a711cb1990f0d3f8d1b741838ca93..0ecd0abb1c0018d5fa0194668ff19215617fead7 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -276,7 +276,7 @@ public:
     # The algorithm names are self-explanatory.
     cameraSortingModes:
       defaultSorting: LOCAL_ALPHABETICAL
-      paginationSorting: LOCAL_PRESENTER_ALPHABETICAL
+      paginationSorting: VOICE_ACTIVITY_LOCAL
     # Entry `thresholds` is an array of:
     # - threshold: minimum number of cameras being shared for profile to applied
     #   profile: a camera profile id from the cameraProfiles configuration array
@@ -400,7 +400,6 @@ public:
     autoSwapLayout: false
     hidePresentation: false
     showParticipantsOnLogin: true
-    webcamsDefaultPlacement: 'top'
   media:
     stunTurnServersFetchAddress: '/bigbluebutton/api/stuns'
     cacheStunTurnServers: true
diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json
index 88b72067bd867ab8dd1b3da73750523b652597fd..8b1468b32c8116897f2ad72096c94821cb8337d3 100755
--- a/bigbluebutton-html5/public/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -374,7 +374,8 @@
     "app.settings.save-notification.label": "Settings have been saved",
     "app.statusNotifier.lowerHands": "Lower Hands",
     "app.statusNotifier.raisedHandsTitle": "Raised Hands",
-    "app.statusNotifier.raisedHandDesc": "{0} raised their hand",
+    "app.statusNotifier.raisedHandDesc": "{0} raised their hands",
+    "app.statusNotifier.raisedHandDescOneUser": "{0} raised hand",
     "app.statusNotifier.and": "and",
     "app.switch.onLabel": "ON",
     "app.switch.offLabel": "OFF",