diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java
index c92943eb4dca41c02f3c1d3f0c18eef612792453..d583f1eaad97ca8d8eb9445b9d3a7950e0f6f029 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ExternalProcessExecutor.java
@@ -19,66 +19,76 @@
 
 package org.bigbluebutton.presentation.imp;
 
-import java.util.Timer;
-import java.util.TimerTask;
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * A wrapper class the executes an external command. 
- * 
- * See http://kylecartmell.com/?p=9
+ * A wrapper class the executes an external command.
  * 
  * @author Richard Alam
- *
+ * @author Marcel Hellkamp
  */
 public class ExternalProcessExecutor {
 	private static Logger log = LoggerFactory.getLogger(ExternalProcessExecutor.class);
-	
-	public boolean exec(String COMMAND, long timeoutMillis) {
-        Timer timer = new Timer(false);
-        Process p = null;
-        try {
-            InterruptTimerTask interrupter = new InterruptTimerTask(Thread.currentThread());
-            timer.schedule(interrupter, timeoutMillis);
-            p = Runtime.getRuntime().exec(COMMAND);
-            int result = p.waitFor();
-            if (result == 0) {
-                return true;
-            } else {
-                return false;
-            }
+	// Replace with ProcessBuilder.Redirect.DISCARD in java 9+
+	private static File DISCARD = new File(
+			System.getProperty("os.name").startsWith("Windows") ? "NUL" : "/dev/null");
 
-        } catch(Exception e) {
-        	log.info("TIMEDOUT excuting : {}", COMMAND);
-        	if (p != null) {
-        	    p.destroy();
-        	}
-        } finally {
-            timer.cancel();     // If the process returns within the timeout period, we have to stop the interrupter
-                                // so that it does not unexpectedly interrupt some other code later.
-
-            Thread.interrupted();   // We need to clear the interrupt flag on the current thread just in case
-                                    // interrupter executed after waitFor had already returned but before timer.cancel
-                                    // took effect.
-                                    //
-                                    // Oh, and there's also Sun bug 6420270 to worry about here.
-        }  
-		return false;
+	/**
+	 * Run COMMAND for at most timeoutMillis while ignoring any output.
+	 * 
+	 * @deprecated The COMMAND string is split on whitespace to create an argument
+	 *             list. This won't work for arguments that contain whitespace. Use
+	 *             {@link #exec(List, Duration)} instead.
+	 * 
+	 * @param COMMAND       A single command or whitespace separated list of
+	 *                      arguments.
+	 * @param timeoutMillis Timeout in milliseconds.
+	 * @return true if the command terminated in time with an exit value of 0.
+	 */
+	@Deprecated
+	public boolean exec(String COMMAND, long timeoutMillis) {
+		return exec(Arrays.asList(COMMAND.split("[ \\t\\n\\r\\f]+")), Duration.ofMillis(timeoutMillis));
 	}
-	
 
-	class InterruptTimerTask extends TimerTask {
-	    private Thread thread;
+	/**
+	 * Run a command for a limited amount of time while ignoring any output.
+	 * 
+	 * @param cmd     List containing the program and its arguments.
+	 * @param timeout Maximum execution time.
+	 * @return true if the command terminated in time with an exit value of 0.
+	 */
+	public boolean exec(List<String> cmd, Duration timeout) {
 
-	    public InterruptTimerTask(Thread t) {
-	        this.thread = t;
-	    }
+		ProcessBuilder pb = new ProcessBuilder(cmd);
+		pb.redirectError(DISCARD);
+		pb.redirectOutput(DISCARD);
 
-	    public void run() {
-	        thread.interrupt();
-	    }
+		Process proc;
+		try {
+			proc = pb.start();
+		} catch (IOException e) {
+			log.error("Failed to execute: {}", String.join(" ", cmd), e);
+			return false;
+		}
 
+		try {
+			if (!proc.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
+				log.warn("TIMEDOUT excuting: {}", String.join(" ", cmd));
+				proc.destroy();
+			}
+			return !proc.isAlive() && proc.exitValue() == 0;
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			proc.destroy();
+			return false;
+		}
 	}
 }
diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release
index e248e56c569cb0a2e307ba8c771a88a371d49782..b8ca37e4f9914d60e24c3fbc86753be9348ec3ee 100644
--- a/bigbluebutton-config/bigbluebutton-release
+++ b/bigbluebutton-config/bigbluebutton-release
@@ -1 +1 @@
-BIGBLUEBUTTON_RELEASE=2.3.6
+BIGBLUEBUTTON_RELEASE=2.3.9
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
index 4141f2a6633f03f7ab90c994e1f8d78833aae11d..587a2ae08cc20db302a65ab6cf49076fa20b4894 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
@@ -277,8 +277,16 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
   }
 
   exitAudio() {
+    const mediaElement = document.getElementById(MEDIA_TAG);
+
     this.broker.stop();
     this.clearReconnectionTimeout();
+
+    if (mediaElement && typeof mediaElement.pause === 'function') {
+      mediaElement.pause();
+      mediaElement.srcObject = null;
+    }
+
     return Promise.resolve();
   }
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 67ef270c1fc7d759105af5d41e230854f9b55cc8..f3a5f329f56cabbec3b6d9839b1fff491e0b08cf 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -730,17 +730,23 @@ class SIPSession {
   }
 
   onIceGatheringStateChange(event) {
-    const secondsToGatherIce = (new Date() - this._sessionStartTime) / 1000;
-
     const iceGatheringState = event.target
       ? event.target.iceGatheringState
       : null;
 
+    if ((iceGatheringState === 'gathering') && (!this._iceGatheringStartTime)) {
+      this._iceGatheringStartTime = new Date();
+    }
+
     if (iceGatheringState === 'complete') {
+      const secondsToGatherIce = (new Date()
+        - (this._iceGatheringStartTime || this._sessionStartTime)) / 1000;
+
       logger.info({
         logCode: 'sip_js_ice_gathering_time',
         extraInfo: {
           callerIdName: this.user.callerIdName,
+          secondsToGatherIce,
         },
       }, `ICE gathering candidates took (s): ${secondsToGatherIce}`);
     }
diff --git a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js
index 1e9b6f0e0dc0270caa47d4bd273ff21f6966e797..f3c39edd06dc0f344412c35b3152cdb355ecd012 100755
--- a/bigbluebutton-html5/imports/api/breakouts/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/breakouts/server/publishers.js
@@ -45,7 +45,28 @@ function breakouts(role) {
     ],
   };
 
-  return Breakouts.find(selector);
+  const fields = {
+    fields: {
+      users: {
+        $elemMatch: {
+          // do not allow users to obtain 'redirectToHtml5JoinURL' for others
+          userId,
+        },
+      },
+      breakoutId: 1,
+      externalId: 1,
+      freeJoin: 1,
+      isDefaultName: 1,
+      joinedUsers: 1,
+      name: 1,
+      parentMeetingId: 1,
+      sequence: 1,
+      shortName: 1,
+      timeRemaining: 1,
+    },
+  };
+
+  return Breakouts.find(selector, fields);
 }
 
 function publish(...args) {
diff --git a/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js b/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js
index 0c1c923014bdd985a900a6c346ad878b867a257d..f451f0d6af0272d18b047d91cbc4bf72c1f2fa49 100644
--- a/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js
+++ b/bigbluebutton-html5/imports/api/connection-status/server/methods/addConnectionStatus.js
@@ -11,8 +11,6 @@ const logConnectionStatus = (meetingId, userId, status, type, value) => {
       Logger.info(`Connection status updated: meetingId=${meetingId} userId=${userId} status=${status} type=${type}`);
       break;
     case 'warning':
-      // Skip
-      break;
     case 'danger':
     case 'critical':
       switch (type) {
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
index 82317c35ee78610f743a9fdd8720669fdce89f8d..9b4c17639a8bdd2262241559376e63beb4adba92 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
@@ -25,7 +25,7 @@ function groupChatMsg(chatsIds) {
     timestamp: { $gte: User.authTokenValidatedTime },
     $or: [
       { meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } },
-      { chatId: { $in: chatsIds } },
+      { meetingId, chatId: { $in: chatsIds } },
     ],
   };
   return GroupChatMsg.find(selector);
diff --git a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js
index bc0b063a221c4f914dde39d4894044ba78105222..136559b95faea131d1bde3c592b36f0d447afb38 100644
--- a/bigbluebutton-html5/imports/api/guest-users/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/guest-users/server/publishers.js
@@ -1,18 +1,30 @@
 import GuestUsers from '/imports/api/guest-users/';
+import Users from '/imports/api/users';
 import { Meteor } from 'meteor/meteor';
 import Logger from '/imports/startup/server/logger';
 import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
 
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
+
 function guestUsers() {
   const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
 
   if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
-    Logger.warn(`Publishing GuestUsers was requested by unauth connection ${this.connection.id}`);
+    Logger.warn(`Publishing GuestUser was requested by unauth connection ${this.connection.id}`);
     return GuestUsers.find({ meetingId: '' });
   }
 
   const { meetingId, userId } = tokenValidation;
 
+  const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });
+  if (!User || User.role !== ROLE_MODERATOR) {
+    Logger.warn(
+      'Publishing current-poll was requested by non-moderator connection',
+      { meetingId, userId, connectionId: this.connection.id },
+    );
+    return GuestUsers.find({ meetingId: '' });
+  }
+
   Logger.debug(`Publishing GuestUsers for ${meetingId} ${userId}`);
 
   return GuestUsers.find({ meetingId });
diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
index 48e29bef3607186454f0afcc22cc73c7f927af49..9463afa4990d2a4f93e1d38447ad0cff3d32deb1 100644
--- a/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/meetingEnd.js
@@ -6,7 +6,7 @@ import Logger from '/imports/startup/server/logger';
 
 export default function handleMeetingEnd({ header, body }) {
   check(body, Object);
-  const { meetingId } = body;
+  const { meetingId, reason } = body;
   check(meetingId, String);
 
   check(header, Object);
@@ -24,7 +24,7 @@ export default function handleMeetingEnd({ header, body }) {
   };
 
   Meetings.update({ meetingId },
-    { $set: { meetingEnded: true, meetingEndedBy: userId } },
+    { $set: { meetingEnded: true, meetingEndedBy: userId, meetingEndedReason: reason } },
     (err, num) => { cb(err, num, 'Meeting'); });
 
   Breakouts.update({ parentMeetingId: meetingId },
diff --git a/bigbluebutton-html5/imports/api/polls/server/publishers.js b/bigbluebutton-html5/imports/api/polls/server/publishers.js
index 85bbd08b0612f7a57df8768241dfad3ad51939d5..620a73b22dceb5847ae7647536ecb8b2bd8ee7cc 100644
--- a/bigbluebutton-html5/imports/api/polls/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/polls/server/publishers.js
@@ -1,8 +1,10 @@
 import { Meteor } from 'meteor/meteor';
 import Logger from '/imports/startup/server/logger';
+import Users from '/imports/api/users';
 import Polls from '/imports/api/polls';
 import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
 
+const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
 function currentPoll() {
   const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
 
@@ -13,6 +15,15 @@ function currentPoll() {
 
   const { meetingId, userId } = tokenValidation;
 
+  const User = Users.findOne({ userId, meetingId }, { fields: { role: 1 } });
+  if (!User || User.role !== ROLE_MODERATOR) {
+    Logger.warn(
+      'Publishing current-poll was requested by non-moderator connection',
+      { meetingId, userId, connectionId: this.connection.id },
+    );
+    return Polls.find({ meetingId: '' });
+  }
+
   Logger.debug('Publishing Polls', { meetingId, userId });
 
   const selector = {
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
index d63005606733887c0c3558f80d594ebdcdcb0520..42ee92cbd5f7d4c77993133f4b9f9549a02b4860 100755
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -283,6 +283,8 @@ export default class KurentoScreenshareBridge {
   };
 
   stop() {
+    const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
+
     if (this.broker) {
       this.broker.stop();
       // Checks if this session is a sharer and if it's not reconnecting
@@ -292,6 +294,12 @@ export default class KurentoScreenshareBridge {
       if (this.broker.role === SEND_ROLE && !this.reconnecting) setSharingScreen(false);
       this.broker = null;
     }
+
+    if (mediaElement && typeof mediaElement.pause === 'function') {
+      mediaElement.pause();
+      mediaElement.srcObject = null;
+    }
+
     this.gdmStream = null;
     this.clearReconnectionTimeout();
   }
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 636d680f9057115ed925659551eb2ae77486997d..da45c6d635f8c20500408872daaea103b6ee078a 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -226,6 +226,7 @@ class Base extends Component {
       ejectedReason,
       meetingExist,
       meetingHasEnded,
+      meetingEndedReason,
       meetingIsBreakout,
       subscriptionsReady,
       User,
@@ -236,7 +237,7 @@ class Base extends Component {
     }
 
     if (ejected) {
-      return (<MeetingEnded code="403" reason={ejectedReason} />);
+      return (<MeetingEnded code="403" ejectedReason={ejectedReason} />);
     }
 
     if ((meetingHasEnded || User?.loggedOut) && meetingIsBreakout) {
@@ -245,7 +246,7 @@ class Base extends Component {
     }
 
     if (((meetingHasEnded && !meetingIsBreakout)) || (codeError && User?.loggedOut)) {
-      return (<MeetingEnded code={codeError} />);
+      return (<MeetingEnded code={codeError} endedReason={meetingEndedReason} ejectedReason={ejectedReason} />);
     }
 
     if (codeError && !meetingHasEnded) {
@@ -317,6 +318,7 @@ const BaseContainer = withTracker(() => {
   const meeting = Meetings.findOne({ meetingId }, {
     fields: {
       meetingEnded: 1,
+      meetingEndedReason: 1,
       meetingProp: 1,
     },
   });
@@ -328,6 +330,7 @@ const BaseContainer = withTracker(() => {
   const approved = User?.approved && User?.guest;
   const ejected = User?.ejected;
   const ejectedReason = User?.ejectedReason;
+  const meetingEndedReason = meeting?.meetingEndedReason;
 
   let userSubscriptionHandler;
 
@@ -425,6 +428,7 @@ const BaseContainer = withTracker(() => {
     isMeteorConnected: Meteor.status().connected,
     meetingExist: !!meeting,
     meetingHasEnded: !!meeting && meeting.meetingEnded,
+    meetingEndedReason,
     meetingIsBreakout: AppService.meetingIsBreakout(),
     subscriptionsReady: Session.get('subscriptionsReady'),
     loggedIn,
diff --git a/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx b/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx
index 8f2c57e9f853859b14b0749c63342e977812ce44..683092791ca0e60054423420d2d1e255fbfba174 100644
--- a/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/captions/pad/component.jsx
@@ -7,6 +7,7 @@ import Button from '/imports/ui/components/button/component';
 import logger from '/imports/startup/client/logger';
 import PadService from './service';
 import CaptionsService from '/imports/ui/components/captions/service';
+import { notify } from '/imports/ui/services/notification';
 import { styles } from './styles';
 
 const intlMessages = defineMessages({
@@ -46,6 +47,10 @@ const intlMessages = defineMessages({
     id: 'app.captions.pad.dictationOffDesc',
     description: 'Aria description for button that turns off speech recognition',
   },
+  speechRecognitionStop: {
+    id: 'app.captions.pad.speechRecognitionStop',
+    description: 'Notification for stopped speech recognition',
+  },  
 });
 
 const propTypes = {
@@ -76,11 +81,21 @@ class Pad extends PureComponent {
       listening: false,
     };
 
-    const { locale } = props;
+    const { locale, intl } = props;
     this.recognition = CaptionsService.initSpeechRecognition(locale);
 
     this.toggleListen = this.toggleListen.bind(this);
     this.handleListen = this.handleListen.bind(this);
+    
+    if (this.recognition) {
+      this.recognition.addEventListener('end', () => {
+        const { listening } = this.state;
+        if (listening) {
+          notify(intl.formatMessage(intlMessages.speechRecognitionStop), 'info', 'warning');
+          this.stopListen();
+        }
+      });
+    }
   }
 
   componentDidUpdate() {
@@ -91,8 +106,13 @@ class Pad extends PureComponent {
     } = this.props;
 
     if (this.recognition) {
+      if (ownerId !== currentUserId) {
+        this.recognition.stop();
+      } else if (this.state.listening && this.recognition.lang !== locale) {
+        this.recognition.stop();
+        this.stopListen();
+      }
       this.recognition.lang = locale;
-      if (ownerId !== currentUserId) this.recognition.stop();
     }
   }
 
@@ -168,6 +188,10 @@ class Pad extends PureComponent {
       listening: !listening,
     }, this.handleListen);
   }
+  
+  stopListen() {
+    this.setState({ listening: false });
+  }
 
   render() {
     const {
diff --git a/bigbluebutton-html5/imports/ui/components/media/component.jsx b/bigbluebutton-html5/imports/ui/components/media/component.jsx
index 4e578bb18d110f16d3525281f1e75450a4718c53..31e2fa01e2ef238311a44ec32aca7d5130b59d94 100644
--- a/bigbluebutton-html5/imports/ui/components/media/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/component.jsx
@@ -18,6 +18,7 @@ const propTypes = {
   disableVideo: PropTypes.bool,
   audioModalIsOpen: PropTypes.bool,
   layoutContextState: PropTypes.instanceOf(Object).isRequired,
+  isRTL: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -46,6 +47,7 @@ export default class Media extends Component {
       usersVideo,
       layoutContextState,
       isMeteorConnected,
+      isRTL,
     } = this.props;
 
     const { webcamsPlacement: placement } = layoutContextState;
@@ -117,6 +119,7 @@ export default class Media extends Component {
             disableVideo={disableVideo}
             audioModalIsOpen={audioModalIsOpen}
             usersVideo={usersVideo}
+            isRTL={isRTL}
           />
         ) : null}
       </div>
diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx
index 0c6cda22f901a1cf429c8d377a4f389c00636fed..0712c41549210dd6284dbdda31e7cec972850120 100755
--- a/bigbluebutton-html5/imports/ui/components/media/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx
@@ -137,6 +137,7 @@ export default withLayoutConsumer(withModalMounter(withTracker(() => {
   }
 
   data.webcamsPlacement = Storage.getItem('webcamsPlacement');
+  data.isRTL = document.documentElement.getAttribute('dir') === 'rtl';
 
   MediaContainer.propTypes = propTypes;
   return data;
diff --git a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
index bf32aa466b8feee5b09f2f25947d635ec8c052df..cc9fdeba4d866d4b4ef87fc3acff45622e3794cb 100644
--- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx
@@ -21,6 +21,7 @@ const propTypes = {
   refMediaContainer: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
   layoutContextState: PropTypes.objectOf(Object).isRequired,
   layoutContextDispatch: PropTypes.func.isRequired,
+  isRTL: PropTypes.bool.isRequired,
 };
 
 const defaultProps = {
@@ -198,12 +199,12 @@ class WebcamDraggable extends PureComponent {
   }
 
   calculatePosition() {
-    const { layoutContextState } = this.props;
+    const { layoutContextState, isRTL } = this.props;
     const { mediaBounds } = layoutContextState;
 
     const { top: mediaTop, left: mediaLeft } = mediaBounds;
     const { top: webcamsListTop, left: webcamsListLeft } = this.getWebcamsListBounds();
-    const x = webcamsListLeft - mediaLeft;
+    const x = !isRTL ? (webcamsListLeft - mediaLeft) : webcamsListLeft;
     const y = webcamsListTop - mediaTop;
     return {
       x,
@@ -282,6 +283,7 @@ class WebcamDraggable extends PureComponent {
       hideOverlay,
       disableVideo,
       audioModalIsOpen,
+      isRTL,
     } = this.props;
 
     const { isMobile } = deviceInfo;
@@ -429,7 +431,7 @@ class WebcamDraggable extends PureComponent {
           />
         </div>
         <div
-          className={dropZoneLeftClassName}
+          className={!isRTL ? dropZoneLeftClassName : dropZoneRightClassName}
           style={{
             width: '15vh',
             height: `calc(${mediaHeight}px - (15vh * 2))`,
@@ -514,7 +516,7 @@ class WebcamDraggable extends PureComponent {
           />
         </div>
         <div
-          className={dropZoneRightClassName}
+          className={!isRTL ? dropZoneRightClassName : dropZoneLeftClassName}
           style={{
             width: '15vh',
             height: `calc(${mediaHeight}px - (15vh * 2))`,
diff --git a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
index 4ff69b97cc5a5c481977833107b28e0953b76b69..05a2e4ece4ca83785f2c14518f54a8c0c49e64a0 100755
--- a/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/meeting-ended/component.jsx
@@ -40,6 +40,14 @@ const intlMessage = defineMessages({
     id: 'app.meeting.endedByUserMessage',
     description: 'message informing who ended the meeting',
   },
+  messageEndedByNoModeratorSingular: {
+    id: 'app.meeting.endedByNoModeratorMessageSingular',
+    description: 'message informing that the meeting was ended due to no moderator present (singular)',
+  },
+  messageEndedByNoModeratorPlural: {
+    id: 'app.meeting.endedByNoModeratorMessagePlural',
+    description: 'message informing that the meeting was ended due to no moderator present (plural)',
+  },
   buttonOkay: {
     id: 'app.meeting.endNotification.ok.label',
     description: 'label okay for button',
@@ -95,11 +103,13 @@ const propTypes = {
     formatMessage: PropTypes.func.isRequired,
   }).isRequired,
   code: PropTypes.string.isRequired,
-  reason: PropTypes.string,
+  ejectedReason: PropTypes.string,
+  endedReason: PropTypes.string,
 };
 
 const defaultProps = {
-  reason: null,
+  ejectedReason: null,
+  endedReason: null,
 };
 
 class MeetingEnded extends PureComponent {
@@ -123,6 +133,8 @@ class MeetingEnded extends PureComponent {
 
     const meeting = Meetings.findOne({ id: user.meetingID });
     if (meeting) {
+      this.endWhenNoModeratorMinutes = meeting.durationProps.endWhenNoModeratorDelayInMinutes;
+
       const endedBy = Users.findOne({
         userId: meeting.meetingEndedBy,
       }, { fields: { name: 1 } });
@@ -136,6 +148,7 @@ class MeetingEnded extends PureComponent {
     this.confirmRedirect = this.confirmRedirect.bind(this);
     this.sendFeedback = this.sendFeedback.bind(this);
     this.shouldShowFeedback = this.shouldShowFeedback.bind(this);
+    this.getEndingMessage = this.getEndingMessage.bind(this);
 
     AudioManager.exitAudio();
     Meteor.disconnect();
@@ -163,6 +176,26 @@ class MeetingEnded extends PureComponent {
     }
   }
 
+  getEndingMessage() {
+    const { intl, code, endedReason } = this.props;
+
+    if (endedReason && endedReason === 'ENDED_DUE_TO_NO_MODERATOR') {
+      return this.endWhenNoModeratorMinutes === 1
+        ? intl.formatMessage(intlMessage.messageEndedByNoModeratorSingular)
+        : intl.formatMessage(intlMessage.messageEndedByNoModeratorPlural, { 0: this.endWhenNoModeratorMinutes });
+    }
+
+    if (this.meetingEndedBy) {
+      return intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy });
+    }
+
+    if (intlMessage[code]) {
+      return intl.formatMessage(intlMessage[code]);
+    }
+
+    return intl.formatMessage(intlMessage[430]);
+  }
+
   sendFeedback() {
     const {
       selected,
@@ -210,19 +243,17 @@ class MeetingEnded extends PureComponent {
   }
 
   renderNoFeedback() {
-    const { intl, code, reason } = this.props;
+    const { intl, code, ejectedReason } = this.props;
 
-    const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured';
-    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage);
+    const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, no feedback configured';
+    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage);
 
     return (
       <div className={styles.parent}>
         <div className={styles.modal}>
           <div className={styles.content}>
             <h1 className={styles.title} data-test="meetingEndedModalTitle">
-              {this.meetingEndedBy
-                ? intl.formatMessage(intlMessage.messageEndedByUser, { 0: this.meetingEndedBy })
-                : intl.formatMessage(intlMessage[code] || intlMessage[430])}
+              {this.getEndingMessage()}
             </h1>
             {!allowRedirectToLogoutURL() ? null : (
               <div>
@@ -248,7 +279,7 @@ class MeetingEnded extends PureComponent {
   }
 
   renderFeedback() {
-    const { intl, code, reason } = this.props;
+    const { intl, code, ejectedReason } = this.props;
     const {
       selected,
       dispatched,
@@ -256,17 +287,15 @@ class MeetingEnded extends PureComponent {
 
     const noRating = selected <= 0;
 
-    const logMessage = reason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed';
-    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason } }, logMessage);
+    const logMessage = ejectedReason === 'user_requested_eject_reason' ? 'User removed from the meeting' : 'Meeting ended component, feedback allowed';
+    logger.info({ logCode: 'meeting_ended_code', extraInfo: { endedCode: code, reason: ejectedReason } }, logMessage);
 
     return (
       <div className={styles.parent}>
         <div className={styles.modal} data-test="meetingEndedModal">
           <div className={styles.content}>
             <h1 className={styles.title}>
-              {
-                intl.formatMessage(intlMessage[reason] || intlMessage[430])
-              }
+              {this.getEndingMessage()}
             </h1>
             <div className={styles.text}>
               {this.shouldShowFeedback()
diff --git a/bigbluebutton-html5/imports/ui/components/note/service.js b/bigbluebutton-html5/imports/ui/components/note/service.js
index 369fbba7fd435ecfa09b78268acb468bcc4f5b0d..d25cf4a46f62e54294bc4157555ac3bb5719ee7b 100644
--- a/bigbluebutton-html5/imports/ui/components/note/service.js
+++ b/bigbluebutton-html5/imports/ui/components/note/service.js
@@ -42,7 +42,7 @@ const isLocked = () => {
   const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, { fields: { 'lockSettingsProps.disableNote': 1 } });
   const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } });
 
-  if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR) {
+  if (meeting.lockSettingsProps && user.role !== ROLE_MODERATOR && user.locked) {
     return meeting.lockSettingsProps.disableNote;
   }
   return false;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 83bd7e632ea84781dfcf00713f7047b38486f4b2..e8d6ed69c2da55cb55cd2c09ae4a9326baa6720e 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -284,16 +284,19 @@ const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
 const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
 
 const isMeetingLocked = (id) => {
-  const meeting = Meetings.findOne({ meetingId: id }, { fields: { lockSettingsProps: 1 } });
+  const meeting = Meetings.findOne({ meetingId: id },
+    { fields: { lockSettingsProps: 1, usersProp: 1 } });
   let isLocked = false;
 
   if (meeting.lockSettingsProps !== undefined) {
-    const lockSettings = meeting.lockSettingsProps;
+    const {lockSettingsProps:lockSettings, usersProp} = meeting;
 
     if (lockSettings.disableCam
       || lockSettings.disableMic
       || lockSettings.disablePrivateChat
-      || lockSettings.disablePublicChat) {
+      || lockSettings.disablePublicChat
+      || lockSettings.disableNote
+      || usersProp.webcamsOnlyForModerator) {
       isLocked = true;
     }
   }
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
index 7bd1a5a9e797e715b835b7e3587e7189699f19f2..87867c96b9b744fd3945db6a421e07cf814069b7 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
@@ -17,6 +17,7 @@ import CaptionsWriterMenu from '/imports/ui/components/captions/writer-menu/cont
 import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
 import { styles } from './styles';
 import { getUserNamesLink } from '/imports/ui/components/user-list/service';
+import Settings from '/imports/ui/services/settings';
 
 const propTypes = {
   intl: PropTypes.shape({
@@ -156,16 +157,17 @@ class UserOptions extends PureComponent {
 
   onSaveUserNames() {
     const { intl, meetingName } = this.props;
+    const lang = Settings.application.locale;
     const date = new Date();
+
+    const dateString = lang ? date.toLocaleDateString(lang) : date.toLocaleDateString();
+    const timeString = lang ? date.toLocaleTimeString(lang) : date.toLocaleTimeString();
+
     getUserNamesLink(
       intl.formatMessage(intlMessages.savedNamesListTitle,
         {
           0: meetingName,
-          1: `${date.toLocaleDateString(
-            document.documentElement.lang,
-          )}:${date.toLocaleTimeString(
-            document.documentElement.lang,
-          )}`,
+          1: `${dateString}:${timeString}`,
         }),
       intl.formatMessage(intlMessages.sortedFirstNameHeading),
       intl.formatMessage(intlMessages.sortedLastNameHeading),
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index 410b992826add6d5905b97c2106081380c1ee359..ba8df05b32cd3d1484dc291e50d10845b3f32ebb 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -787,7 +787,16 @@ class VideoProvider extends Component {
   }
 
   destroyVideoTag(stream) {
-    delete this.videoTags[stream]
+    const videoElement = this.videoTags[stream];
+
+    if (videoElement == null) return;
+
+    if (typeof videoElement.pause === 'function') {
+      videoElement.pause();
+      videoElement.srcObject = null;
+    }
+
+    delete this.videoTags[stream];
   }
 
   handlePlayStop(message) {
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
index 27bdfe70c9d870e8c9c4dad5876a38cf15b1c49a..98f403b2b85b62f7c704871b9fba7fbb21d67eaa 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js
@@ -519,9 +519,14 @@ class VideoService {
   }
 
   webcamsOnlyForModerator() {
-    const m = Meetings.findOne({ meetingId: Auth.meetingID },
+    const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
       { fields: { 'usersProp.webcamsOnlyForModerator': 1 } });
-    return m?.usersProp ? m.usersProp.webcamsOnlyForModerator : false;
+    const user = Users.findOne({ userId: Auth.userID }, { fields: { locked: 1, role: 1 } });
+
+    if (meeting?.usersProp && user?.role !== ROLE_MODERATOR && user?.locked) {
+      return meeting.usersProp.webcamsOnlyForModerator;
+    }
+    return false;
   }
 
   getInfo() {
diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json
index ea3770d7f80943229dce805fd4f59d712beb498f..e03abfab98caccb56fe8355bacd2b0e986f8a8ad 100755
--- a/bigbluebutton-html5/public/locales/en.json
+++ b/bigbluebutton-html5/public/locales/en.json
@@ -49,6 +49,7 @@
     "app.captions.pad.dictationStop": "Stop dictation",
     "app.captions.pad.dictationOnDesc": "Turns speech recognition on",
     "app.captions.pad.dictationOffDesc": "Turns speech recognition off",
+    "app.captions.pad.speechRecognitionStop": "Speech recognition stopped due to the browser incompatibility or some time of silence",
     "app.textInput.sendLabel": "Send",
     "app.note.title": "Shared Notes",
     "app.note.label": "Note",
@@ -140,6 +141,8 @@
     "app.meeting.meetingTimeRemaining": "Meeting time remaining: {0}",
     "app.meeting.meetingTimeHasEnded": "Time ended. Meeting will close soon",
     "app.meeting.endedByUserMessage": "This session was ended by {0}",
+    "app.meeting.endedByNoModeratorMessageSingular": "The meeting has ended due to no moderator being present after one minute",
+    "app.meeting.endedByNoModeratorMessagePlural": "The meeting has ended due to no moderator being present after {0} minutes",
     "app.meeting.endedMessage": "You will be forwarded back to the home screen",
     "app.meeting.alertMeetingEndsUnderMinutesSingular": "Meeting is closing in one minute.",
     "app.meeting.alertMeetingEndsUnderMinutesPlural": "Meeting is closing in {0} minutes.",
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 422db063a1d5dfdc50c3313d693554757fd967e0..c3f4e7c2680b59611164a7dc620da2b801a456e0 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -191,8 +191,8 @@ authenticatedGuest=true
 #
 # native2ascii -encoding UTF8 bigbluebutton.properties bigbluebutton.properties
 #
-defaultWelcomeMessage=Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="https://www.bigbluebutton.org/html5"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the phone button.  Use a headset to avoid causing background noise for others.
-defaultWelcomeMessageFooter=This server is running <a href="https://docs.bigbluebutton.org/" target="_blank"><u>BigBlueButton</u></a>.
+defaultWelcomeMessage=Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="https://bigbluebutton.org/html5"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the phone button.  Use a headset to avoid causing background noise for others.
+defaultWelcomeMessageFooter=This server is running <a href="https://bigbluebutton.org/" target="_blank"><u>BigBlueButton</u></a>.
 
 # Default maximum number of users a meeting can have.
 # Current default is 0 (meeting doesn't have a user limit).