diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
index 54c67fecf2083e824e013d0ef74634b2d063edbc..05c9415d49b818609ce4f0240b39cbca17455e5d 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
@@ -43,6 +43,7 @@ trait SystemConfiguration {
   lazy val voiceConfRecordCodec = Try(config.getString("voiceConf.recordCodec")).getOrElse("wav")
   lazy val checkVoiceRecordingInterval = Try(config.getInt("voiceConf.checkRecordingInterval")).getOrElse(19)
   lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43)
+  lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(false)
 
   lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
index d2d374d8aab589e6542c1a67bcd634be8ac7c53a..4a49d961f1e4155bcf0f983a75b945551e08815e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
@@ -1,15 +1,16 @@
 package org.bigbluebutton.core.apps.voice
 
+import org.bigbluebutton.SystemConfiguration
 import org.bigbluebutton.LockSettingsUtil
 import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, ConfVoiceUser, MessageTypes, Routing, UserJoinedVoiceConfToClientEvtMsg, UserJoinedVoiceConfToClientEvtMsgBody, UserLeftVoiceConfToClientEvtMsg, UserLeftVoiceConfToClientEvtMsgBody, UserMutedVoiceEvtMsg, UserMutedVoiceEvtMsgBody }
 import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
 import org.bigbluebutton.core.bus.InternalEventBus
-import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
+import org.bigbluebutton.core.models.{ Users2x, VoiceUserState, VoiceUsers }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
 import org.bigbluebutton.core2.MeetingStatus2x
 import org.bigbluebutton.core2.message.senders.MsgBuilder
 
-object VoiceApp {
+object VoiceApp extends SystemConfiguration {
 
   def genRecordPath(
       recordDir:       String,
@@ -136,6 +137,20 @@ object VoiceApp {
               // Update the user status to indicate they are still in the voice conference.
               VoiceUsers.setLastStatusUpdate(liveMeeting.voiceUsers, vu)
             }
+
+            // Purge voice users that don't have a matching user record
+            // Avoid this if the meeting is a breakout room since might be real
+            // voice users participating
+            // Also avoid ejecting if the user is dial-in (v_*)
+            if (ejectRogueVoiceUsers && !liveMeeting.props.meetingProp.isBreakout && !cvu.intId.startsWith("v_")) {
+              Users2x.findWithIntId(liveMeeting.users2x, cvu.intId) match {
+                case Some(_) =>
+                case None =>
+                  println(s"Ejecting rogue voice user. meetingId=${liveMeeting.props.meetingProp.intId} userId=${cvu.intId}")
+                  val event = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props.voiceProp.voiceConf, cvu.voiceUserId)
+                  outGW.send(event)
+              }
+            }
           case None =>
             handleUserJoinedVoiceConfEvtMsg(
               liveMeeting,
diff --git a/akka-bbb-apps/src/universal/conf/application.conf b/akka-bbb-apps/src/universal/conf/application.conf
index 564b1520387cae194d58c8f1c258467a79d7d50f..4318352efb679a5fd158b21e22b3c4728e6891f2 100755
--- a/akka-bbb-apps/src/universal/conf/application.conf
+++ b/akka-bbb-apps/src/universal/conf/application.conf
@@ -85,6 +85,8 @@ voiceConf {
   checkRecordingInterval = 23
   # Internval seconds to sync voice users status.
   syncUserStatusInterval = 41
+  # Voice users with no matching user record
+  ejectRogueVoiceUsers = true
 }
 
 recording {
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java
index 6c520c36d9c9fb25c40ab021fdf3e1f4ebc35425..1d5c6ac8ea22807d528439bbc46144b2c7f5e93c 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/GetUsersStatusCommand.java
@@ -85,10 +85,12 @@ public class GetUsersStatusCommand extends FreeswitchCommand {
                 voiceUserId = callWithSess.group(1).trim();
                 clientSession = callWithSess.group(2).trim();
                 callerIdName = callWithSess.group(3).trim();
-              } else
-              if (matcher.matches()) {
+              } else if (matcher.matches()) {
                 voiceUserId = matcher.group(1).trim();
                 callerIdName = matcher.group(2).trim();
+              } else {
+                // This is a caller using dial in or out
+                voiceUserId = "v_" + member.getId().toString();
               }
 
               log.info("Conf user. uuid=" + uuid
diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
index a91c5cc78e11cc43ad663431091f6d39a461d0ea..3f89e94ee9887cae0b4da15af4f94b538e2a27b8 100644
--- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
+++ b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js
@@ -6,7 +6,7 @@ import VoiceUsers from '/imports/api/voice-users';
 import Meetings from '/imports/api/meetings';
 import Logger from '/imports/startup/server/logger';
 
-export default function muteToggle(uId) {
+export default function muteToggle(uId, toggle) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
   const EVENT_NAME = 'MuteUserCmdMsg';
@@ -39,10 +39,18 @@ export default function muteToggle(uId) {
     }
   }
 
+  let _muted;
+
+  if ((toggle === undefined) || (toggle === null)) {
+    _muted = !muted;
+  } else {
+    _muted = !!toggle;
+  }
+
   const payload = {
     userId: userToMute,
     mutedBy: requesterUserId,
-    mute: !muted,
+    mute: _muted,
   };
 
   RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js
index 7e111df651d2a4e8147082cfcd0aeb1720a0a35e..2ba4d373b19a8a0434e1cf1c8fe86ad4f36ba12d 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/service.js
@@ -6,10 +6,41 @@ import Meetings from '/imports/api/meetings';
 import { makeCall } from '/imports/ui/services/api';
 import VoiceUsers from '/imports/api/voice-users';
 import logger from '/imports/startup/client/logger';
+import { throttle } from 'lodash';
+import Storage from '../../services/storage/session';
 
 const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
 const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
 
+const MUTED_KEY = 'muted';
+
+const recoverMicState = () => {
+  const muted = Storage.getItem(MUTED_KEY);
+
+  if ((muted === undefined) || (muted === null)) {
+    return;
+  }
+
+  logger.debug({
+    logCode: 'audio_recover_mic_state',
+  }, `Audio recover previous mic state: muted = ${muted}`);
+  makeCall('toggleVoice', null, muted);
+};
+
+const audioEventHandler = (event) => {
+  if (!event) {
+    return;
+  }
+
+  switch (event.name) {
+    case 'started':
+      recoverMicState();
+      break;
+    default:
+      break;
+  }
+};
+
 const init = (messages, intl) => {
   AudioManager.setAudioMessages(messages, intl);
   if (AudioManager.initialized) return;
@@ -33,7 +64,7 @@ const init = (messages, intl) => {
     microphoneLockEnforced,
   };
 
-  AudioManager.init(userData);
+  AudioManager.init(userData, audioEventHandler);
 };
 
 const isVoiceUser = () => {
@@ -46,6 +77,9 @@ const toggleMuteMicrophone = throttle(() => {
   const user = VoiceUsers.findOne({
     meetingId: Auth.meetingID, intId: Auth.userID,
   }, { fields: { muted: 1 } });
+
+  Storage.setItem(MUTED_KEY, !user.muted);
+
   if (user.muted) {
     logger.info({
       logCode: 'audiomanager_unmute_audio',
diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js
index 66bd3ea7ccc3d2fe70c51db90ce3a4c454af614e..9de4de3c7aa4f426542756442ce902a89c1335ec 100644
--- a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js
+++ b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js
@@ -11,7 +11,7 @@ import ReactPlayer from 'react-player';
 import Panopto from './custom-players/panopto';
 
 const isUrlValid = (url) => {
-  return ReactPlayer.canPlay(url) || Panopto.canPlay(url);
+  return /^https.*$/.test(url) && (ReactPlayer.canPlay(url) || Panopto.canPlay(url));
 }
 
 const startWatching = (url) => {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 8ff349355f90a92a7a62d0cd9894e457926e18b0..f5c020879464709dabc3c67484cceef93d6674d2 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -58,13 +58,14 @@ class AudioManager {
     this.monitor = this.monitor.bind(this);
   }
 
-  init(userData) {
+  init(userData, audioEventHandler) {
     this.bridge = new SIPBridge(userData); // no alternative as of 2019-03-08
     if (this.useKurento) {
       this.listenOnlyBridge = new KurentoBridge(userData);
     }
     this.userData = userData;
     this.initialized = true;
+    this.audioEventHandler = audioEventHandler;
   }
 
   setAudioMessages(messages, intl) {
@@ -338,6 +339,7 @@ class AudioManager {
       this.notify(this.intl.formatMessage(this.messages.info.JOINED_AUDIO));
       logger.info({ logCode: 'audio_joined' }, 'Audio Joined');
       if (STATS.enabled) this.monitor();
+      this.audioEventHandler({ name: 'started' });
     }
   }
 
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index cb90b82f13de0742821d7b945903a452caa4a7ac..98a1dda53d433f91ed6f8eb37e405723ef1749dc 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -249,7 +249,7 @@ public:
     #   profile: a camera profile id from the cameraProfiles configuration array
     #            that will be applied to all cameras when threshold is hit
     cameraQualityThresholds:
-      enabled: false
+      enabled: true
       thresholds:
         - threshold: 8
           profile: low-u8
@@ -265,7 +265,7 @@ public:
           profile: low-u30
     pagination:
       # whether to globally enable or disable pagination.
-      enabled: false
+      enabled: true
       # how long (in ms) the negotiation will be debounced after a page change.
       pageChangeDebounceTime: 2500
       # video page sizes for DESKTOP endpoints. It stands for the number of SUBSCRIBER streams.
@@ -273,11 +273,11 @@ public:
       # A page size of 0 (zero) means that the page size is unlimited (disabled).
       desktopPageSizes:
         moderator: 0
-        viewer: 5
+        viewer: 0
       # video page sizes for MOBILE endpoints
       mobilePageSizes:
-        moderator: 2
-        viewer: 2
+        moderator: 6
+        viewer: 4
   syncUsersWithConnectionManager:
     enabled: false
     syncInterval: 60000
diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
index 4e4f61a7dba1bcaaaf00c6b616cd3fd76712f8dd..9e4fd1028f418bac486f7064fd2571033cf68235 100755
--- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
+++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
@@ -212,7 +212,12 @@ class ApiController {
 
     // BEGIN - backward compatibility
     if (StringUtils.isEmpty(params.checksum)) {
-      invalid("checksumError", "You did not pass the checksum security check", REDIRECT_RESPONSE)
+      invalid("checksumError", "You did not pass the checksum security check")
+      return
+    }
+
+    if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
+      invalid("checksumError", "You did not pass the checksum security check")
       return
     }
 
@@ -244,11 +249,6 @@ class ApiController {
       return
     }
 
-    if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
-      invalid("checksumError", "You did not pass the checksum security check", REDIRECT_RESPONSE)
-      return
-    }
-
     // END - backward compatibility
 
     // Do we have a checksum? If none, complain.