diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermisssionCheck.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermissionCheck.scala
similarity index 95%
rename from akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermisssionCheck.scala
rename to akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermissionCheck.scala
index 9c6f0e5757b80f134caa99425f24ecab546bb4bc..c8a2fe75b763cf75f1cfbcc57611a7eeb0c7b8c5 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermisssionCheck.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PermissionCheck.scala
@@ -4,7 +4,7 @@ import org.bigbluebutton.core.models.{ Roles, UserState, Users2x }
 import org.bigbluebutton.core.running.OutMsgRouter
 import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
 
-object PermisssionCheck {
+object PermissionCheck {
 
   val MOD_LEVEL = 100
   val AUTHED_LEVEL = 50
@@ -47,7 +47,6 @@ object PermisssionCheck {
 
         println("PERMLEVELCHECK = " + permLevelCheck + " ROLELEVELCHECK=" + roleLevelCheck)
         permLevelCheck && roleLevelCheck
-        false
       case None => false
     }
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala
index 10c7e629bd4dac42f7d43eaf722f6ec42bb25db1..b870a29a79f53560a20d27f3e4e18d27619b1171 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ClearPublicChatHistoryPubMsgHdlr.scala
@@ -4,8 +4,10 @@ import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.apps.ChatModel
 import org.bigbluebutton.core.bus.MessageBus
 import org.bigbluebutton.core.running.{ LiveMeeting, LogHelper }
+import org.bigbluebutton.SystemConfiguration
+import org.bigbluebutton.core.apps.PermissionCheck
 
-trait ClearPublicChatHistoryPubMsgHdlr extends LogHelper {
+trait ClearPublicChatHistoryPubMsgHdlr extends LogHelper with SystemConfiguration {
   def handle(msg: ClearPublicChatHistoryPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
     def broadcastEvent(msg: ClearPublicChatHistoryPubMsg): Unit = {
       val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId)
@@ -18,8 +20,14 @@ trait ClearPublicChatHistoryPubMsgHdlr extends LogHelper {
       bus.outGW.send(msgEvent)
     }
 
-    ChatModel.clearPublicChatHistory(liveMeeting.chatModel)
-    broadcastEvent(msg)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to clear chat in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW)
+    } else {
+      ChatModel.clearPublicChatHistory(liveMeeting.chatModel)
+      broadcastEvent(msg)
+    }
   }
 
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/layout/BroadcastLayoutMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/layout/BroadcastLayoutMsgHdlr.scala
index 62623c2691f77d9f37ace2a9b38f9860066c89a3..5b2a09ef40114dae5bc224ff4ba2e45d01944e9e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/layout/BroadcastLayoutMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/layout/BroadcastLayoutMsgHdlr.scala
@@ -4,16 +4,24 @@ import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.Layouts
 import org.bigbluebutton.core.running.OutMsgRouter
 import org.bigbluebutton.core2.MeetingStatus2x
+import org.bigbluebutton.core.apps.PermissionCheck
+import org.bigbluebutton.SystemConfiguration
 
-trait BroadcastLayoutMsgHdlr {
+trait BroadcastLayoutMsgHdlr extends SystemConfiguration {
   this: LayoutApp2x =>
 
   val outGW: OutMsgRouter
 
   def handleBroadcastLayoutMsg(msg: BroadcastLayoutMsg): Unit = {
-    Layouts.setCurrentLayout(liveMeeting.layouts, msg.body.layout, msg.header.userId)
-
-    sendBroadcastLayoutEvtMsg(msg.header.userId)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to broadcast layout to meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      Layouts.setCurrentLayout(liveMeeting.layouts, msg.body.layout, msg.header.userId)
+
+      sendBroadcastLayoutEvtMsg(msg.header.userId)
+    }
   }
 
   def sendBroadcastLayoutEvtMsg(fromUserId: String): Unit = {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AddUserToPresenterGroupCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AddUserToPresenterGroupCmdMsgHdlr.scala
old mode 100644
new mode 100755
index 112c5a2bc8cc7c62f5e32bee7bee17c8fde26d18..8d664ebc520bd1e3b130b6b2034a7cf12e885939
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AddUserToPresenterGroupCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AddUserToPresenterGroupCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.{ Roles, Users2x }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait AddUserToPresenterGroupCmdMsgHdlr {
   this: UsersApp =>
@@ -23,15 +24,21 @@ trait AddUserToPresenterGroupCmdMsgHdlr {
       outGW.send(msgEvent)
     }
 
-    val userId = msg.body.userId
-    val requesterId = msg.body.requesterId
-
-    for {
-      requester <- Users2x.findWithIntId(liveMeeting.users2x, requesterId)
-    } yield {
-      if (requester.role == Roles.MODERATOR_ROLE) {
-        Users2x.addUserToPresenterGroup(liveMeeting.users2x, userId)
-        broadcastAddUserToPresenterGroup(liveMeeting.props.meetingProp.intId, userId, requesterId)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to add user to presenter group."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      val userId = msg.body.userId
+      val requesterId = msg.body.requesterId
+
+      for {
+        requester <- Users2x.findWithIntId(liveMeeting.users2x, requesterId)
+      } yield {
+        if (requester.role == Roles.MODERATOR_ROLE) {
+          Users2x.addUserToPresenterGroup(liveMeeting.users2x, userId)
+          broadcastAddUserToPresenterGroup(liveMeeting.props.meetingProp.intId, userId, requesterId)
+        }
       }
     }
   }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AssignPresenterReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AssignPresenterReqMsgHdlr.scala
index c6509ad3af347cf288fe0e5e40d6a1e427dc2140..dbe2634af616fce0afe2dfe4142deff242083eb6 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AssignPresenterReqMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/AssignPresenterReqMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.{ UserState, Users2x }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait AssignPresenterReqMsgHdlr {
   this: UsersApp =>
@@ -44,18 +45,24 @@ trait AssignPresenterReqMsgHdlr {
       outGW.send(msgEventAssign)
     }
 
-    for {
-      oldPres <- Users2x.findPresenter(this.liveMeeting.users2x)
-    } yield {
-      Users2x.makeNotPresenter(this.liveMeeting.users2x, oldPres.intId)
-      broadcastOldPresenterChange(oldPres)
-    }
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to change presenter in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      for {
+        oldPres <- Users2x.findPresenter(this.liveMeeting.users2x)
+      } yield {
+        Users2x.makeNotPresenter(this.liveMeeting.users2x, oldPres.intId)
+        broadcastOldPresenterChange(oldPres)
+      }
 
-    for {
-      newPres <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.newPresenterId)
-    } yield {
-      Users2x.makePresenter(this.liveMeeting.users2x, newPres.intId)
-      broadcastNewPresenterChange(newPres)
+      for {
+        newPres <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.newPresenterId)
+      } yield {
+        Users2x.makePresenter(this.liveMeeting.users2x, newPres.intId)
+        broadcastNewPresenterChange(newPres)
+      }
     }
   }
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala
index b43ee36a44578544ed14b45c1b1d762c5b024783..7de83f9646fed65a566a4e09d7469f791ac69f5d 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeLockSettingsInMeetingCmdMsgHdlr.scala
@@ -3,7 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.SystemConfiguration
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.api.Permissions
-import org.bigbluebutton.core.apps.PermisssionCheck
+import org.bigbluebutton.core.apps.PermissionCheck
 import org.bigbluebutton.core.running.{ OutMsgRouter }
 import org.bigbluebutton.core.running.MeetingActor
 import org.bigbluebutton.core2.MeetingStatus2x
@@ -19,15 +19,15 @@ trait ChangeLockSettingsInMeetingCmdMsgHdlrCheckPerm
   val outGW: OutMsgRouter
 
   override def handleSetLockSettings(msg: ChangeLockSettingsInMeetingCmdMsg): Unit = {
-    val isAllowed = PermisssionCheck.isAllowed(
-      PermisssionCheck.MOD_LEVEL,
-      PermisssionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.body.setBy
+    val isAllowed = PermissionCheck.isAllowed(
+      PermissionCheck.MOD_LEVEL,
+      PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
     )
 
     if (applyPermissionCheck && !isAllowed) {
       val meetingId = liveMeeting.props.meetingProp.intId
       val reason = "No permission to change lock settings"
-      PermisssionCheck.ejectUserForFailedPermission(meetingId, msg.body.setBy, reason, outGW)
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.body.setBy, reason, outGW)
     } else {
       super.handleSetLockSettings(msg)
     }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala
index f8ed292a05738cda7450ccec579b44168a03f25e..aef749dc67e94aef5ba7027151f1dfd159db17d3 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserEmojiCmdMsgHdlr.scala
@@ -3,8 +3,10 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.Users2x
 import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
+import org.bigbluebutton.SystemConfiguration
 
-trait ChangeUserEmojiCmdMsgHdlr {
+trait ChangeUserEmojiCmdMsgHdlr extends SystemConfiguration {
   this: BaseMeetingActor =>
 
   val liveMeeting: LiveMeeting
@@ -12,10 +14,16 @@ trait ChangeUserEmojiCmdMsgHdlr {
 
   def handleChangeUserEmojiCmdMsg(msg: ChangeUserEmojiCmdMsg) {
     log.debug("handling " + msg)
-    for {
-      uvo <- Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, msg.body.emoji)
-    } yield {
-      sendUserEmojiChangedEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji)
+    if (msg.header.userId != msg.body.userId && applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to clear chat in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      for {
+        uvo <- Users2x.setEmojiStatus(liveMeeting.users2x, msg.body.userId, msg.body.emoji)
+      } yield {
+        sendUserEmojiChangedEvtMsg(outGW, liveMeeting.props.meetingProp.intId, msg.body.userId, msg.body.emoji)
+      }
     }
   }
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRoleCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRoleCmdMsgHdlr.scala
index bd2fec1ca763762cac4bac4b6ea4b6f93e11b681..ad92dcde3790167c75b3ba71941e34cab9c426c6 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRoleCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/ChangeUserRoleCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.{ Roles, Users2x }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait ChangeUserRoleCmdMsgHdlr {
   this: UsersApp =>
@@ -11,15 +12,21 @@ trait ChangeUserRoleCmdMsgHdlr {
   val outGW: OutMsgRouter
 
   def handleChangeUserRoleCmdMsg(msg: ChangeUserRoleCmdMsg) {
-    for {
-      uvo <- Users2x.changeRole(liveMeeting.users2x, msg.body.userId, msg.body.role)
-    } yield {
-      val userRole = if (uvo.role == Roles.MODERATOR_ROLE) "MODERATOR" else "VIEWER"
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to change user role in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      for {
+        uvo <- Users2x.changeRole(liveMeeting.users2x, msg.body.userId, msg.body.role)
+      } yield {
+        val userRole = if (uvo.role == Roles.MODERATOR_ROLE) "MODERATOR" else "VIEWER"
 
-      val event = buildUserRoleChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId,
-        msg.body.changedBy, userRole)
+        val event = buildUserRoleChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.userId,
+          msg.body.changedBy, userRole)
 
-      outGW.send(event)
+        outGW.send(event)
+      }
     }
   }
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectUserFromMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectUserFromMeetingCmdMsgHdlr.scala
index ce9bbaebe0c3781611890feba1c98f4fd1ff79cc..3706a2110de99f01f81a61f86f2a5835eaaed06e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectUserFromMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/EjectUserFromMeetingCmdMsgHdlr.scala
@@ -4,6 +4,7 @@ import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models._
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
 import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait EjectUserFromMeetingCmdMsgHdlr {
   this: UsersApp =>
@@ -12,47 +13,53 @@ trait EjectUserFromMeetingCmdMsgHdlr {
   val outGW: OutMsgRouter
 
   def handleEjectUserFromMeetingCmdMsg(msg: EjectUserFromMeetingCmdMsg) {
-    for {
-      user <- Users2x.ejectFromMeeting(liveMeeting.users2x, msg.body.userId)
-    } yield {
-      RegisteredUsers.remove(msg.body.userId, liveMeeting.registeredUsers)
-      val reason = "user ejected by another user"
-      // send a message to client
-      Sender.sendUserEjectedFromMeetingClientEvtMsg(
-        liveMeeting.props.meetingProp.intId,
-        user.intId, msg.body.ejectedBy, reason, outGW
-      )
-
-      log.info("Ejecting user from meeting (client msg).  meetingId=" + liveMeeting.props.meetingProp.intId +
-        " userId=" + msg.body.userId)
-
-      // send a system message to force disconnection
-      Sender.sendUserEjectedFromMeetingSystemMsg(
-        liveMeeting.props.meetingProp.intId,
-        user.intId, msg.body.ejectedBy, outGW
-      )
-
-      log.info("Ejecting user from meeting (system msg).  meetingId=" + liveMeeting.props.meetingProp.intId +
-        " userId=" + msg.body.userId)
-
-      // send a user left event for the clients to update
-      val userLeftMeetingEvent = MsgBuilder.buildUserLeftMeetingEvtMsg(liveMeeting.props.meetingProp.intId, user.intId)
-      outGW.send(userLeftMeetingEvent)
-      log.info("User left meetingId=" + liveMeeting.props.meetingProp.intId + " userId=" + msg.body.userId)
-
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to eject user from meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
       for {
-        vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
+        user <- Users2x.ejectFromMeeting(liveMeeting.users2x, msg.body.userId)
       } yield {
-        val ejectFromVoiceEvent = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(
+        RegisteredUsers.remove(msg.body.userId, liveMeeting.registeredUsers)
+        val reason = "user ejected by another user"
+        // send a message to client
+        Sender.sendUserEjectedFromMeetingClientEvtMsg(
           liveMeeting.props.meetingProp.intId,
-          liveMeeting.props.voiceProp.voiceConf, vu.voiceUserId
+          user.intId, msg.body.ejectedBy, reason, outGW
         )
-        outGW.send(ejectFromVoiceEvent)
-        log.info("Ejecting user from voice.  meetingId=" + liveMeeting.props.meetingProp.intId + " userId=" + vu.intId)
-      }
 
-      if (user.presenter) {
-        automaticallyAssignPresenter(outGW, liveMeeting)
+        log.info("Ejecting user from meeting (client msg).  meetingId=" + liveMeeting.props.meetingProp.intId +
+          " userId=" + msg.body.userId)
+
+        // send a system message to force disconnection
+        Sender.sendUserEjectedFromMeetingSystemMsg(
+          liveMeeting.props.meetingProp.intId,
+          user.intId, msg.body.ejectedBy, outGW
+        )
+
+        log.info("Ejecting user from meeting (system msg).  meetingId=" + liveMeeting.props.meetingProp.intId +
+          " userId=" + msg.body.userId)
+
+        // send a user left event for the clients to update
+        val userLeftMeetingEvent = MsgBuilder.buildUserLeftMeetingEvtMsg(liveMeeting.props.meetingProp.intId, user.intId)
+        outGW.send(userLeftMeetingEvent)
+        log.info("User left meetingId=" + liveMeeting.props.meetingProp.intId + " userId=" + msg.body.userId)
+
+        for {
+          vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
+        } yield {
+          val ejectFromVoiceEvent = MsgBuilder.buildEjectUserFromVoiceConfSysMsg(
+            liveMeeting.props.meetingProp.intId,
+            liveMeeting.props.voiceProp.voiceConf, vu.voiceUserId
+          )
+          outGW.send(ejectFromVoiceEvent)
+          log.info("Ejecting user from voice.  meetingId=" + liveMeeting.props.meetingProp.intId + " userId=" + vu.intId)
+        }
+
+        if (user.presenter) {
+          automaticallyAssignPresenter(outGW, liveMeeting)
+        }
       }
     }
   }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserInMeetingCmdMsgHdlr.scala
index aca476f3fc45f61accae57e765b7d344488a1c61..5c87548eab52b707ff2abb73599c7ffa88cbb3d4 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserInMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUserInMeetingCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.Users2x
 import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait LockUserInMeetingCmdMsgHdlr {
   this: MeetingActor =>
@@ -21,12 +22,18 @@ trait LockUserInMeetingCmdMsgHdlr {
       BbbCommonEnvCoreMsg(envelope, event)
     }
 
-    for {
-      uvo <- Users2x.setUserLocked(liveMeeting.users2x, msg.body.userId, msg.body.lock)
-    } yield {
-      log.info("Lock user.  meetingId=" + props.meetingProp.intId + " userId=" + uvo.intId + " locked=" + uvo.locked)
-      val event = build(props.meetingProp.intId, uvo.intId, msg.body.lockedBy, uvo.locked)
-      outGW.send(event)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to lock user in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      for {
+        uvo <- Users2x.setUserLocked(liveMeeting.users2x, msg.body.userId, msg.body.lock)
+      } yield {
+        log.info("Lock user.  meetingId=" + props.meetingProp.intId + " userId=" + uvo.intId + " locked=" + uvo.locked)
+        val event = build(props.meetingProp.intId, uvo.intId, msg.body.lockedBy, uvo.locked)
+        outGW.send(event)
+      }
     }
   }
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUsersInMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUsersInMeetingCmdMsgHdlr.scala
index 9cbceb7c04231bc241d48c8bb00713db7e0d25a4..518e3520ec2237c22a194b6de5d083ab52d57d5e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUsersInMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LockUsersInMeetingCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.Users2x
 import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait LockUsersInMeetingCmdMsgHdlr {
   this: MeetingActor =>
@@ -21,16 +22,21 @@ trait LockUsersInMeetingCmdMsgHdlr {
       BbbCommonEnvCoreMsg(envelope, event)
     }
 
-    val usersToLock = Users2x.findAll(liveMeeting.users2x).filter(u => !msg.body.except.toSet(u))
-    usersToLock foreach { utl =>
-      for {
-        uvo <- Users2x.setUserLocked(liveMeeting.users2x, utl.intId, msg.body.lock)
-      } yield {
-        log.info("Lock user.  meetingId=" + props.meetingProp.intId + " userId=" + uvo.intId + " locked=" + uvo.locked)
-        val event = build(props.meetingProp.intId, uvo.intId, msg.body.lockedBy, uvo.locked)
-        outGW.send(event)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to lock users in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      val usersToLock = Users2x.findAll(liveMeeting.users2x).filter(u => !msg.body.except.toSet(u))
+      usersToLock foreach { utl =>
+        for {
+          uvo <- Users2x.setUserLocked(liveMeeting.users2x, utl.intId, msg.body.lock)
+        } yield {
+          log.info("Lock user.  meetingId=" + props.meetingProp.intId + " userId=" + uvo.intId + " locked=" + uvo.locked)
+          val event = build(props.meetingProp.intId, uvo.intId, msg.body.lockedBy, uvo.locked)
+          outGW.send(event)
+        }
       }
     }
-
   }
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
index 7c7dcc45188b120ac86e88f999d32ebf1b02edc4..d1a4e6a7bbfe81ccc2d4a32d94645dc89b9d1577 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
@@ -5,6 +5,7 @@ import org.bigbluebutton.core.bus.InternalEventBus
 import org.bigbluebutton.core.domain.{ MeetingEndReason, MeetingState2x }
 import org.bigbluebutton.core.models.{ Roles, Users2x }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait LogoutAndEndMeetingCmdMsgHdlr {
   this: UsersApp =>
@@ -14,14 +15,20 @@ trait LogoutAndEndMeetingCmdMsgHdlr {
   val eventBus: InternalEventBus
 
   def handleLogoutAndEndMeetingCmdMsg(msg: LogoutAndEndMeetingCmdMsg, state: MeetingState2x): Unit = {
-    for {
-      u <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
-    } yield {
-      if (u.role == Roles.MODERATOR_ROLE) {
-        endAllBreakoutRooms(eventBus, liveMeeting, state)
-        log.info("Meeting {} ended by user [{}, {}} when logging out.", liveMeeting.props.meetingProp.intId,
-          u.intId, u.name)
-        sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_AFTER_USER_LOGGED_OUT, eventBus, outGW, liveMeeting)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to end meeting on logout."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      for {
+        u <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
+      } yield {
+        if (u.role == Roles.MODERATOR_ROLE) {
+          endAllBreakoutRooms(eventBus, liveMeeting, state)
+          log.info("Meeting {} ended by user [{}, {}} when logging out.", liveMeeting.props.meetingProp.intId,
+            u.intId, u.name)
+          sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_AFTER_USER_LOGGED_OUT, eventBus, outGW, liveMeeting)
+        }
       }
     }
   }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
index 3f1709c6ca62b667e907073ce21998500ca8f2ec..513c158fe580e335aedfbcb8d7d2302f9d64bdd5 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MuteUserCmdMsgHdlr.scala
@@ -2,7 +2,7 @@ package org.bigbluebutton.core.apps.users
 
 import org.bigbluebutton.SystemConfiguration
 import org.bigbluebutton.common2.msgs.MuteUserCmdMsg
-import org.bigbluebutton.core.apps.PermisssionCheck
+import org.bigbluebutton.core.apps.PermissionCheck
 import org.bigbluebutton.core.models.VoiceUsers
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
 import org.bigbluebutton.core2.message.senders.MsgBuilder
@@ -22,15 +22,15 @@ trait MuteUserCmdMsgHdlrPermCheck extends MuteUserCmdMsgHdlrDefault with SystemC
   override def handleMuteUserCmdMsg(msg: MuteUserCmdMsg): Unit = {
     println("**************** MuteUserCmdMsgHdlrPermCheck ")
 
-    val isAllowed = PermisssionCheck.isAllowed(
-      PermisssionCheck.MOD_LEVEL,
-      PermisssionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.body.mutedBy
+    val isAllowed = PermissionCheck.isAllowed(
+      PermissionCheck.MOD_LEVEL,
+      PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
     )
 
-    if (applyPermissionCheck && !isAllowed) {
+    if (msg.header.userId != msg.body.userId && applyPermissionCheck && !isAllowed) {
       val meetingId = liveMeeting.props.meetingProp.intId
       val reason = "No permission to mute user in meeting."
-      PermisssionCheck.ejectUserForFailedPermission(meetingId, msg.body.mutedBy, reason, outGW)
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.body.mutedBy, reason, outGW)
     } else {
       super.handleMuteUserCmdMsg(msg)
     }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RemoveUserFromPresenterGroupCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RemoveUserFromPresenterGroupCmdMsgHdlr.scala
old mode 100644
new mode 100755
index 0ea4b02f66a936e9019cd831339c182955e69ab6..f7e8281cb855546eb40be6c8bbc57b9e92e2a46c
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RemoveUserFromPresenterGroupCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RemoveUserFromPresenterGroupCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.models.{ Roles, Users2x }
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait RemoveUserFromPresenterGroupCmdMsgHdlr {
   this: UsersApp =>
@@ -23,15 +24,21 @@ trait RemoveUserFromPresenterGroupCmdMsgHdlr {
       outGW.send(msgEvent)
     }
 
-    val userId = msg.body.userId
-    val requesterId = msg.body.requesterId
-
-    for {
-      requester <- Users2x.findWithIntId(liveMeeting.users2x, requesterId)
-    } yield {
-      if (requester.role == Roles.MODERATOR_ROLE) {
-        Users2x.removeUserFromPresenterGroup(liveMeeting.users2x, userId)
-        broadcastRemoveUserFromPresenterGroup(liveMeeting.props.meetingProp.intId, userId, requesterId)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to remove user from presenter group."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      val userId = msg.body.userId
+      val requesterId = msg.body.requesterId
+
+      for {
+        requester <- Users2x.findWithIntId(liveMeeting.users2x, requesterId)
+      } yield {
+        if (requester.role == Roles.MODERATOR_ROLE) {
+          Users2x.removeUserFromPresenterGroup(liveMeeting.users2x, userId)
+          broadcastRemoveUserFromPresenterGroup(liveMeeting.props.meetingProp.intId, userId, requesterId)
+        }
       }
     }
   }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala
index 135285709243597b4d800dcc86ffdf4961187b3f..ed4083b1f8f81b40ea9ed766f80f8ad9ce45ad58 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/SetRecordingStatusCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.users
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
 import org.bigbluebutton.core2.MeetingStatus2x
+import org.bigbluebutton.core.apps.PermissionCheck
 
 trait SetRecordingStatusCmdMsgHdlr {
   this: UsersApp =>
@@ -12,16 +13,23 @@ trait SetRecordingStatusCmdMsgHdlr {
 
   def handleSetRecordingStatusCmdMsg(msg: SetRecordingStatusCmdMsg) {
     log.info("Change recording status. meetingId=" + liveMeeting.props.meetingProp.intId + " recording=" + msg.body.recording)
-    if (liveMeeting.props.recordProp.allowStartStopRecording &&
-      MeetingStatus2x.isRecording(liveMeeting.status) != msg.body.recording) {
-      if (msg.body.recording) {
-        MeetingStatus2x.recordingStarted(liveMeeting.status)
-      } else {
-        MeetingStatus2x.recordingStopped(liveMeeting.status)
-      }
 
-      val event = buildRecordingStatusChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.recording)
-      outGW.send(event)
+    if (applyPermissionCheck && !PermissionCheck.isAllowed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
+      val meetingId = liveMeeting.props.meetingProp.intId
+      val reason = "No permission to clear chat in meeting."
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW)
+    } else {
+      if (liveMeeting.props.recordProp.allowStartStopRecording &&
+        MeetingStatus2x.isRecording(liveMeeting.status) != msg.body.recording) {
+        if (msg.body.recording) {
+          MeetingStatus2x.recordingStarted(liveMeeting.status)
+        } else {
+          MeetingStatus2x.recordingStopped(liveMeeting.status)
+        }
+
+        val event = buildRecordingStatusChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.recording)
+        outGW.send(event)
+      }
     }
 
     def buildRecordingStatusChangedEvtMsg(meetingId: String, userId: String, recording: Boolean): BbbCommonEnvCoreMsg = {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
index 493cb562942eb1a8639a5a280d944b0238fc3488..ae1af3e3844ff5605e197bbad107cb47ebb8d1f6 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/MuteMeetingCmdMsgHdlr.scala
@@ -2,7 +2,7 @@ package org.bigbluebutton.core2.message.handlers
 
 import org.bigbluebutton.SystemConfiguration
 import org.bigbluebutton.common2.msgs._
-import org.bigbluebutton.core.apps.PermisssionCheck
+import org.bigbluebutton.core.apps.PermissionCheck
 import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
 import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
 import org.bigbluebutton.core2.MeetingStatus2x
@@ -17,15 +17,15 @@ trait MuteMeetingCmdMsgHdlrCheckPerm extends MuteMeetingCmdMsgHdlrDefault with S
   val outGW: OutMsgRouter
 
   override def handleMuteMeetingCmdMsg(msg: MuteMeetingCmdMsg): Unit = {
-    val isAllowed = PermisssionCheck.isAllowed(
-      PermisssionCheck.MOD_LEVEL,
-      PermisssionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.body.mutedBy
+    val isAllowed = PermissionCheck.isAllowed(
+      PermissionCheck.MOD_LEVEL,
+      PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
     )
 
     if (applyPermissionCheck && !isAllowed) {
       val meetingId = liveMeeting.props.meetingProp.intId
       val reason = "No permission to mute meeting."
-      PermisssionCheck.ejectUserForFailedPermission(meetingId, msg.body.mutedBy, reason, outGW)
+      PermissionCheck.ejectUserForFailedPermission(meetingId, msg.body.mutedBy, reason, outGW)
     } else {
       super.handleMuteMeetingCmdMsg(msg)
     }
diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java
index f59b595fb0ce3d82bf56944ed0c5527aecefd060..4794a85500020c1697ebd103809a1da5bb65f493 100755
--- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java
+++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/PresentationUrlDownloadService.java
@@ -144,7 +144,8 @@ public class PresentationUrlDownloadService {
             }
         }
 
-        processUploadedFile("THREE", destinationMeetingId, presId, "default-"
+        // Hardcode pre-uploaded presentation for breakout room to the default presentation window
+        processUploadedFile("DEFAULT_PRESENTATION_POD", destinationMeetingId, presId, "default-"
                 + presentationSlide.toString() + "." + filenameExt,
                 newPresentation, true);
     }
diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
index e7c446919b362ec37878e6aa44044f4c8ed06e23..ff147e44924acdbc94b8271e775da10637e29d8b 100755
--- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
+++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/meeting/OldMeetingMsgHdlrActor.scala
@@ -48,8 +48,6 @@ class OldMeetingMsgHdlrActor(val olgMsgGW: OldMessageReceivedGW)
   }
 
   def handleRecordingChapterBreakSysMsg(msg: RecordingChapterBreakSysMsg): Unit = {
-    println("*************!!!!!!!!!!!!!! HANDLE RECORDING CHAPTER BREAK !!!!!!!!!!!!!!!")
-
     olgMsgGW.handle(new RecordChapterBreak(msg.body.meetingId, msg.body.timestamp))
   }
 
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar
index 6e47da1b9c487fbe8a0b47643c75111c9ff8cd69..4b256148829d6ecd5bbc21ec46b847955212b6ea 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar
index d7afa49c12551564a5316882bea744b2d03de502..c1352f988803561f26f0cf143f2caeed66587c0b 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar
index aba354529523e4e9be32b8514782d3c525aab061..c7f70e4361d203c85f5f34c2ae97b72e42557a0f 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar
index 08f3de1035fcdc49fa2f20e7d6ff3ee6303577ef..fc66280b36b0b5ef8eca3fa47f34fda0462bb48f 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg.jar b/bbb-screenshare/app/jws/lib/ffmpeg.jar
index 74a76e65086bc8b784ba8204ae3778a357a8a8d0..d7cdcaebc3485cae63174913a1d55a8b348e72bd 100755
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg.jar and b/bbb-screenshare/app/jws/lib/ffmpeg.jar differ
diff --git a/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar b/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar
index ddfa8a89108c115b3b30fad54a4c98dd85851288..90fd2843fd81898e0bf1c2b82afa268d1847ca72 100755
Binary files a/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar and b/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar
index 5dbdc8e794c60a6dbcd3bd4dc7dde3d050e3e402..152c2809cbd352e39ddad781b70eeeeef9f63026 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar
index 9423d37bda46152ded9faee813a139fb210f0444..5e3f3f20f16355efa4202c272a91674963db7441 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar
index 6a3463a45ba98e5a99600071773f5a7a3b74dcc9..72ac594607242c505ffe1e28365fb1b8c6cc051f 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar
index 2c90fec4215c695370e23e93b7488e8b5090dca9..27399a878e24ab148670ae3ec2c11ff02476bee9 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar differ
diff --git a/bigbluebutton-client/build.xml b/bigbluebutton-client/build.xml
index 49afe82cf197a7a0e7d9255be4b33bade178157e..b7281c804ad2838697ea6214a873aa8a4a72aa1d 100755
--- a/bigbluebutton-client/build.xml
+++ b/bigbluebutton-client/build.xml
@@ -418,6 +418,7 @@
 			<fileset dir="${PROD_RESOURCES_DIR}/help" />
 		</copy>
 		<copy file="${PROD_RESOURCES_DIR}/BigBlueButtonTest.html" todir="${OUTPUT_DIR}" overwrite="true" />
+		<copy file="${PROD_RESOURCES_DIR}/guest-wait.html" todir="${OUTPUT_DIR}" overwrite="true" />
 		<copy file="${PROD_RESOURCES_DIR}/BigBlueButton.html" todir="${OUTPUT_DIR}" overwrite="true" />
 		<copy file="${PROD_RESOURCES_DIR}/ScreenshareStandalone.html" todir="${OUTPUT_DIR}" overwrite="true" />
 		<copy file="${PROD_RESOURCES_DIR}/get_flash_player.gif" todir="${OUTPUT_DIR}" overwrite="true" />
diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html
index 15a8c952d1c11d41806e92b88b59900173be631d..5c6b3f68a54fbd296f8269d84ebcbbb46bfdff32 100755
--- a/bigbluebutton-client/resources/prod/BigBlueButton.html
+++ b/bigbluebutton-client/resources/prod/BigBlueButton.html
@@ -79,7 +79,7 @@
       } else {
         params.wmode = "window";
       }
-      params.allowscriptaccess = "true";
+      params.allowscriptaccess = "always";
       params.seamlesstabbing = "true";
       var attributes = {};
       attributes.id = "BigBlueButton";
@@ -98,7 +98,7 @@
         var fillContent = function(){
           var content = document.getElementById("content");
           if (content) {
-            content.innerHTML = '<object type="application/x-shockwave-flash" id="BigBlueButton" name="BigBlueButton" tabindex="0" data="BigBlueButton.swf?v=VERSION" style="position: relative; top: 0.5px;" width="100%" height="100%" align="middle"><param name="quality" value="high"><param name="bgcolor" value="#FFFFFF"><param name="allowfullscreen" value="true"><param name="allowfullscreeninteractive" value="true"><param name="wmode" value="window"><param name="allowscriptaccess" value="true"><param name="seamlesstabbing" value="true"></object>';
+            content.innerHTML = '<object type="application/x-shockwave-flash" id="BigBlueButton" name="BigBlueButton" tabindex="0" data="BigBlueButton.swf?v=VERSION" style="position: relative; top: 0.5px;" width="100%" height="100%" align="middle"><param name="quality" value="high"><param name="bgcolor" value="#FFFFFF"><param name="allowfullscreen" value="true"><param name="allowfullscreeninteractive" value="true"><param name="wmode" value="window"><param name="allowscriptaccess" value="always"><param name="seamlesstabbing" value="true"></object>';
           }
         };
       } else {
diff --git a/bigbluebutton-client/resources/prod/guest-wait.html b/bigbluebutton-client/resources/prod/guest-wait.html
new file mode 100755
index 0000000000000000000000000000000000000000..99c744aa3375abbf7881c2d88c3c46a8ffe92af6
--- /dev/null
+++ b/bigbluebutton-client/resources/prod/guest-wait.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+
+<head>
+  <title>Guest Lobby</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+  <style></style>
+
+  <script src="lib/jquery-2.1.1.min.js" type="text/javascript"></script>
+  
+  <script type="text/javascript">
+    function updateMessage(message) {
+      $('#content > p').html(message);
+    }
+
+    function findSessionToken() {
+      return location.search
+        .substr(1)
+        .split('&')
+        .find(function(item) {
+          return item.split('=')[0] === 'sessionToken'
+        });
+    };
+
+    function fetchGuestWait(sessionToken) {
+      const GUEST_WAIT_ENDPOINT = 'http://HOST/bigbluebutton/api/guestWait';
+
+      return $.get(GUEST_WAIT_ENDPOINT, sessionToken.concat('&redirect=false'));
+    };
+
+    function pollGuestStatus(token, attempt, limit, everyMs) {
+      setTimeout(function() {
+        var REDIRECT_STATUSES = ['ALLOW', 'DENY'];
+
+
+        if (attempt >= limit) {
+          updateMessage('TIMEOUT_MESSAGE_HERE');
+          return;
+        }
+
+        fetchGuestWait(token).always(function(data) {
+          console.log("data=" + JSON.stringify(data));
+          var status = data.response.guestStatus;
+
+          if (REDIRECT_STATUSES.includes(status)) {
+            window.location = data.response.url;
+            return;
+          }
+
+          return pollGuestStatus(token, attempt + 1, limit, everyMs);
+        })
+      }, everyMs);
+    };
+
+    window.onload = function() {
+      try {
+        var ATTEMPT_EVERY_MS = 5000;
+        var ATTEMPT_LIMIT = 100;
+
+        var sessionToken = findSessionToken();
+
+        if(!sessionToken) {
+          updateMessage('NO_SESSION_TOKEN_MESSAGE');
+          return;
+        }
+
+        pollGuestStatus(sessionToken, 0, ATTEMPT_LIMIT, ATTEMPT_EVERY_MS);
+      } catch (e) {
+        console.error(e);
+        updateMessage('GENERIC_ERROR_MESSAGE');
+      }
+    };
+  </script>
+</head>
+
+<body>
+  <div id="banner"></div>
+  <div id="content">
+    <p>Please wait for a moderator to approve you joining the meeting.</p>
+  </div>
+</body>
+
+</html>
diff --git a/bigbluebutton-client/src/BigBlueButton.mxml b/bigbluebutton-client/src/BigBlueButton.mxml
index 70be7320dcf9986a13033053128004443d945e7c..41d3e30c3bd8f006a7457669f7eea07c912fbbdf 100755
--- a/bigbluebutton-client/src/BigBlueButton.mxml
+++ b/bigbluebutton-client/src/BigBlueButton.mxml
@@ -36,11 +36,31 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 
 	<fx:Script>
 		<![CDATA[
+			import mx.utils.URLUtil;
+			
+			import org.as3commons.logging.api.ILogger;
+			import org.as3commons.logging.api.getClassLogger;
 			import org.bigbluebutton.common.LogUtil;
+			
+			private static const LOGGER:ILogger = getClassLogger(BigBlueButtonMainContainer);
 
 			private function init():void {
 				LogUtil.initLogging(true);
 			}
+			
+			public function determineHtmlUrl():String {
+				var url:String = "";
+				if(ExternalInterface.available) {
+					try {
+						url = String(ExternalInterface.call("window.location.href.toString"));
+						LOGGER.debug("HTML URL [{0}]", [url]);
+					} catch(s:Error) {
+						LOGGER.debug("Cannot determine HTML URL");
+					}
+				}
+				
+				return url;
+			}
 		]]>
 	</fx:Script>
   <views:BigBlueButtonMainContainer id="bbbShell"/>
diff --git a/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml b/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml
old mode 100644
new mode 100755
index f89ef629dc3706db318bd3577de27109d486a935..3c77c147803ea3a71ed23a70313bedc7c6eaf56c
--- a/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml
+++ b/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml
@@ -109,23 +109,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
       private function setupAPI():void {
         langResources = ResourceUtil.getInstance();
         api = new ExternalApiCallbacks();
-        Security.allowDomain(determineHTMLURL());
-		LOGGER.debug("Security.allowDomain({0});", [determineHTMLURL()]);
-      }
-      
-      private function determineHTMLURL():String {
-        var serverName:String = "*";
-        if(ExternalInterface.available) {
-          try {
-            var htmlURL:String = String(ExternalInterface.call("window.location.href.toString"));
-            serverName = URLUtil.getServerName(htmlURL);
-            LOGGER.debug("HTML URL [{0}]", [htmlURL]);
-          } catch(s:Error) {
-            LOGGER.debug("Cannot determine HTML URL");
-          }
-        }
-        
-        return serverName;
+        //Security.allowDomain(FlexGlobals.topLevelApplication.determineHtmlUrl());
+        //LOGGER.debug("Security.allowDomain({0});", [FlexGlobals.topLevelApplication.determineHtmlUrl()]);
+        Security.allowDomain("*");
+        LOGGER.debug("Security.allowDomain({0});", ["*"]);
       }
 		
 	  /**
diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as b/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as
index 8b35594cc05aff4b9ee24e380ed7e973a1b41203..279ff41b3b34e23cf11350279e44c309b761545e 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as
@@ -127,9 +127,9 @@ package org.bigbluebutton.core {
 			return logoutUrl;
 		}
 
-		public static function getBaseURL():String {
-			var protocol:String = URLUtil.getProtocol(FlexGlobals.topLevelApplication.url);
-			var serverName:String = URLUtil.getServerNameWithPort(FlexGlobals.topLevelApplication.url);
+		private static function getBaseURL():String {
+			var protocol:String = URLUtil.getProtocol(FlexGlobals.topLevelApplication.determineHtmlUrl());
+			var serverName:String = URLUtil.getServerNameWithPort(FlexGlobals.topLevelApplication.determineHtmlUrl());
 			return protocol + "://" + serverName;
 		}
 	}
diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConfigManager2.as b/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConfigManager2.as
index d1ba4c13eb16bed92496e29c0032733deebb8cbd..11ed5370ebff6626deb1ef504edb13821e3cb9aa 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConfigManager2.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConfigManager2.as
@@ -70,7 +70,7 @@ package org.bigbluebutton.core.managers {
         }
 
         private function buildRequestURL():String {
-            var swfURL:String = FlexGlobals.topLevelApplication.url;
+            var swfURL:String = FlexGlobals.topLevelApplication.determineHtmlUrl();
             var protocol:String = URLUtil.getProtocol(swfURL);
             var serverName:String = URLUtil.getServerNameWithPort(swfURL);
             return protocol + "://" + serverName + "/" + CONFIG_XML;
diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/managers/VideoProfileManager.as b/bigbluebutton-client/src/org/bigbluebutton/core/managers/VideoProfileManager.as
old mode 100644
new mode 100755
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
index 103ddb3da55c79d9c353de6c3f79aaa0aca999f3..c68ae270502e14acd8c58b70a15b5c48642f8d8e 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
@@ -162,7 +162,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			private var stoppedModules:ArrayCollection;			
 			private var logWindow:LogWindow;
 			private var waitWindow:WaitingWindow = null;
-			private var guestWindow:PendingGuestsWindow = null;
 			private var scWindow:ShortcutHelpWindow;
 			private var connectionLostWindow:ConnectionLostWindow;
 			
@@ -347,34 +346,25 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				}
 			}
 
-			private function closeGuestWindow(e:Event = null):void {
-				if(guestWindow != null) {
-					guestWindow.closeWindow();
-					guestWindow = null;
-				}
-			}
-
 			private function refreshGuestView(evt:NewGuestWaitingEvent):void {
+        LOGGER.debug("NewGuestWaitingEvent");
 				// do not show the guest window if the user isn't moderator or if he's waiting for acceptance
 				if (!UsersUtil.amIModerator() || UsersUtil.amIWaitingForAcceptance() && usersOptions.enableGuestUI) {
-					closeGuestWindow();
 					return;
 				}
 
-				if (guestWindow == null) {
-					guestWindow = PopUpUtil.createModalPopUp( mdiCanvas, PendingGuestsWindow, false) as PendingGuestsWindow;
-					guestWindow.addEventListener(Event.CLOSE, closeGuestWindow);
-
+          LOGGER.debug("OPENING GUEST WINDOW");
+          var guestWindow:PendingGuestsWindow = PopUpUtil.createModalPopUp( mdiCanvas, PendingGuestsWindow, false) as PendingGuestsWindow;
 					guestWindow.x = systemManager.screen.width - guestWindow.width - 20;
 					guestWindow.y = 20;
-				}
 				guestWindow.refreshGuestView();
 			}
 
 			public function removeGuestWindow(evt:BBBEvent):void {
-				if (guestWindow != null) {
-					guestWindow.remove(evt.payload.userId);
-				}
+        LOGGER.debug("REMOVE GUEST WINDOW");
+				//if (guestWindow != null) {
+				//	guestWindow.remove(evt.payload.userId);
+				//}
 			}
 
 			private function closeWaitWindow(e:BBBEvent):void {
@@ -635,8 +625,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
                 } else {
                     mdiCanvas.removeAllPopUps();
                     removeToolBars();
-                    var pageHost:String = FlexGlobals.topLevelApplication.url.split("/")[0];
-                    var pageURL:String = FlexGlobals.topLevelApplication.url.split("/")[2];
+                    var serverURL:String = FlexGlobals.topLevelApplication.determineHtmlUrl();
+                    var pageHost:String = serverURL.split("/")[0];
+                    var pageURL:String = serverURL.split("/")[2];
                     LOGGER.debug("SingOut to [{0}//{1}/bigbluebutton/api/signOut]", [pageHost, pageURL]);
                     var request:URLRequest = new URLRequest(pageHost + "//" + pageURL + "/bigbluebutton/api/signOut");
                     var urlLoader:URLLoader = new URLLoader();
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
index 21bf47a02c0296f3a43258a8c40c20482bdec18e..3955d785ab51f56bb335d05b01d99f4c76c3258b 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
@@ -401,7 +401,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				}
 
 				if (!StringUtils.isEmpty(brandingOptions.toolbarColor)) {
-					topBox.setStyle("backgroundColor", uint("0x" + brandingOptions.toolbarColor.substr(1)));
+					mainBox.setStyle("backgroundColor", uint("0x" + brandingOptions.toolbarColor.substr(1)));
 				}
 			}
 			
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/PendingGuestsWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/PendingGuestsWindow.mxml
index 036da8ae8c51d68a9598073732698d2ec68f940d..f4aa6b8d0767a1a0ccfef79509b0c15aa03b5ead 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/PendingGuestsWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/PendingGuestsWindow.mxml
@@ -37,19 +37,17 @@ $Id: $
 
     <fx:Script>
         <![CDATA[
-			import com.asfusion.mate.events.Dispatcher;
-			
-			import mx.collections.ArrayCollection;
-			import mx.managers.PopUpManager;
-			
-			import org.bigbluebutton.core.model.LiveMeeting;
-			import org.bigbluebutton.core.model.users.GuestWaiting;
-			import org.bigbluebutton.main.events.BBBEvent;
-			import org.bigbluebutton.main.events.RemoveGuestEvent;
-			import org.bigbluebutton.main.events.RemoveGuestFromViewEvent;
-			import org.bigbluebutton.main.events.ResponseModeratorEvent;
-			import org.bigbluebutton.main.model.GuestPolicy;
-			import org.bigbluebutton.util.i18n.ResourceUtil;
+          import com.asfusion.mate.events.Dispatcher;
+          import mx.collections.ArrayCollection;
+          import org.bigbluebutton.core.PopUpUtil;
+          import org.bigbluebutton.core.model.LiveMeeting;
+          import org.bigbluebutton.core.model.users.GuestWaiting;
+          import org.bigbluebutton.main.events.BBBEvent;
+          import org.bigbluebutton.main.events.RemoveGuestEvent;
+          import org.bigbluebutton.main.events.RemoveGuestFromViewEvent;
+          import org.bigbluebutton.main.events.ResponseModeratorEvent;
+          import org.bigbluebutton.main.model.GuestPolicy;
+          import org.bigbluebutton.util.i18n.ResourceUtil;
             
 			[Bindable]
 			private var guestUsers:ArrayCollection = new ArrayCollection();
@@ -112,7 +110,7 @@ $Id: $
             }
             
             public function closeWindow():void {
-                PopUpManager.removePopUp(this);
+              PopUpUtil.removePopUp(this);
             }
             
         ]]>
diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton
index e3be361bb6fd9241705ed00cebd440b8f7ef59bf..9349a42b5041b8b79a3225864b59802f1fa89c6d 100755
--- a/bigbluebutton-config/cron.daily/bigbluebutton
+++ b/bigbluebutton-config/cron.daily/bigbluebutton
@@ -115,10 +115,3 @@ remove_raw_of_published_recordings(){
 #
 find /tmp -name "*.afm" -mtime +$history -delete
 find /tmp -name "*.pfb" -mtime +$history -delete
-
-#
-# If there are no users currently logged in, restart libreoffice to clear its memory usage
-#
-if [[ $(netstat -ant | egrep ":1935\ " | egrep -v ":::|0.0.0.0"  | wc | awk '{print $1}') == 0 ]]; then
-  systemctl restart libreoffice.service
-fi
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
index 58037d1e5340be2ccf2787d649456a226113b702..69e256274f1c44a6468168fb41d85cba8b4c352f 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
@@ -1,14 +1,35 @@
 export default class BaseAudioBridge {
-  constructor() {
+  constructor(userData) {
+    this.userData = userData;
+
+    this.baseErrorCodes = {
+      INVALID_TARGET: 'INVALID_TARGET',
+      CONNECTION_ERROR: 'CONNECTION_ERROR',
+      REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
+      GENERIC_ERROR: 'GENERIC_ERROR',
+      MEDIA_ERROR: 'MEDIA_ERROR',
+    };
+
+    this.baseCallStates = {
+      started: 'started',
+      ended: 'ended',
+      failed: 'failed',
+    };
   }
 
   exitAudio() {
+    console.error('The Bridge must implement exitAudio');
   }
 
-  joinListenOnly() {
+  joinAudio() {
+    console.error('The Bridge must implement joinAudio');
   }
 
-  joinMicrophone() {
+  changeInputDevice() {
+    console.error('The Bridge must implement changeInputDevice');
   }
 
+  changeOutputDevice() {
+    console.error('The Bridge must implement changeOutputDevice');
+  }
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 81b9825f44a6aed5eb0630cf706a18859af35e41..271b8e5a3634373776114af6a4b076535e3f9f75 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -1,85 +1,373 @@
-import { makeCall } from '/imports/ui/services/api';
-
+import VoiceUsers from '/imports/api/voice-users';
+import { Tracker } from 'meteor/tracker';
 import BaseAudioBridge from './base';
 
-const APP_CONFIG = Meteor.settings.public.app;
-const MEDIA_CONFIG = Meteor.settings.public.media;
+const MEDIA = Meteor.settings.public.media;
+const STUN_TURN_FETCH_URL = MEDIA.stunTurnServersFetchAddress;
+const MEDIA_TAG = MEDIA.mediaTag;
+const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
+const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
+const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
+
+const fetchStunTurnServers = (sessionToken) => {
+  const handleStunTurnResponse = ({ stunServers, turnServers }) => {
+    if (!stunServers && !turnServers) {
+      return { error: 404, stun: [], turn: [] };
+    }
+    return {
+      stun: stunServers.map(server => server.url),
+      turn: turnServers.map(server => server.url),
+    };
+  };
+
+  const url = `${STUN_TURN_FETCH_URL}?sessionToken=${sessionToken}`;
+  return fetch(url)
+    .then(res => res.json())
+    .then(handleStunTurnResponse)
+    .then((response) => {
+      if (response.error) {
+        return Promise.reject('Could not fetch the stuns/turns servers!');
+      }
+      return response;
+    });
+};
 
-let triedHangup = false;
 
 export default class SIPBridge extends BaseAudioBridge {
   constructor(userData) {
-    super();
-    this.userData = userData;
+    super(userData);
+
+    const {
+      userId,
+      username,
+      sessionToken,
+    } = userData;
+
+    this.user = {
+      userId,
+      sessionToken,
+      name: username,
+    };
+
+    this.media = {
+      inputDevice: {},
+    };
+
+    this.protocol = window.document.location.protocol;
+    this.hostname = window.document.location.hostname;
+
+    const causes = window.SIP.C.causes;
+
+    this.errorCodes = {
+      [causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT,
+      [causes.INVALID_TARGET]: this.baseErrorCodes.INVALID_TARGET,
+      [causes.CONNECTION_ERROR]: this.baseErrorCodes.CONNECTION_ERROR,
+    };
+  }
+
+  joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
+    return new Promise((resolve, reject) => {
+      const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
+
+      const callback = (message) => {
+        managerCallback(message).then(resolve);
+      };
+
+      this.callback = callback;
+
+      return this.doCall({ callExtension, isListenOnly, inputStream })
+                 .catch((reason) => {
+                   callback({
+                     status: this.baseCallStates.failed,
+                     error: this.baseErrorCodes.GENERIC_ERROR,
+                     bridgeError: reason,
+                   });
+                   reject(reason);
+                 });
+    });
   }
 
-  joinListenOnly(stunServers, turnServers, callbackFromManager) {
-    makeCall('listenOnlyToggle', true);
-    this._joinVoiceCallSIP({ isListenOnly: true }, stunServers, turnServers, callbackFromManager);
+  doCall(options) {
+    const {
+      isListenOnly,
+    } = options;
+
+    const {
+      userId,
+      name,
+      sessionToken,
+    } = this.user;
+
+    const callerIdName = [
+      userId,
+      'bbbID',
+      isListenOnly ? `LISTENONLY-${name}` : name,
+    ].join('-');
+
+    this.user.callerIdName = callerIdName;
+    this.callOptions = options;
+
+    return fetchStunTurnServers(sessionToken)
+                        .then(this.createUserAgent.bind(this))
+                        .then(this.inviteUserAgent.bind(this))
+                        .then(this.setupEventHandlers.bind(this));
   }
 
-  joinMicrophone(stunServers, turnServers, callbackFromManager) {
-    this._joinVoiceCallSIP({ isListenOnly: false }, stunServers, turnServers, callbackFromManager);
+  transferCall(onTransferSuccess) {
+    return new Promise((resolve, reject) => {
+      let trackerControl = null;
+
+      const timeout = setTimeout(() => {
+        clearTimeout(timeout);
+        trackerControl.stop();
+        this.callback({
+          status: this.baseCallStates.failed,
+          error: this.baseErrorCodes.REQUEST_TIMEOUT,
+          bridgeError: 'Timeout on call transfer' });
+        reject(this.baseErrorCodes.REQUEST_TIMEOUT);
+      }, CALL_TRANSFER_TIMEOUT);
+
+      // This is is the call transfer code ask @chadpilkey
+      this.currentSession.dtmf(1);
+
+      Tracker.autorun((c) => {
+        trackerControl = c;
+        const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId };
+        const query = VoiceUsers.find(selector);
+
+        query.observeChanges({
+          changed: (id, fields) => {
+            if (fields.joined) {
+              clearTimeout(timeout);
+              onTransferSuccess();
+              c.stop();
+              resolve();
+            }
+          },
+        });
+      });
+    });
   }
 
-  // Periodically check the status of the WebRTC call, when a call has been established attempt to
-  // hangup, retry if a call is in progress, send the leave voice conference message to BBB
-  exitAudio(isListenOnly, afterExitCall = () => { }) {
-    // To be called when the hangup is confirmed
-    const hangupCallback = function () {
-      console.log(`Exited Voice Conference, listenOnly=${isListenOnly}`);
+  exitAudio() {
+    return new Promise((resolve, reject) => {
+      let hangupRetries = 0;
+      let hangup = false;
+      const tryHangup = () => {
+        this.currentSession.bye();
+        hangupRetries += 1;
 
-      // notify BBB-apps we are leaving the call if we are in listen only mode
-      if (isListenOnly) {
-        makeCall('listenOnlyToggle', false);
-      }
+        setTimeout(() => {
+          if (hangupRetries > CALL_HANGUP_MAX_RETRIES) {
+            this.callback({
+              status: this.baseCallStates.failed,
+              error: this.baseErrorCodes.REQUEST_TIMEOUT,
+              bridgeError: 'Timeout on call hangup',
+            });
+            return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
+          }
+
+          if (!hangup) return tryHangup();
+          return resolve();
+        }, CALL_HANGUP_TIMEOUT);
+      };
+
+      this.currentSession.on('bye', () => {
+        hangup = true;
+        resolve();
+      });
+
+      return tryHangup();
+    });
+  }
+
+  createUserAgent({ stun, turn }) {
+    return new Promise((resolve, reject) => {
+      const {
+        hostname,
+        protocol,
+      } = this;
+
+      const {
+        callerIdName,
+      } = this.user;
+
+      let userAgent = new window.SIP.UA({
+        uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
+        wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
+        // log: {
+        //   builtinEnabled: false,
+        // },
+        displayName: callerIdName,
+        register: false,
+        traceSip: true,
+        autostart: false,
+        userAgentString: 'BigBlueButton',
+        stunServers: stun,
+        turnServers: turn,
+      });
+
+      userAgent.removeAllListeners('connected');
+      userAgent.removeAllListeners('disconnected');
+
+      const handleUserAgentConnection = () => {
+        resolve(userAgent);
+      };
+
+      const handleUserAgentDisconnection = () => {
+        userAgent.stop();
+        userAgent = null;
+        this.callback({
+          status: this.baseCallStates.failed,
+          error: this.baseErrorCodes.CONNECTION_ERROR,
+          bridgeError: 'User Agent Disconnected' });
+        reject(this.baseErrorCodes.CONNECTION_ERROR);
+      };
+
+      userAgent.on('connected', handleUserAgentConnection);
+      userAgent.on('disconnected', handleUserAgentDisconnection);
+
+      userAgent.start();
+    });
+  }
+
+  inviteUserAgent(userAgent) {
+    const {
+      hostname,
+    } = this;
+
+    const {
+      inputStream,
+      callExtension,
+    } = this.callOptions;
+
+    const options = {
+      media: {
+        stream: inputStream,
+        constraints: {
+          audio: true,
+          video: false,
+        },
+        render: {
+          remote: document.querySelector(MEDIA_TAG),
+        },
+      },
+      RTCConstraints: {
+        mandatory: {
+          OfferToReceiveAudio: true,
+          OfferToReceiveVideo: false,
+        },
+      },
     };
 
-    // Checks periodically until a call is established so we can successfully
-    // end the call clean state
-    triedHangup = false;
+    return userAgent.invite(`sip:${callExtension}@${hostname}`, options);
+  }
 
-    // function to initiate call
-    const checkToHangupCall = ((context, afterExitCall = () => { }) => {
-      // if an attempt to hang up the call is made when the current session is not yet finished,
-      // the request has no effect keep track in the session if we haven't tried a hangup
-      if (window.getCallStatus() != null && !triedHangup) {
-        console.log('Attempting to hangup on WebRTC call');
-        window.webrtc_hangup(hangupCallback);
+  setupEventHandlers(currentSession) {
+    return new Promise((resolve) => {
+      this.connectionCompleted = false;
 
-        // we have hung up, prevent retries
-        triedHangup = true;
+      const handleConnectionCompleted = () => {
+        if (this.connectionCompleted) return;
+        this.callback({ status: this.baseCallStates.started });
+        this.connectionCompleted = true;
+        resolve();
+      };
 
-        if (afterExitCall) {
-          afterExitCall(this, APP_CONFIG.listenOnly);
+      const handleSessionTerminated = (message, cause) => {
+        this.connectionCompleted = false;
+        if (!message && !cause) {
+          return this.callback({
+            status: this.baseCallStates.ended,
+          });
         }
-      } else {
-        console.log('RETRYING hangup on WebRTC call in ' +
-          `${MEDIA_CONFIG.WebRTCHangupRetryInterval} ms`);
 
-        // try again periodically
-        setTimeout(checkToHangupCall, MEDIA_CONFIG.WebRTCHangupRetryInterval);
-      }
-    })(this, afterExitCall);
+        const mappedCause = cause in this.errorCodes ?
+                            this.errorCodes[cause] :
+                            this.baseErrorCodes.GENERIC_ERROR;
+
+        return this.callback({
+          status: this.baseCallStates.failed,
+          error: mappedCause,
+          bridgeError: cause,
+        });
+      };
+
+      currentSession.on('terminated', handleSessionTerminated);
+      currentSession.mediaHandler.on('iceConnectionCompleted', handleConnectionCompleted);
+      currentSession.mediaHandler.on('iceConnectionConnected', handleConnectionCompleted);
+
+      this.currentSession = currentSession;
+    });
+  }
+
+  getMediaStream(constraints) {
+    return navigator.mediaDevices.getUserMedia(constraints).catch((err) => {
+      console.error(err);
+      throw new Error(this.baseErrorCodes.MEDIA_ERROR);
+    });
+  }
 
-    return false;
+  async setDefaultInputDevice() {
+    const mediaStream = await this.getMediaStream({ audio: true });
+    const deviceLabel = mediaStream.getAudioTracks()[0].label;
+    const mediaDevices = await navigator.mediaDevices.enumerateDevices();
+    const device = mediaDevices.find(d => d.label === deviceLabel);
+    return this.changeInputDevice(device.deviceId);
   }
 
-  // join the conference. If listen only send the request to the server
-  _joinVoiceCallSIP(options, stunServers, turnServers, callbackFromManager) {
-    const extension = this.userData.voiceBridge;
-    console.log(options);
+  async changeInputDevice(value) {
+    const {
+      media,
+    } = this;
 
-    // create voice call params
-    const joinCallback = function (message) {
-      console.log('Beginning WebRTC Conference Call');
-    };
+    if (media.inputDevice.audioContext) {
+      media.inputDevice.audioContext.close().then(() => {
+        media.inputDevice.audioContext = null;
+        media.inputDevice.scriptProcessor = null;
+        media.inputDevice.source = null;
+        return this.changeInputDevice(value);
+      });
+    }
 
-    const stunsAndTurns = {
-      stun: stunServers,
-      turn: turnServers,
+    media.inputDevice.id = value;
+    if ('AudioContext' in window) {
+      media.inputDevice.audioContext = new window.AudioContext();
+    } else {
+      media.inputDevice.audioContext = new window.webkitAudioContext();
+    }
+    media.inputDevice.scriptProcessor = media.inputDevice.audioContext
+                                              .createScriptProcessor(2048, 1, 1);
+    media.inputDevice.source = null;
+
+    const constraints = {
+      audio: {
+        deviceId: value,
+      },
     };
 
-    callIntoConference(extension, callbackFromManager, options.isListenOnly, stunsAndTurns);
+    const mediaStream = await this.getMediaStream(constraints);
+    media.inputDevice.stream = mediaStream;
+    media.inputDevice.source = media.inputDevice.audioContext.createMediaStreamSource(mediaStream);
+    media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
+    media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
+
+    return this.media.inputDevice;
+  }
+
+  async changeOutputDevice(value) {
+    const audioContext = document.querySelector(MEDIA_TAG);
+
+    if (audioContext.setSinkId) {
+      try {
+        await audioContext.setSinkId(value);
+        this.media.outputDeviceId = value;
+      } catch (err) {
+        console.error(err);
+        throw new Error(this.baseErrorCodes.MEDIA_ERROR);
+      }
+    }
+
+    return this.media.outputDeviceId;
   }
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js b/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
index df5a59fd3742222f2b95e5396c48b1382d640c39..0293d77a358254629b696462277a9a857dc17c9b 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
@@ -17,17 +17,10 @@ export default class VertoBridge extends BaseAudioBridge {
     window.vertoExitAudio();
   }
 
-  joinListenOnly() {
-    window.vertoJoinListenOnly(
-      'remote-media',
-      this.voiceBridge,
-      this.vertoUsername,
-      null,
-    );
-  }
+  joinAudio({ isListenOnly }) {
+    const vertoJoin = isListenOnly ? 'vertoJoinListenOnly' : 'vertoJoinMicrophone';
 
-  joinMicrophone() {
-    window.vertoJoinMicrophone(
+    window[vertoJoin](
       'remote-media',
       this.voiceBridge,
       this.vertoUsername,
diff --git a/bigbluebutton-html5/imports/api/audio/client/manager/index.js b/bigbluebutton-html5/imports/api/audio/client/manager/index.js
deleted file mode 100644
index 621f720136696728c28e9ec1fa5308dc1bf3d67d..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/api/audio/client/manager/index.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import Auth from '/imports/ui/services/auth';
-import BaseAudioBridge from '../bridge/base';
-import VertoBridge from '../bridge/verto';
-import SIPBridge from '../bridge/sip';
-
-class CallStates {
-  static get init() {
-    return 'initialized state';
-  }
-  static get echo() {
-    return 'do echo test state';
-  }
-  static get callIntoEcho() {
-    return 'calling into echo test state';
-  }
-  static get inEchoTest() {
-    return 'in echo test state';
-  }
-  static get joinVoiceConference() {
-    return 'join voice conference state';
-  }
-  static get callIntoConference() {
-    return 'calling into conference state';
-  }
-  static get inConference() {
-    return 'in conference state';
-  }
-  static get transferToConference() {
-    return 'joining from echo into conference state';
-  }
-  static get echoTestFailed() {
-    return 'echo test failed state';
-  }
-  static get callToListenOnly() {
-    return 'call to listen only state';
-  }
-  static get connectToListenOnly() {
-    return 'connecting to listen only state';
-  }
-  static get inListenOnly() {
-    return 'in listen only state';
-  }
-  static get reconnecting() {
-    return 'reconecting';
-  }
-}
-
-const ErrorCodes = {
-  CODE_1001: '1001',
-  CODE_1002: '1002',
-  CODE_1003: '1003',
-  CODE_1004: '1004',
-  CODE_1005: '1005',
-  CODE_1006: '1006',
-  CODE_1007: '1007',
-  CODE_1008: '1008',
-  CODE_1009: '1009',
-  CODE_1010: '1010',
-  CODE_1011: '1011',
-};
-
-const AudioErrorCodes = Object.freeze(ErrorCodes);
-
-// manages audio calls and audio bridges
-class AudioManager {
-  init(userData) {
-    // this check ensures changing locales will not rerun init
-    if (this.currentState !== undefined) {
-      return;
-    }
-    const MEDIA_CONFIG = Meteor.settings.public.media;
-    const audioBridge = MEDIA_CONFIG.useSIPAudio
-      ? new SIPBridge(userData)
-      : new VertoBridge(userData);
-
-    if (!(audioBridge instanceof BaseAudioBridge)) {
-      throw 'Audio Bridge not compatible';
-    }
-
-    this.bridge = audioBridge;
-    this.isListenOnly = false;
-    this.microphoneLockEnforced = userData.microphoneLockEnforced;
-    this.callStates = CallStates;
-    this.currentState = this.callStates.init;
-
-    callbackToAudioBridge = function (message) {
-      switch (message.status) {
-        case 'failed': {
-          this.currentState = this.callStates.init;
-          const audioFailed = new CustomEvent('bbb.webrtc.failed', {
-            detail: {
-              status: 'Failed',
-              errorCode: message.errorcode,
-            },
-          });
-          window.dispatchEvent(audioFailed);
-          break;
-        }
-        case 'mediafail': {
-          const mediaFailed = new CustomEvent('bbb.webrtc.mediaFailed', {
-            detail: {
-              status: 'MediaFailed',
-            },
-          });
-          window.dispatchEvent(mediaFailed);
-          break;
-        }
-        case 'mediasuccess':
-        case 'started': {
-          const connected = new CustomEvent('bbb.webrtc.connected', {
-            detail: {
-              status: 'started',
-            },
-          });
-          window.dispatchEvent(connected);
-          break;
-        }
-      }
-    };
-  }
-
-  getCurrentState() {
-    return this.currentState;
-  }
-
-  exitAudio() {
-    this.bridge.exitAudio(this.isListenOnly);
-    this.currentState = this.callStates.init;
-  }
-
-  joinAudio(listenOnly) {
-    AudioManager.fetchServers().then(({ error, stunServers, turnServers }) => {
-      if (error || error !== undefined) {
-        // We need to alert the user about this problem by some gui message.
-        console.error("Couldn't fetch the stuns/turns servers!");
-        AudioManager.stunTurnServerFail();
-        return;
-      }
-
-      if (listenOnly || this.microphoneLockEnforced) {
-        this.isListenOnly = true;
-        this.bridge.joinListenOnly(stunServers, turnServers, callbackToAudioBridge.bind(this));
-        // TODO: remove line below after echo test implemented, use webRTCCallStarted instead
-        this.currentState = this.callStates.inListenOnly;
-      } else {
-        this.bridge.joinMicrophone(stunServers, turnServers, callbackToAudioBridge.bind(this));
-        // TODO: remove line below after echo test implemented, use webRTCCallStarted instead
-        this.currentState = this.callStates.inConference;
-      }
-    });
-  }
-
-  transferToConference() {
-    // TODO: transfer from initialized state
-    // TODO: transfer from echo test to conference
-    // this.bridge.transferToConference();
-  }
-
-  webRTCCallStarted(inEchoTest) {
-    if (this.isListenOnly) {
-      this.currentState = this.callStates.inListenOnly;
-    }
-    this.currentState = this.callStates.inConference;
-  }
-
-  webRTCCallFailed(inEchoTest, errorcode, cause) {
-    if (this.currentState !== this.CallStates.reconecting) {
-      this.currentState = this.CallStates.reconecting;
-    }
-  }
-
-  getMicId() {
-    // Placeholder, will get the microphone ID for switching input device
-    // this.bridge.getMicId();
-  }
-
-  setMicId() {
-    // Placeholder, will set the microphone ID for switching input device
-    // this.bridge.setMicId();
-  }
-
-  getSpeakerId() {
-    // Placeholder, will get the speaker ID for switching output device
-    // this.bridge.getSpeakerId();
-  }
-
-  setSpeakerId() {
-    // Placeholder, will set the speaker ID for switching output device
-    // this.bridge.setSpeakerId();
-  }
-
-  getActiveMic() {
-    // Placeholder, will detect active input hardware
-    // this.bridge.getActiveMic();
-  }
-
-  stunTurnServerFail() {
-    const audioFailed = new CustomEvent('bbb.webrtc.failed', {
-      detail: {
-        status: 'Failed',
-        errorCode: AudioErrorCodes.CODE_1009,
-      },
-    });
-    window.dispatchEvent(audioFailed);
-  }
-
-  // We use on the SIP an String Array, while in the server, it comes as
-  // an Array of objects, we need to map from Array<Object> to Array<String>
-  static mapToArray({ response, stunServers, turnServers }) {
-    const promise = new Promise((resolve) => {
-      if (response) {
-        resolve({ error: 404, stunServers: [], turnServers: [] });
-      }
-      resolve({
-        stunServers: stunServers.map(server => server.url),
-        turnServers: turnServers.map(server => ({
-          urls: server.url,
-          username: server.username,
-          password: server.password,
-        })),
-      });
-    });
-    return promise;
-  }
-
-  static fetchServers() {
-    const url = `/bigbluebutton/api/stuns?sessionToken=${Auth.sessionToken}`;
-
-    return fetch(url)
-      .then(response => response.json())
-      .then(json => AudioManager.mapToArray(json));
-  }
-}
-
-const AudioManagerSingleton = new AudioManager();
-export default AudioManagerSingleton;
-export { AudioErrorCodes };
diff --git a/bigbluebutton-html5/imports/api/bbb/index.js b/bigbluebutton-html5/imports/api/bbb/index.js
deleted file mode 100644
index c8b4406628a1c012919bd107129b332106621c5b..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/api/bbb/index.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import AudioManager from '/imports/api/audio/client/manager';
-import Auth from '/imports/ui/services/auth';
-import Users from '/imports/api/users';
-import Meetings from '/imports/api/meetings';
-
-class BBB {
-
-  getUserId() {
-    const userID = Auth.userID;
-    return userID;
-  }
-
-  getUsername() {
-    return Users.findOne({ userId: this.getUserId() }).name;
-  }
-
-  getExtension() {
-    const extension = Meetings.findOne().voiceProp.voiceConf;
-    return extension;
-  }
-
-  getMyUserInfo(callback) {
-    const result = {
-      myUserID: this.getUserId(),
-      myUsername: this.getUsername(),
-      myInternalUserID: this.getUserId(),
-      myAvatarURL: null,
-      myRole: 'getMyRole',
-      amIPresenter: 'false',
-      voiceBridge: this.getExtension(),
-      dialNumber: null,
-    };
-    return callback(result);
-  }
-
-  webRTCCallFailed(inEchoTest, errorcode, cause) {
-    AudioManager.webRTCCallFailed(inEchoTest, errorcode, cause);
-  }
-
-  webRTCCallStarted(inEchoTest) {
-    AudioManager.webRTCCallStarted(inEchoTest);
-  }
-
-  getSessionToken(callback) {
-    callback(Auth.sessionToken);
-  }
-}
-
-export const initBBB = () => {
-  if (window.BBB == undefined) {
-    window.BBB = new BBB();
-  }
-};
diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/changeRole.js b/bigbluebutton-html5/imports/api/users/server/handlers/changeRole.js
index 5b0638cd2f0d763973a8ea6038a1acaf8d5803b7..5b4a77fea5d863df2dde3868411d3d9c58238213 100644
--- a/bigbluebutton-html5/imports/api/users/server/handlers/changeRole.js
+++ b/bigbluebutton-html5/imports/api/users/server/handlers/changeRole.js
@@ -1,9 +1,27 @@
 import { check } from 'meteor/check';
-import changeRole from '../modifiers/changeRole';
+import Users from '/imports/api/users';
+import changeRole from '/imports/api/users/server/modifiers/changeRole';
 
 export default function handleChangeRole(payload, meetingId) {
+  const USER_CONFIG = Meteor.settings.public.user;
+  const ROLE_MODERATOR = USER_CONFIG.role_moderator;
+  const ROLE_VIEWER = USER_CONFIG.role_viewer;
+
   check(payload.body, Object);
   check(meetingId, String);
 
-  changeRole(payload, meetingId);
+  const { userId, role, changedBy } = payload.body;
+
+  const selector = {
+    meetingId,
+    userId,
+  };
+
+  const user = Users.findOne(selector);
+
+  if (role === ROLE_VIEWER && user.role === ROLE_MODERATOR) {
+    changeRole(ROLE_MODERATOR, false, userId, meetingId, changedBy);
+  }
+
+  return changeRole(role, true, userId, meetingId, changedBy);
 }
diff --git a/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js b/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js
index ec3af049649a43097a948d85f57070d14c2d44ba..728665685fa8d54d63c9f76da4a60919f4ca8fb9 100644
--- a/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js
+++ b/bigbluebutton-html5/imports/api/users/server/handlers/presenterAssigned.js
@@ -1,65 +1,20 @@
-import Logger from '/imports/startup/server/logger';
-import { check } from 'meteor/check';
 import Users from '/imports/api/users';
-
-const unassignCurrentPresenter = (meetingId, presenterId) => {
-  const selector = {
-    meetingId,
-    userId: { $ne: presenterId },
-    presenter: true,
-  };
-
-  const modifier = {
-    $set: {
-      presenter: false,
-    },
-    $pop: {
-      roles: 'presenter',
-    },
-  };
-
-  const cb = (err) => {
-    if (err) {
-      return Logger.error(`Unassigning current presenter from collection: ${err}`);
-    }
-
-    return Logger.info(`Unassign current presenter meeting=${meetingId}`);
-  };
-
-  return Users.update(selector, modifier, cb);
-};
+import changeRole from '/imports/api/users/server/modifiers/changeRole';
 
 export default function handlePresenterAssigned({ body }, meetingId) {
-  const { presenterId } = body;
+  const USER_CONFIG = Meteor.settings.public.user;
+  const ROLE_PRESENTER = USER_CONFIG.role_presenter;
+
+  const { presenterId, assignedBy } = body;
 
-  check(presenterId, String);
+  changeRole(ROLE_PRESENTER, true, presenterId, meetingId, assignedBy);
 
   const selector = {
     meetingId,
-    userId: presenterId,
-  };
-
-  const modifier = {
-    $set: {
-      presenter: true,
-    },
-    $push: {
-      roles: 'presenter',
-    },
-  };
-
-  const cb = (err, numChange) => {
-    if (err) {
-      return Logger.error(`Assigning user as presenter: ${err}`);
-    }
-
-    if (numChange) {
-      unassignCurrentPresenter(meetingId, presenterId);
-      return Logger.info(`Assigned user as presenter id=${presenterId} meeting=${meetingId}`);
-    }
-
-    return Logger.info(`User not assigned as presenter id=${presenterId} meeting=${meetingId}`);
+    userId: { $ne: presenterId },
+    presenter: true,
   };
 
-  return Users.update(selector, modifier, cb);
+  const prevPresenter = Users.findOne(selector);
+  return changeRole(ROLE_PRESENTER, false, prevPresenter.userId, meetingId, assignedBy);
 }
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
index b191cb034cbe24d591236262e6e7999e31216796..26c2d6e8c08414a3837ed07dab82f72e81385bf0 100644
--- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
@@ -6,6 +6,7 @@ import stringHash from 'string-hash';
 import flat from 'flat';
 
 import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
+import changeRole from '/imports/api/users/server/modifiers/changeRole';
 
 const COLOR_LIST = [
   '#d32f2f', '#c62828', '#b71c1c', '#d81b60', '#c2185b', '#ad1457', '#880e4f',
@@ -43,6 +44,7 @@ export default function addUser(meetingId, user) {
   };
 
   const USER_CONFIG = Meteor.settings.public.user;
+  const ROLE_PRESENTER = USER_CONFIG.role_presenter;
   const ROLE_MODERATOR = USER_CONFIG.role_moderator;
   const ROLE_VIEWER = USER_CONFIG.role_viewer;
   const APP_CONFIG = Meteor.settings.public.app;
@@ -61,12 +63,6 @@ export default function addUser(meetingId, user) {
     userRole = ROLE_VIEWER;
   }
 
-  const userRoles = [
-    'viewer',
-    user.presenter ? 'presenter' : false,
-    userRole === ROLE_MODERATOR ? 'moderator' : false,
-  ].filter(Boolean);
-
   /* While the akka-apps dont generate a color we just pick one
     from a list based on the userId */
   const color = COLOR_LIST[stringHash(user.intId) % COLOR_LIST.length];
@@ -76,7 +72,7 @@ export default function addUser(meetingId, user) {
       {
         meetingId,
         connectionStatus: 'online',
-        roles: userRoles,
+        roles: [ROLE_VIEWER.toLowerCase()],
         sortName: user.name.trim().toLowerCase(),
         color,
       },
@@ -102,6 +98,14 @@ export default function addUser(meetingId, user) {
       return Logger.error(`Adding user to collection: ${err}`);
     }
 
+    if (user.presenter) {
+      changeRole(ROLE_PRESENTER, true, userId, meetingId);
+    }
+
+    if (userRole === ROLE_MODERATOR) {
+      changeRole(ROLE_MODERATOR, true, userId, meetingId);
+    }
+
     const { insertedId } = numChanged;
     if (insertedId) {
       return Logger.info(`Added user id=${userId} meeting=${meetingId}`);
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js
index b0290d06666af74a44df4935c0914b9f933f8c29..42842aa520be978ef7b941f5a0290d473db74232 100644
--- a/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/changeRole.js
@@ -1,30 +1,39 @@
 import Logger from '/imports/startup/server/logger';
 import Users from '/imports/api/users';
 
-export default function changeRole({ body }, meetingId) {
-  const { userId, role, changedBy } = body;
+export default function changeRole(role, status, userId, meetingId, changedBy) {
+  const USER_CONFIG = Meteor.settings.public.user;
+  const ROLE_PRESENTER = USER_CONFIG.role_presenter;
 
   const selector = {
     meetingId,
     userId,
   };
 
+  const action = status ? '$push' : '$pop';
+
+  const user = Users.findOne(selector);
+
   const modifier = {
     $set: {
-      role,
+      role: (role === ROLE_PRESENTER ? user.role : role),
+      [role.toLowerCase()]: status,
     },
-    $push: {
-      roles: (role === 'MODERATOR' ? 'moderator' : 'viewer'),
+    [action]: {
+      roles: (role.toLowerCase()),
     },
   };
 
   const cb = (err, numChanged) => {
+    const actionVerb = (status) ? 'Changed' : 'Removed';
+
     if (err) {
       return Logger.error(`Changed user role: ${err}`);
     }
 
     if (numChanged) {
-      return Logger.info(`Changed user role ${role} id=${userId} meeting=${meetingId} by changedBy=${changedBy}`);
+      return Logger.info(`${actionVerb} user role=${role} id=${userId} meeting=${meetingId}`
+      + `${changedBy ? ` changedBy=${changedBy}` : ''}`);
     }
 
     return null;
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 3583457ace752cf3bdf53f9dfcce3ad8ea6b27ca..425f64adcd25fb502cdb292a1ad78ee35f7b429b 100644
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -6,7 +6,6 @@ import AppContainer from '/imports/ui/components/app/container';
 import ErrorScreen from '/imports/ui/components/error-screen/component';
 import LoadingScreen from '/imports/ui/components/loading-screen/component';
 import Settings from '/imports/ui/services/settings';
-import { initBBB } from '/imports/api/bbb';
 import IntlStartup from './intl';
 
 const BROWSER_LANGUAGE = window.navigator.userLanguage || window.navigator.language;
@@ -35,8 +34,6 @@ class Base extends Component {
 
     this.updateLoadingState = this.updateLoadingState.bind(this);
     this.updateErrorState = this.updateErrorState.bind(this);
-
-    initBBB();
   }
 
   updateLoadingState(loading = false) {
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 1f9ec1b10d90daa4f4f44027eef4a3c41142306a..ad405b15fd069caf4e9ba80d590cf90fb0bdf962 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -2,25 +2,18 @@ import React from 'react';
 import styles from './styles.scss';
 import EmojiContainer from './emoji-menu/container';
 import ActionsDropdown from './actions-dropdown/component';
-import JoinAudioOptionsContainer from '../audio/audio-menu/container';
-import MuteAudioContainer from './mute-button/container';
+import AudioControlsContainer from '../audio/audio-controls/container';
 
 const ActionsBar = ({
   isUserPresenter,
-  handleOpenJoinAudio,
-  handleExitAudio,
 }) => (
   <div className={styles.actionsbar}>
     <div className={styles.left}>
       <ActionsDropdown {...{ isUserPresenter }} />
     </div>
     <div className={styles.center}>
-      <MuteAudioContainer />
-      <JoinAudioOptionsContainer
-        handleJoinAudio={handleOpenJoinAudio}
-        handleCloseAudio={handleExitAudio}
-      />
-      {/* <JoinVideo />*/}
+      <AudioControlsContainer />
+      {/* <JoinVideo /> */}
       <EmojiContainer />
     </div>
   </div>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
index efd132d6fe463d2f0875d770f5ef650130a7518b..c14930d45945edd8d73097b0b0ca7debfbfef5fb 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
+import { EMOJI_NORMALIZE } from '/imports/utils/statuses';
 
 import Button from '/imports/ui/components/button/component';
 import Dropdown from '/imports/ui/components/dropdown/component';
@@ -9,8 +10,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
 import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
-
-import { EMOJI_NORMALIZE } from '/imports/utils/statuses';
+import styles from './styles';
 
 const intlMessages = defineMessages({
   statusTriggerLabel: {
@@ -109,6 +109,7 @@ const intlMessages = defineMessages({
 
 const propTypes = {
   // Emoji status of the current user
+  intl: intlShape.isRequired,
   userEmojiStatus: PropTypes.string.isRequired,
   actions: PropTypes.object.isRequired,
 };
@@ -121,6 +122,7 @@ const EmojiMenu = ({
   <Dropdown autoFocus>
     <DropdownTrigger tabIndex={0}>
       <Button
+        className={styles.button}
         role="button"
         label={intl.formatMessage(intlMessages.statusTriggerLabel)}
         aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..136d7cad950af9b4c99c08fd6eb35d83fa0f0172
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
@@ -0,0 +1,9 @@
+.button {
+  &:focus {
+    outline: none !important;
+  }
+
+  span:first-child {
+    box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx
deleted file mode 100644
index 6e04a34a78c315e34cde96550c86d478df368258..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { defineMessages, injectIntl } from 'react-intl';
-import React from 'react';
-import Button from '/imports/ui/components/button/component';
-import styles from '../styles.scss';
-
-const intlMessages = defineMessages({
-  muteLabel: {
-    id: 'app.actionsBar.muteLabel',
-    description: 'Mute audio button label',
-  },
-  unmuteLabel: {
-    id: 'app.actionsBar.unmuteLabel',
-    description: 'Unmute audio button label',
-  },
-});
-
-const MuteAudio = ({ intl, toggleSelfVoice, voiceUserData }) => {
-  const { isInAudio, isMuted, isTalking, listenOnly } = voiceUserData;
-
-  if (!isInAudio || listenOnly) return null;
-  const muteLabel = intl.formatMessage(intlMessages.muteLabel);
-  const unmuteLabel = intl.formatMessage(intlMessages.unmuteLabel);
-
-  const label = !isMuted ? muteLabel : unmuteLabel;
-  const icon = !isMuted ? 'unmute' : 'mute';
-  const tabIndex = !isInAudio ? -1 : 0;
-  let className = null;
-
-  if (isInAudio && isTalking) {
-    className = styles.circleGlow;
-  }
-
-  return (
-    <Button
-      onClick={toggleSelfVoice}
-      label={label}
-      color={'primary'}
-      icon={icon}
-      size={'lg'}
-      circle
-      className={className}
-      tabIndex={tabIndex}
-    />
-  );
-};
-
-export default injectIntl(MuteAudio);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx
deleted file mode 100644
index 4e5437b42472ff139db12e854c8b0af9313ce2ca..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import MuteAudio from './component';
-import MuteAudioService from '../service';
-
-const MuteAudioContainer = props => (<MuteAudio {...props} />);
-
-export default createContainer(() => ({
-  toggleSelfVoice: MuteAudioService.toggleSelfVoice,
-  voiceUserData: MuteAudioService.getVoiceUserData(),
-}), MuteAudioContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index b74ee7dfa20d98d04c4c65c2cabcd73f2cd340e3..dbea4fdf5f5e29ee97d4a60e1b58ce12d6a4e679 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -24,7 +24,3 @@
 .center {
   align-items: center;
 }
-
-.circleGlow > :first-child{
-    box-shadow: 0 0 .15rem #FFF !important;
-}
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index f3c0b2440aaf21e1820f893783ff4e0ecb4c4b82..0949efcc97f0d24dfda41c6a5ae5b06ac152fa9e 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -7,7 +7,6 @@ import cx from 'classnames';
 import ToastContainer from '../toast/container';
 import ModalContainer from '../modal/container';
 import NotificationsBarContainer from '../notifications-bar/container';
-import AudioNotificationContainer from '../audio/audio-notification/container';
 import AudioContainer from '../audio/container';
 import ChatNotificationContainer from '../chat/notification/container';
 import styles from './styles';
@@ -176,7 +175,6 @@ class App extends Component {
 
     return (
       <main className={styles.main}>
-        <AudioNotificationContainer />
         <NotificationsBarContainer />
         <section className={styles.wrapper}>
           {this.renderUserList()}
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 0d0d3430c7b224b84f0afc5e49ac09c1a9de894d..88abd7c9b6e1e4d076700b4a8e27247437c05537 100644
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -75,6 +75,7 @@ const AppContainer = (props) => {
 export default withRouter(injectIntl(withModalMounter(createContainer((
   { router, intl, baseControls }) => {
   const currentUser = Users.findOne({ userId: Auth.userID });
+  const isMeetingBreakout = meetingIsBreakout();
 
   if (!currentUser.approved) {
     baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
@@ -101,9 +102,8 @@ export default withRouter(injectIntl(withModalMounter(createContainer((
   // forcelly logged out when the meeting is ended
   Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
     removed() {
-      if (!meetingIsBreakout) {
-        sendToError(410, intl.formatMessage(intlMessages.endMeetingMessage));
-      }
+      if (isMeetingBreakout) return;
+      sendToError(410, intl.formatMessage(intlMessages.endMeetingMessage));
     },
   });
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3d6a95c3277c2ea25cd03f02a202b0d82a3d7499
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Button from '/imports/ui/components/button/component';
+import styles from './styles';
+
+const propTypes = {
+  handleToggleMuteMicrophone: PropTypes.func.isRequired,
+  handleJoinAudio: PropTypes.func.isRequired,
+  handleLeaveAudio: PropTypes.func.isRequired,
+  disable: PropTypes.bool.isRequired,
+  unmute: PropTypes.bool.isRequired,
+  mute: PropTypes.bool.isRequired,
+  join: PropTypes.bool.isRequired,
+};
+
+const AudioControls = ({
+  handleToggleMuteMicrophone,
+  handleJoinAudio,
+  handleLeaveAudio,
+  mute,
+  unmute,
+  disable,
+  join,
+}) => (
+  <span className={styles.container}>
+    {mute ?
+      <Button
+        className={styles.button}
+        onClick={handleToggleMuteMicrophone}
+        disabled={disable}
+        label={unmute ? 'Unmute' : 'Mute'}
+        color={'primary'}
+        icon={unmute ? 'mute' : 'unmute'}
+        size={'lg'}
+        circle
+      /> : null}
+    <Button
+      className={styles.button}
+      onClick={join ? handleLeaveAudio : handleJoinAudio}
+      disabled={disable}
+      label={join ? 'Leave Audio' : 'Join Audio'}
+      color={join ? 'danger' : 'primary'}
+      icon={join ? 'audio_off' : 'audio_on'}
+      size={'lg'}
+      circle
+    />
+  </span>);
+
+AudioControls.propTypes = propTypes;
+
+export default AudioControls;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..63504178b3a490018099b45a90c7280ddec7b2af
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import AudioControls from './component';
+import AudioModalContainer from '../audio-modal/container';
+import Service from '../service';
+
+const AudioControlsContainer = props => <AudioControls {...props} />;
+
+export default withModalMounter(createContainer(({ mountModal }) =>
+   ({
+     mute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest(),
+     unmute: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(),
+     join: Service.isConnected() && !Service.isEchoTest(),
+     disable: Service.isConnecting() || Service.isHangingUp(),
+     handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
+     handleJoinAudio: () => mountModal(<AudioModalContainer />),
+     handleLeaveAudio: () => Service.exitAudio(),
+   }), AudioControlsContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b29874dc65d7e6eb7d5baafffd8e17ed996b035c
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
@@ -0,0 +1,22 @@
+.container {
+  display: flex;
+  flex-flow: row;
+
+  > * {
+    margin: 0 1rem;
+
+    span:first-child {
+      box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
+    }
+  }
+
+  > :last-child {
+    margin-right: 0;
+  }
+}
+
+.button {
+  &:focus {
+    outline: none !important;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx
deleted file mode 100644
index 68a107185166324d65bee740af82faf6d43d2a55..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import Button from '/imports/ui/components/button/component';
-import { withRouter } from 'react-router';
-import { defineMessages, injectIntl } from 'react-intl';
-import AudioManager from '/imports/api/audio/client/manager';
-
-const intlMessages = defineMessages({
-  joinAudio: {
-    id: 'app.audio.joinAudio',
-    description: 'Join audio button label',
-  },
-  leaveAudio: {
-    id: 'app.audio.leaveAudio',
-    description: 'Leave audio button label',
-  },
-});
-
-class JoinAudioOptions extends React.Component {
-  render() {
-    const {
-      intl,
-      isInAudio,
-      isInListenOnly,
-      handleJoinAudio,
-      handleCloseAudio,
-    } = this.props;
-
-    if (isInAudio || isInListenOnly) {
-      if (AudioManager.currentState == AudioManager.callStates.inConference ||
-      AudioManager.currentState == AudioManager.callStates.inListenOnly) {
-        return (
-          <Button
-            onClick={handleCloseAudio}
-            label={intl.formatMessage(intlMessages.leaveAudio)}
-            color={'danger'}
-            icon={'audio_off'}
-            size={'lg'}
-            circle
-          />
-        );
-      }
-    }
-
-    return (
-      <Button
-        onClick={handleJoinAudio}
-        label={intl.formatMessage(intlMessages.joinAudio)}
-        color={'primary'}
-        icon={'audio_on'}
-        size={'lg'}
-        circle
-      />
-    );
-  }
-}
-
-export default withRouter(injectIntl(JoinAudioOptions));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx
deleted file mode 100644
index 26c410c557f703924e92723562d48b0d10ff9f09..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import VoiceUsers from '/imports/api/voice-users';
-import Auth from '/imports/ui/services/auth/index';
-import JoinAudioOptions from './component';
-
-const JoinAudioOptionsContainer = props => (<JoinAudioOptions {...props} />);
-
-export default createContainer((params) => {
-  const userId = Auth.userID;
-  const voiceUser = VoiceUsers.findOne({ intId: userId });
-
-  const { joined, listenOnly } = voiceUser;
-
-  return {
-    isInAudio: joined,
-    isInListenOnly: listenOnly,
-    handleJoinAudio: params.handleJoinAudio,
-    handleCloseAudio: params.handleCloseAudio,
-  };
-}, JoinAudioOptionsContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
index 2685e511b49698d37e7a0a14c9eb0abaacf16c64..c063971081b4552ebb0d6cc77a70dbe7db82889f 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -1,54 +1,287 @@
-import React from 'react';
-import ModalBase from '../../modal/base/component';
-import styles from './styles.scss';
-import JoinAudio from '../join-audio/component';
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import ModalBase from '/imports/ui/components/modal/base/component';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
+import styles from './styles';
 import AudioSettings from '../audio-settings/component';
+import EchoTest from '../echo-test/component';
 
-export default class AudioModal extends React.Component {
+const propTypes = {
+  intl: intlShape.isRequired,
+  closeModal: PropTypes.func.isRequired,
+  joinMicrophone: PropTypes.func.isRequired,
+  joinListenOnly: PropTypes.func.isRequired,
+  joinEchoTest: PropTypes.func.isRequired,
+  exitAudio: PropTypes.func.isRequired,
+  leaveEchoTest: PropTypes.func.isRequired,
+  changeInputDevice: PropTypes.func.isRequired,
+  changeOutputDevice: PropTypes.func.isRequired,
+  isEchoTest: PropTypes.bool.isRequired,
+  isConnecting: PropTypes.bool.isRequired,
+  isConnected: PropTypes.bool.isRequired,
+  inputDeviceId: PropTypes.string,
+  outputDeviceId: PropTypes.string,
+};
+
+const defaultProps = {
+  inputDeviceId: null,
+  outputDeviceId: null,
+};
+
+const intlMessages = defineMessages({
+  microphoneLabel: {
+    id: 'app.audioModal.microphoneLabel',
+    description: 'Join mic audio button label',
+  },
+  listenOnlyLabel: {
+    id: 'app.audioModal.listenOnlyLabel',
+    description: 'Join listen only audio button label',
+  },
+  closeLabel: {
+    id: 'app.audioModal.closeLabel',
+    description: 'close audio modal button label',
+  },
+  audioChoiceLabel: {
+    id: 'app.audioModal.audioChoiceLabel',
+    description: 'Join audio modal title',
+  },
+  echoTestTitle: {
+    id: 'app.audioModal.echoTestTitle',
+    description: 'Title for the echo test',
+  },
+  settingsTitle: {
+    id: 'app.audioModal.settingsTitle',
+    description: 'Title for the audio modal',
+  },
+  connecting: {
+    id: 'app.audioModal.connecting',
+    description: 'Message for audio connecting',
+  },
+  connectingEchoTest: {
+    id: 'app.audioModal.connectingEchoTest',
+    description: 'Message for echo test connecting',
+  },
+});
+
+class AudioModal extends Component {
   constructor(props) {
     super(props);
 
-    this.JOIN_AUDIO = 0;
-    this.AUDIO_SETTINGS = 1;
+    this.state = {
+      content: null,
+    };
+
+    const {
+      intl,
+      closeModal,
+      joinListenOnly,
+      joinEchoTest,
+      exitAudio,
+      leaveEchoTest,
+      changeInputDevice,
+      changeOutputDevice,
+    } = props;
+
+    this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this);
+    this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this);
+    this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this);
+    this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
+    this.closeModal = closeModal;
+    this.handleJoinListenOnly = joinListenOnly;
+    this.joinEchoTest = joinEchoTest;
+    this.exitAudio = exitAudio;
+    this.leaveEchoTest = leaveEchoTest;
+    this.changeInputDevice = changeInputDevice;
+    this.changeOutputDevice = changeOutputDevice;
+
+    this.contents = {
+      echoTest: {
+        title: intl.formatMessage(intlMessages.echoTestTitle),
+        component: () => this.renderEchoTest(),
+      },
+      settings: {
+        title: intl.formatMessage(intlMessages.settingsTitle),
+        component: () => this.renderAudioSettings(),
+      },
+    };
+  }
+
+  componentWillUnmount() {
+    const {
+      isEchoTest,
+    } = this.props;
 
-    this.submenus = [];
+    if (isEchoTest) {
+      this.exitAudio();
+    }
   }
 
-  componentWillMount() {
-    /* activeSubmenu represents the submenu in the submenus array to be displayed to the user,
-     * initialized to 0
-     */
-    this.setState({ activeSubmenu: 0 });
-    this.submenus.push({ componentName: JoinAudio });
-    this.submenus.push({ componentName: AudioSettings });
+  handleGoToAudioOptions() {
+    this.setState({
+      content: null,
+    });
   }
 
-  handleSubmenuChange(i) {
-    this.setState({ activeSubmenu: i });
+  handleGoToAudioSettings() {
+    this.leaveEchoTest().then(() => {
+      this.setState({
+        content: 'settings',
+      });
+    });
   }
 
-  renderSubmenu(key) {
-    const curr = this.state.activeSubmenu ? 0 : this.state.activeSubmenu;
+  handleGoToEchoTest() {
+    this.joinEchoTest().then(() => {
+      this.setState({
+        content: 'echoTest',
+      });
+    });
+  }
 
-    const props = {
-      changeMenu: this.handleSubmenuChange.bind(this),
-      JOIN_AUDIO: this.JOIN_AUDIO,
-      AUDIO_SETTINGS: this.AUDIO_SETTINGS,
-      LISTEN_ONLY: this.LISTEN_ONLY,
-      handleJoinListenOnly: this.props.handleJoinListenOnly,
-    };
+  handleJoinMicrophone() {
+    const {
+      joinMicrophone,
+    } = this.props;
 
-    const Submenu = this.submenus[key].componentName;
-    return <Submenu {...props} />;
+    joinMicrophone().catch(this.handleGoToAudioOptions);
+  }
+
+  renderAudioOptions() {
+    const {
+      intl,
+    } = this.props;
+
+    return (
+      <span>
+        <Button
+          className={styles.audioBtn}
+          label={intl.formatMessage(intlMessages.microphoneLabel)}
+          icon={'unmute'}
+          circle
+          size={'jumbo'}
+          onClick={this.handleGoToEchoTest}
+        />
+        <Button
+          className={styles.audioBtn}
+          label={intl.formatMessage(intlMessages.listenOnlyLabel)}
+          icon={'listen'}
+          circle
+          size={'jumbo'}
+          onClick={this.handleJoinListenOnly}
+        />
+      </span>
+    );
+  }
+
+  renderContent() {
+    const {
+      isConnecting,
+      isEchoTest,
+      intl,
+    } = this.props;
+
+    const {
+      content,
+    } = this.state;
+
+    if (isConnecting) {
+      return (
+        <span className={styles.connecting}>
+          { !isEchoTest ?
+            intl.formatMessage(intlMessages.connecting) :
+            intl.formatMessage(intlMessages.connectingEchoTest)
+          }
+        </span>
+      );
+    }
+    return content ? this.contents[content].component() : this.renderAudioOptions();
+  }
+
+  renderEchoTest() {
+    const {
+      isConnecting,
+    } = this.props;
+
+    return (
+      <EchoTest
+        isConnecting={isConnecting}
+        joinEchoTest={this.joinEchoTest}
+        leaveEchoTest={this.leaveEchoTest}
+        handleNo={this.handleGoToAudioSettings}
+        handleYes={this.handleJoinMicrophone}
+      />
+    );
+  }
+
+  renderAudioSettings() {
+    const {
+      isConnecting,
+      isConnected,
+      isEchoTest,
+      inputDeviceId,
+      outputDeviceId,
+    } = this.props;
+
+    return (
+      <AudioSettings
+        handleBack={this.handleGoToAudioOptions}
+        handleRetry={this.handleGoToEchoTest}
+        joinEchoTest={this.joinEchoTest}
+        exitAudio={this.exitAudio}
+        changeInputDevice={this.changeInputDevice}
+        changeOutputDevice={this.changeOutputDevice}
+        isConnecting={isConnecting}
+        isConnected={isConnected}
+        isEchoTest={isEchoTest}
+        inputDeviceId={inputDeviceId}
+        outputDeviceId={outputDeviceId}
+      />
+    );
   }
 
   render() {
+    const {
+      intl,
+      isConnecting,
+    } = this.props;
+
+    const {
+      content,
+    } = this.state;
+
     return (
-      <ModalBase overlayClassName={styles.overlay} className={styles.modal}>
-        <div>
-          {this.renderSubmenu(this.state.activeSubmenu)}
+      <ModalBase
+        overlayClassName={styles.overlay}
+        className={styles.modal}
+        onRequestClose={this.closeModal}
+      >
+        { isConnecting ? null :
+        <header className={styles.header}>
+          <h3 className={styles.title}>
+            { content ?
+              this.contents[content].title :
+              intl.formatMessage(intlMessages.audioChoiceLabel)}
+          </h3>
+          <Button
+            className={styles.closeBtn}
+            label={intl.formatMessage(intlMessages.closeLabel)}
+            icon={'close'}
+            size={'md'}
+            hideLabel
+            onClick={this.closeModal}
+          />
+        </header>
+        }
+        <div className={styles.content}>
+          { this.renderContent() }
         </div>
       </ModalBase>
     );
   }
 }
+
+AudioModal.propTypes = propTypes;
+AudioModal.defaultProps = defaultProps;
+
+export default injectIntl(AudioModal);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..27d11d6302d0e06e17f3c5af452fcdee54f83380
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import AudioModal from './component';
+import Service from '../service';
+
+const AudioModalContainer = props => <AudioModal {...props} />;
+
+export default withModalMounter(createContainer(({ mountModal }) =>
+   ({
+     closeModal: () => {
+       if (!Service.isConnecting()) mountModal(null);
+     },
+     joinMicrophone: () =>
+       new Promise((resolve, reject) => {
+         Service.transferCall().then(() => {
+           mountModal(null);
+           resolve();
+         }).catch(() => {
+           Service.exitAudio();
+           reject();
+         });
+       }),
+     joinListenOnly: () => Service.joinListenOnly().then(() => mountModal(null)),
+     leaveEchoTest: () => {
+       if (!Service.isEchoTest()) {
+         return Promise.resolve();
+       }
+       return Service.exitAudio();
+     },
+     changeInputDevice: inputDeviceId => Service.changeInputDevice(inputDeviceId),
+     changeOutputDevice: outputDeviceId => Service.changeOutputDevice(outputDeviceId),
+     joinEchoTest: () => Service.joinEchoTest(),
+     exitAudio: () => Service.exitAudio(),
+     isConnecting: Service.isConnecting(),
+     isConnected: Service.isConnected(),
+     isEchoTest: Service.isEchoTest(),
+     inputDeviceId: Service.inputDeviceId(),
+     outputDeviceId: Service.outputDeviceId(),
+   }), AudioModalContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
index 8b5cd2048c1ac4a5f409db2e1d8ee573ae1c07b1..0d3e497c58bbc4476cad217521cab43248737039 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
@@ -1,189 +1,141 @@
 @import "/imports/ui/stylesheets/variables/_all";
 @import "/imports/ui/components/modal/simple/styles";
 
-.overlay {
-  @extend .overlay;
-}
-
-.modal {
-  @extend .modal;
+.header {
+  margin: 0;
+  padding: 0;
+  border: none;
+  line-height: 2rem;
 }
 
-.center {
+.content {
   display: flex;
   justify-content: center;
-  align-items: center;
-  padding: 2rem 0;
+  padding: 0;
+  margin-top: auto;
+  margin-bottom: auto;
+  padding: 0.5rem 0;
+
+  .audioBtn:first-child {
+    margin-right: 3rem;
+
+    @include mq($small-only) {
+      margin-right: 1rem;
+    }
+  }
 }
 
-.closeBtnWrapper {
-   display: flex;
-   justify-content: flex-end;
+.overlay {
+  @extend .overlay;
+}
+
+.modal {
+  @extend .modal;
+  padding: 1.5rem;
+  min-height: 20rem;
 }
 
 .closeBtn {
+  right: 0;
+  top: 0;
+  position: absolute;
   background-color: $color-white;
   border: none;
+  padding: .75rem;
+
   i {
     color: $color-gray-light;
   }
 
   &:focus,
   &:hover{
-    background-color: #0a5eac;
+    background-color: $color-white;
     i{
-      color: $color-white;
+      color: $color-primary;
     }
   }
 }
 
-Button.audioBtn {
+.audioBtn {
+  &:focus {
+    outline: none !important;
+  }
+
   i{
     color: #3c5764;
   }
 }
 
 // Modifies the audio button icon colour
-Button.audioBtn span:first-child {
+.audioBtn span:first-child {
   color: #1b3c4b;
   background-color: #f1f8ff;
   box-shadow: none;
   border: 5px solid  #f1f8ff;
+  font-size: 3.5rem;
+
+  @include mq($small-only) {
+    font-size: 2.5rem;
+  }
 }
 
 // When hovering over a button of class audioBtn, change the border colour of first span-child
-Button.audioBtn:hover span:first-child, 
-Button.audioBtn:focus span:first-child {
+.audioBtn:hover span:first-child,
+.audioBtn:focus span:first-child {
   border: 5px solid $color-primary;
   background-color: #f1f8ff;
 }
 
 // Modifies the button label text
-Button.audioBtn span:last-child {
+.audioBtn span:last-child {
   color: black;
-  font-size: 0.8rem;
+  font-size: 1rem;
   font-weight: 600;
 }
 
-Button.audioBtn:first-of-type {
-  margin-right: 5%;
-}
-
-Button.audioBtn:last-of-type {
-  margin-left: 5%;
-}
-
-.backBtn {
-  border: none;
-  i {
-    color: $color-link;
-  }
-
-  &,
-  &:focus,
-  &:hover {
-    i {
-      color: $color-white;
-    }
-  }
-}
-
-.topRow {
-  align-items: center;
-  display: flex;
-}
-
-.audioNote {
-  color: $color-text;
-  display: inline-block;
-  font-size: 0.9rem;
-}
-
 .title {
   text-align: center;
-  margin: auto;
-  color: black;
   font-weight: 400;
   font-size: 1.3rem;
-  display: block;
-}
+  white-space: normal;
 
-.form {
-  display: flex;
-  flex-flow: column;
-  padding: 2em;
+  @include mq($small-only) {
+    font-size: 1rem;
+    padding: 0 1rem;
+  }
 }
 
-.row {
-  display: flex;
-  flex-flow: row;
-  flex-grow: 1;
-  justify-content: space-between;
-  margin-bottom: 0.7rem;
+.connecting {
+  font-size: 2rem;
 }
 
-.col {
-  display: flex;
-  flex-grow: 1;
-  flex-basis: 0;
-  margin-right: 1rem;
+.connecting:after {
+  overflow: hidden;
+  display: inline-block;
+  vertical-align: bottom;
+  -webkit-animation: ellipsis steps(4,end) 900ms infinite;
+  animation: ellipsis steps(4,end) 900ms infinite;
+  content: "\2026"; /* ascii code for the ellipsis character */
+  width: 0;
+  margin-right: 1.25em;
+}
 
-  &:last-child {
+@keyframes ellipsis {
+  to {
+    width: 1.25em;
     margin-right: 0;
-    padding-right: 0.1rem;
-    padding-left: 4rem;
   }
 }
 
-.labelSmall {
-  color: black;
-  font-size: 0.7rem;
-  font-weight: 600;
-  margin-bottom: 0.3rem;
-}
-
-.formElement {
-  position: relative;
-  display: flex;
-  flex-flow: column;
-  flex-grow: 1;
+@-webkit-keyframes ellipsis {
+  to {
+    width: 1.25em;
+    margin-right: 0;
+  }
 }
 
-.select {
-  @extend %customSelectFocus;
-  background-color: $color-white;
-  border: 0;
-  border-bottom: 0.1rem solid $color-text;
+.audioNote {
   color: $color-text;
-  width: 100%;
-  // appearance: none;
-  height: 1.75rem;
-}
-
-.audioMeter {
-  width: 100%;
-}
-
-.pullContentRight {
-  display: flex;
-  justify-content: flex-end;
-  flex-flow: row;
-}
-
-.verticalLine {
-  color: #f3f6f9;
-  border-left: 1px solid;
-  height: 5rem;
-}
-
-.enterAudio {
-  display: flex;
-  justify-content: flex-end;
-  margin-right: 2rem;
-}
-
-.chooseAudio {
-  position:absolute;
-  left:50%;
-  transform: translate(-50%, 0);
-}
+  display: inline-block;
+  font-size: 0.9rem;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx
deleted file mode 100644
index d0c3d1823f4e599bb3b2bb8cbe713407fc14578f..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import styles from './styles.scss';
-import cx from 'classnames';
-import Button from '/imports/ui/components/button/component';
-
-const COLORS = [
-  'default', 'primary', 'danger', 'success',
-];
-
-const propTypes = {
-  color: PropTypes.oneOf(COLORS),
-  message: PropTypes.string,
-};
-
-const defaultProps = {
-  color: 'default',
-};
-
-const intlMessages = defineMessages({
-  closeLabel: {
-    id: 'app.audioNotification.closeLabel',
-    description: 'Audio notification dismiss label',
-  },
-});
-
-class AudioNotification extends Component {
-  constructor(props) {
-    super(props);
-
-    this.handleClose = this.handleClose.bind(this);
-  }
-
-  handleClose() {
-    this.props.handleClose();
-  }
-
-  render() {
-    const {
-      color,
-      message,
-      intl,
-    } = this.props;
-
-    if (!color || !message) {
-      return null;
-    }
-    return (
-      <div
-        role="alert"
-        className={cx(styles.audioNotifications, styles[this.props.color])}
-      >
-        {message}
-        <Button
-          className={styles.closeBtn}
-          label={intl.formatMessage(intlMessages.closeLabel)}
-          icon={'close'}
-          size={'sm'}
-          circle
-          hideLabel
-          onClick={this.handleClose}
-        />
-      </div>
-    );
-  }
-}
-
-AudioNotification.propTypes = propTypes;
-AudioNotification.defaultProps = defaultProps;
-
-export default injectIntl(AudioNotification);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx
deleted file mode 100644
index ec6a7de4ccdda937b22104eefd03535796531211..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { createContainer } from 'meteor/react-meteor-data';
-import React, { Component } from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import AudioNotification from './component';
-import AudioManager, { AudioErrorCodes } from '/imports/api/audio/client/manager';
-
-const intlMessages = defineMessages({
-  [AudioErrorCodes.CODE_1001]: {
-    id: 'app.audioNotification.audioFailedError1001',
-    description: 'Audio connection failed with error 1001',
-  },
-  [AudioErrorCodes.CODE_1002]: {
-    id: 'app.audioNotification.audioFailedError1002',
-    description: 'Audio connection failed with error 1002',
-  },
-  [AudioErrorCodes.CODE_1003]: {
-    id: 'app.audioNotification.audioFailedError1003',
-    description: 'Audio connection failed with error 1003',
-  },
-  [AudioErrorCodes.CODE_1004]: {
-    id: 'app.audioNotification.audioFailedError1004',
-    description: 'Audio connection failed with error 1004',
-  },
-  [AudioErrorCodes.CODE_1005]: {
-    id: 'app.audioNotification.audioFailedError1005',
-    description: 'Audio connection failed with error 1005',
-  },
-  [AudioErrorCodes.CODE_1006]: {
-    id: 'app.audioNotification.audioFailedError1006',
-    description: 'Audio connection failed with error 1006',
-  },
-  [AudioErrorCodes.CODE_1007]: {
-    id: 'app.audioNotification.audioFailedError1007',
-    description: 'Audio connection failed with error 1007',
-  },
-  [AudioErrorCodes.CODE_1008]: {
-    id: 'app.audioNotification.audioFailedError1008',
-    description: 'Audio connection failed with error 1008',
-  },
-  [AudioErrorCodes.CODE_1009]: {
-    id: 'app.audioNotification.audioFailedError1009',
-    description: 'Audio connection failed with error 1009',
-  },
-  [AudioErrorCodes.CODE_1010]: {
-    id: 'app.audioNotification.audioFailedError1010',
-    description: 'Audio connection failed with error 1010',
-  },
-  [AudioErrorCodes.CODE_1011]: {
-    id: 'app.audioNotification.audioFailedError1011',
-    description: 'Audio connection failed with error 1011',
-  },
-  audioFailed: {
-    id: 'app.audioNotification.audioFailedMessage',
-    description: 'The audio could not connect',
-  },
-  mediaFailed: {
-    id: 'app.audioNotification.mediaFailedMessage',
-    description: 'Could not access getUserMicMedia',
-  },
-});
-
-class AudioNotificationContainer extends Component {
-  constructor(props) {
-    super(props);
-
-    this.color = null;
-    this.message = null;
-
-    this.state = {
-      status: null,
-    };
-
-    this.handleAudioFailure = this.handleAudioFailure.bind(this);
-    this.handleMediaFailure = this.handleMediaFailure.bind(this);
-    this.handleClose = this.handleClose.bind(this);
-  }
-
-  componentDidMount() {
-    window.addEventListener('bbb.webrtc.failed', this.handleAudioFailure);
-    window.addEventListener('bbb.webrtc.mediaFailed', this.handleMediaFailure);
-    window.addEventListener('bbb.webrtc.connected', this.handleClose);
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('bbb.webrtc.failed', this.handleAudioFailure);
-    window.removeEventListener('bbb.webrtc.mediaFailed', this.handleMediaFailure);
-    window.removeEventListener('bbb.webrtc.connected', this.handleClose);
-  }
-
-  handleClose() {
-    this.color = null;
-    this.message = null;
-    this.setState({ status: null });
-  }
-
-  handleAudioFailure(e) {
-    this.message = this.props.messages[e.detail.errorCode];
-    if (this.message == null || this.message == undefined) {
-      this.message = this.props.audioFailure;
-    }
-    this.setState({ status: 'failed' });
-  }
-
-  handleMediaFailure() {
-    this.message = this.props.mediaFailure;
-    this.setState({ status: 'failed' });
-  }
-
-  render() {
-    const handleClose = this.handleClose;
-    this.color = 'danger';
-
-    return (
-      <AudioNotification
-        color={this.color}
-        message={this.message}
-        handleClose={handleClose}
-      />
-    );
-  }
-}
-
-export default injectIntl(createContainer(({ intl }) => {
-  const messages = {};
-  messages[AudioErrorCodes.CODE_1001] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1001]);
-  messages[AudioErrorCodes.CODE_1002] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1002]);
-  messages[AudioErrorCodes.CODE_1003] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1003]);
-  messages[AudioErrorCodes.CODE_1004] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1004]);
-  messages[AudioErrorCodes.CODE_1005] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1005]);
-  messages[AudioErrorCodes.CODE_1006] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1006]);
-  messages[AudioErrorCodes.CODE_1007] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1007]);
-  messages[AudioErrorCodes.CODE_1008] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1008]);
-  messages[AudioErrorCodes.CODE_1009] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1009]);
-  messages[AudioErrorCodes.CODE_1010] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1010]);
-  messages[AudioErrorCodes.CODE_1011] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1011]);
-  messages.audioFailure = intl.formatMessage(intlMessages.audioFailed);
-  messages.mediaFailure = intl.formatMessage(intlMessages.mediaFailed);
-
-  return { messages };
-}, AudioNotificationContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss
deleted file mode 100644
index 1033621bbdba24af89257aea1d57bf8a0342931c..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-@import "/imports/ui/stylesheets/variables/_all";
-
-$nb-default-color: $color-gray;
-$nb-default-bg: $color-white;
-$nb-default-border: $color-white;
-
-$nb-primary-color: $color-white;
-$nb-primary-bg: $color-primary;
-$nb-primary-border: $color-primary;
-
-$nb-success-color: $color-white;
-$nb-success-bg: $color-success;
-$nb-success-border: $color-success;
-
-$nb-danger-color: $color-white;
-$nb-danger-bg: $color-danger;
-$nb-danger-border: $color-danger;
-
-.audioNotifications {
-  padding: $line-height-computed / 2;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-  font-weight: 600;
-}
-
-.closeBtn {
-  position: absolute;
-  right: 1.65em;
-  top: .5em;
-}
-
-// Modifies the close button style
-Button.closeBtn span:first-child {
-  color: $color-gray-light;
-  background: none;
-  border: none;
-  box-shadow: none;
-}
-
-
-@mixin nb-variant($color, $background, $border) {
-  color: $color;
-  background-color: $background;
-  border-color: $border;
-}
-
-.default {
-  @include nb-variant($nb-default-color, $nb-default-bg, $nb-default-border);
-}
-
-.primary {
-  @include nb-variant($nb-primary-color, $nb-primary-bg, $nb-primary-border);
-}
-
-.success {
-  @include nb-variant($nb-success-color, $nb-success-bg, $nb-success-border);
-}
-
-.danger {
-  @include nb-variant($nb-danger-color, $nb-danger-bg, $nb-danger-border);
-}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
index 3c76e00120a3311d1b6a387e9dce536b9b774fd0..6e6534192d45e5c9ba88eb4976acb4243f38b2ca 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
@@ -1,73 +1,103 @@
 import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import Button from '/imports/ui/components/button/component';
 import { withModalMounter } from '/imports/ui/components/modal/service';
-import styles from '../audio-modal/styles.scss';
-
 import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
-import AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
-import EnterAudioContainer from '/imports/ui/components/audio/enter-audio/container';
 import AudioTestContainer from '/imports/ui/components/audio/audio-test/container';
 import cx from 'classnames';
+import styles from './styles';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  exitAudio: PropTypes.func.isRequired,
+  changeInputDevice: PropTypes.func.isRequired,
+  changeOutputDevice: PropTypes.func.isRequired,
+  handleBack: PropTypes.func.isRequired,
+  handleRetry: PropTypes.func.isRequired,
+  isConnecting: PropTypes.bool.isRequired,
+  inputDeviceId: PropTypes.string.isRequired,
+  outputDeviceId: PropTypes.string.isRequired,
+};
+
+const intlMessages = defineMessages({
+  backLabel: {
+    id: 'app.audio.backLabel',
+    description: 'audio settings back button label',
+  },
+  descriptionLabel: {
+    id: 'app.audio.audioSettings.descriptionLabel',
+    description: 'audio settings description label',
+  },
+  micSourceLabel: {
+    id: 'app.audio.audioSettings.microphoneSourceLabel',
+    description: 'Label for mic source',
+  },
+  speakerSourceLabel: {
+    id: 'app.audio.audioSettings.speakerSourceLabel',
+    description: 'Label for speaker source',
+  },
+  streamVolumeLabel: {
+    id: 'app.audio.audioSettings.microphoneStreamLabel',
+    description: 'Label for stream volume',
+  },
+  retryLabel: {
+    id: 'app.audio.audioSettings.retryLabel',
+    description: 'Retry button label',
+  },
+});
 
 class AudioSettings extends React.Component {
   constructor(props) {
     super(props);
 
-    this.chooseAudio = this.chooseAudio.bind(this);
+    const {
+      inputDeviceId,
+      outputDeviceId,
+    } = props;
+
     this.handleInputChange = this.handleInputChange.bind(this);
     this.handleOutputChange = this.handleOutputChange.bind(this);
-    this.handleClose = this.handleClose.bind(this);
 
     this.state = {
-      inputDeviceId: undefined,
+      inputDeviceId,
+      outputDeviceId,
     };
   }
 
-  chooseAudio() {
-    this.props.changeMenu(this.props.JOIN_AUDIO);
-  }
-
   handleInputChange(deviceId) {
-    console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
+    const {
+      changeInputDevice,
+    } = this.props;
+
+    changeInputDevice(deviceId);
     this.setState({
       inputDeviceId: deviceId,
     });
   }
 
   handleOutputChange(deviceId) {
-    console.log(`OUTPUT DEVICE CHANGED: ${deviceId}`);
-  }
+    const {
+      changeOutputDevice,
+    } = this.props;
 
-  handleClose() {
-    this.setState({ isOpen: false });
-    this.props.mountModal(null);
+    changeOutputDevice(deviceId);
+    this.setState({
+      outputDeviceId: deviceId,
+    });
   }
 
   render() {
     const {
+      isConnecting,
       intl,
+      handleBack,
+      handleRetry,
     } = this.props;
 
     return (
       <div>
-        <div className={styles.topRow}>
-          <Button
-            className={styles.backBtn}
-            label={intl.formatMessage(intlMessages.backLabel)}
-            icon={'left_arrow'}
-            size={'md'}
-            color={'primary'}
-            ghost
-            onClick={this.chooseAudio}
-          />
-          <div className={cx(styles.title, styles.chooseAudio)}>
-            {intl.formatMessage(intlMessages.titleLabel)}
-          </div>
-        </div>
-
         <div className={styles.form}>
-
           <div className={styles.row}>
             <div className={styles.audioNote}>
               {intl.formatMessage(intlMessages.descriptionLabel)}
@@ -77,9 +107,13 @@ class AudioSettings extends React.Component {
           <div className={styles.row}>
             <div className={styles.col}>
               <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
+                <label
+                  htmlFor="inputDeviceSelector"
+                  className={cx(styles.label, styles.labelSmall)}
+                >
                   {intl.formatMessage(intlMessages.micSourceLabel)}
                   <DeviceSelector
+                    id="inputDeviceSelector"
                     value={this.state.inputDeviceId}
                     className={styles.select}
                     kind="audioinput"
@@ -90,9 +124,13 @@ class AudioSettings extends React.Component {
             </div>
             <div className={styles.col}>
               <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
+                <label
+                  htmlFor="outputDeviceSelector"
+                  className={cx(styles.label, styles.labelSmall)}
+                >
                   {intl.formatMessage(intlMessages.speakerSourceLabel)}
                   <DeviceSelector
+                    id="outputDeviceSelector"
                     value={this.state.outputDeviceId}
                     className={styles.select}
                     kind="audiooutput"
@@ -104,57 +142,42 @@ class AudioSettings extends React.Component {
           </div>
 
           <div className={styles.row}>
-            <div className={styles.col}>
-              <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
-                  {intl.formatMessage(intlMessages.streamVolumeLabel)}
-                  <AudioStreamVolume
-                    deviceId={this.state.inputDeviceId}
-                    className={styles.audioMeter}
-                  />
-                </label>
-              </div>
-            </div>
-            <div className={styles.col}>
-              <label className={styles.label}> </label>
-              <AudioTestContainer />
+            <div className={cx(styles.col, styles.spacedLeft)}>
+              <label
+                htmlFor="audioTest"
+                className={styles.labelSmall}
+              >
+                Test your speaker volume
+                <AudioTestContainer id="audioTest" />
+              </label>
             </div>
           </div>
         </div>
 
+
         <div className={styles.enterAudio}>
-          <EnterAudioContainer isFullAudio />
+          <Button
+            className={styles.backBtn}
+            label={intl.formatMessage(intlMessages.backLabel)}
+            size={'md'}
+            color={'primary'}
+            onClick={handleBack}
+            disabled={isConnecting}
+            ghost
+          />
+          <Button
+            size={'md'}
+            color={'primary'}
+            label={intl.formatMessage(intlMessages.retryLabel)}
+            onClick={handleRetry}
+          />
         </div>
       </div>
     );
   }
+
 }
 
-const intlMessages = defineMessages({
-  backLabel: {
-    id: 'app.audio.backLabel',
-    description: 'audio settings back button label',
-  },
-  titleLabel: {
-    id: 'app.audio.audioSettings.titleLabel',
-    description: 'audio setting title label',
-  },
-  descriptionLabel: {
-    id: 'app.audio.audioSettings.descriptionLabel',
-    description: 'audio settings description label',
-  },
-  micSourceLabel: {
-    id: 'app.audio.audioSettings.microphoneSourceLabel',
-    description: 'Label for mic source',
-  },
-  speakerSourceLabel: {
-    id: 'app.audio.audioSettings.speakerSourceLabel',
-    description: 'Label for speaker source',
-  },
-  streamVolumeLabel: {
-    id: 'app.audio.audioSettings.microphoneStreamLabel',
-    description: 'Label for stream volume',
-  },
-});
+AudioSettings.propTypes = propTypes;
 
 export default withModalMounter(injectIntl(AudioSettings));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..0ee59b2e4a8bc4df9f2fde592fecd7548bd061c4
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss
@@ -0,0 +1,126 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.form {
+  display: flex;
+  flex-flow: column;
+  margin-top: 1.5rem;
+}
+
+.audioNote {
+  @include mq($small-only) {
+    font-size: 0.8rem;
+  }
+}
+
+.row {
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  margin-bottom: 0.7rem;
+}
+
+.col {
+  display: flex;
+  flex-grow: 1;
+  flex-basis: 0;
+  margin-right: 1rem;
+
+  &:last-child {
+    margin-right: 0;
+    padding-right: 0.1rem;
+    padding-left: 4rem;
+  }
+
+  &.spacedLeft {
+    // @extend .spaced;
+
+    label {
+      flex-grow: 1;
+      flex-basis: 0;
+      margin-right: 0;
+      padding-right: 0.1rem;
+      padding-left: 4rem;
+    }
+
+    &:before {
+      content: "";
+      display: block;
+      flex-grow: 1;
+      flex-basis: 0;
+      margin-right: 1rem;
+    }
+
+    &:last-child {
+      margin-right: 0;
+      padding-right: 0;
+      padding-left: 0;
+    }
+  }
+}
+
+.labelSmall {
+  color: black;
+  font-size: 0.85rem;
+  font-weight: 600;
+
+  & > :first-child {
+    margin-top: 0.5rem;
+  }
+}
+
+.formElement {
+  position: relative;
+  display: flex;
+  flex-flow: column;
+  flex-grow: 1;
+}
+
+.select {
+  -webkit-appearance: none;
+  -webkit-border-radius: 0px;
+  background: $color-white url("data:image/svg+xml;charset=utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='#667189' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>") no-repeat right .35rem center/.4rem .5rem;
+  background-repeat: no-repeat;
+  border: 0.07rem solid $color-gray-light;
+  border-radius: .125rem;
+  color: $color-text;
+  width: 100%;
+  // appearance: none;
+  padding: .4rem;
+}
+
+.audioMeter {
+  width: 100%;
+}
+
+.pullContentRight {
+  display: flex;
+  justify-content: flex-end;
+  flex-flow: row;
+}
+
+.verticalLine {
+  color: #f3f6f9;
+  border-left: 1px solid;
+  height: 5rem;
+}
+
+.backBtn {
+  margin-right: 0.5rem;
+  border: none;
+
+  @include mq($small-only) {
+    margin-right: auto;
+  }
+}
+
+.enterAudio {
+  margin-top: 1.5rem;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.chooseAudio {
+  position:absolute;
+  left:50%;
+  transform: translate(-50%, 0);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
index ce1b691aa462887bcc8c0ab8267f8835d318a828..24dac7849875a785f3cdb2173941d6c58005b3a3 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
@@ -99,7 +99,7 @@ class AudioStreamVolume extends Component {
 
     this.setState(prevState => ({
       instant,
-      slow: 0.75 * prevState.slow + 0.25 * instant,
+      slow: (0.75 * prevState.slow) + (0.25 * instant),
     }));
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
index 33ace856628851727ace7c1b0e192c0e80c05e32..cb57d7fca081160092b97dbdcf90301e0eb41c36 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
@@ -1,15 +1,36 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import Button from '/imports/ui/components/button/component';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
 import styles from './styles.scss';
-import { defineMessages, injectIntl } from 'react-intl';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  handlePlayAudioSample: PropTypes.func.isRequired,
+  outputDeviceId: PropTypes.string,
+};
+
+const defaultProps = {
+  outputDeviceId: null,
+};
+
+const intlMessages = defineMessages({
+  playSoundLabel: {
+    id: 'app.audio.playSoundLabel',
+    description: 'Play sound button label',
+  },
+});
 
 class AudioTest extends React.Component {
   constructor(props) {
     super(props);
+
+    this.handlePlayAudioSample = props.handlePlayAudioSample.bind(this);
   }
 
   render() {
     const {
+      outputDeviceId,
       intl,
     } = this.props;
 
@@ -20,17 +41,13 @@ class AudioTest extends React.Component {
         icon={'unmute'}
         size={'sm'}
         color={'primary'}
-        onClick={this.props.handlePlayAudioSample}
+        onClick={() => this.handlePlayAudioSample(outputDeviceId)}
       />
     );
   }
 }
 
-const intlMessages = defineMessages({
-  playSoundLabel: {
-    id: 'app.audio.playSoundLabel',
-    description: 'Play sound button label',
-  },
-});
+AudioTest.propTypes = propTypes;
+AudioTest.defaultProps = defaultProps;
 
 export default injectIntl(AudioTest);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
index 37420893afe78df3005acb3826e9ae1fb85dbd16..8e74d4fb66e964df7834c81b5cc31effa0395885 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
@@ -1,24 +1,15 @@
-import React, { Component } from 'react';
+import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
+import Service from '/imports/ui/components/audio/service';
 import AudioTest from './component';
 
-class AudioTestContainer extends Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    return (
-      <AudioTest {...this.props}>
-        {this.props.children}
-      </AudioTest>
-    );
-  }
-}
+const AudioTestContainer = props => <AudioTest {...props} />;
 
 export default createContainer(() => ({
-  handlePlayAudioSample: () => {
-    const snd = new Audio('resources/sounds/audioSample.mp3');
-    snd.play();
+  outputDeviceId: Service.outputDeviceId(),
+  handlePlayAudioSample: (deviceId) => {
+    const sound = new Audio('resources/sounds/audioSample.mp3');
+    if (deviceId && sound.setSinkId) sound.setSinkId(deviceId);
+    sound.play();
   },
 }), AudioTestContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
index b9ff81b24d83075b58b77c47453223500846bb26..2df5f1a13de80fd66cc40fd2df34eb7c5d3257f9 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
@@ -1,12 +1,21 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
 .testAudioBtn {
-  border: none;
-  padding-left: 0;
   background-color: transparent;
   color: $color-primary;
   font-weight: normal;
+  border: none;
+
   i {
     color: $color-primary;
+    transition: all .2s ease-in-out;
+  }
+
+  &:hover, &:focus, &:active {
+    background-color: transparent !important;
+    color: darken($color-primary, 17%) !important;
+    i {
+      color: darken($color-primary, 17%);
+    }
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/component.jsx
index 978cbe9af0ead95e4fd69bf3aa5174e27ea7ace5..717836bd01404a41ead99856dab6ef6038a10fa0 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/component.jsx
@@ -1,12 +1,28 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  init: PropTypes.func.isRequired,
+};
 
 export default class Audio extends Component {
   constructor(props) {
     super(props);
-    props.init.call(this);
+
+    this.init = props.init.bind(this);
+  }
+
+  componentDidMount() {
+    this.init();
   }
 
   render() {
-    return (<audio id="remote-media" autoPlay="autoplay" />);
+    return (
+      <audio id="remote-media" autoPlay="autoplay">
+        <track kind="captions" /> {/* These captions are brought to you by eslint */}
+      </audio>
+    );
   }
 }
+
+Audio.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index 845ac98ee0d8b717b5a2665def2bce4787f1162b..fbf0ea116c9cfd9d9cdbb38c30af8d33182f6167 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -1,10 +1,11 @@
 import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
 import { withModalMounter } from '/imports/ui/components/modal/service';
+import { injectIntl, defineMessages } from 'react-intl';
 import PropTypes from 'prop-types';
 import Service from './service';
 import Audio from './component';
-import AudioModal from './audio-modal/component';
+import AudioModalContainer from './audio-modal/container';
 
 const propTypes = {
   children: PropTypes.element,
@@ -14,28 +15,75 @@ const defaultProps = {
   children: null,
 };
 
-const AudioContainer = props =>
-  (<Audio {...props}>
-    {props.children}
-  </Audio>
-  );
+const intlMessages = defineMessages({
+  joinedAudio: {
+    id: 'app.audioManager.joinedAudio',
+    description: 'Joined audio toast message',
+  },
+  joinedEcho: {
+    id: 'app.audioManager.joinedEcho',
+    description: 'Joined echo test toast message',
+  },
+  leftAudio: {
+    id: 'app.audioManager.leftAudio',
+    description: 'Left audio toast message',
+  },
+  genericError: {
+    id: 'app.audioManager.genericError',
+    description: 'Generic error messsage',
+  },
+  connectionError: {
+    id: 'app.audioManager.connectionError',
+    description: 'Connection error messsage',
+  },
+  requestTimeout: {
+    id: 'app.audioManager.requestTimeout',
+    description: 'Request timeout error messsage',
+  },
+  invalidTarget: {
+    id: 'app.audioManager.invalidTarget',
+    description: 'Invalid target error messsage',
+  },
+  mediaError: {
+    id: 'app.audioManager.mediaError',
+    description: 'Media error messsage',
+  },
+});
+
+
+const AudioContainer = props => <Audio {...props} />;
 
 let didMountAutoJoin = false;
 
-export default withModalMounter(createContainer(({ mountModal }) => {
+export default withModalMounter(injectIntl(createContainer(({ mountModal, intl }) => {
   const APP_CONFIG = Meteor.settings.public.app;
 
   const { autoJoinAudio } = APP_CONFIG;
 
+  const messages = {
+    info: {
+      JOINED_AUDIO: intl.formatMessage(intlMessages.joinedAudio),
+      JOINED_ECHO: intl.formatMessage(intlMessages.joinedEcho),
+      LEFT_AUDIO: intl.formatMessage(intlMessages.leftAudio),
+    },
+    error: {
+      GENERIC_ERROR: intl.formatMessage(intlMessages.genericError),
+      CONNECTION_ERROR: intl.formatMessage(intlMessages.connectionError),
+      REQUEST_TIMEOUT: intl.formatMessage(intlMessages.requestTimeout),
+      INVALID_TARGET: intl.formatMessage(intlMessages.invalidTarget),
+    },
+  };
+
   return {
     init: () => {
-      Service.init();
+      Service.init(messages);
+      Service.changeOutputDevice(document.querySelector('#remote-media').sinkId);
       if (!autoJoinAudio || didMountAutoJoin) return;
-      mountModal(<AudioModal handleJoinListenOnly={Service.joinListenOnly} />);
+      mountModal(<AudioModalContainer />);
       didMountAutoJoin = true;
     },
   };
-}, AudioContainer));
+}, AudioContainer)));
 
 AudioContainer.propTypes = propTypes;
 AudioContainer.defaultProps = defaultProps;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
index ec40ddb175df0186d1c21c16b9ee61739dc0594f..7c68859eb02a25eb199a7b38f2de24b301c70b74 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
@@ -1,24 +1,28 @@
 import React, { Component } from 'react';
+import _ from 'lodash';
 import PropTypes from 'prop-types';
+import cx from 'classnames';
 import styles from '../audio-modal/styles';
 
 const propTypes = {
   kind: PropTypes.oneOf(['audioinput', 'audiooutput', 'videoinput']),
   onChange: PropTypes.func.isRequired,
   value: PropTypes.string,
+  handleDeviceChange: PropTypes.func,
+  className: PropTypes.string,
 };
 
 const defaultProps = {
   kind: 'audioinput',
   value: undefined,
+  className: null,
+  handleDeviceChange: null,
 };
 
 class DeviceSelector extends Component {
   constructor(props) {
     super(props);
 
-    this.handleEnumerateDevicesSuccess = this.handleEnumerateDevicesSuccess.bind(this);
-    this.handleEnumerateDevicesError = this.handleEnumerateDevicesError.bind(this);
     this.handleSelectChange = this.handleSelectChange.bind(this);
 
     this.state = {
@@ -29,26 +33,22 @@ class DeviceSelector extends Component {
   }
 
   componentDidMount() {
-    navigator.mediaDevices
-      .enumerateDevices()
-      .then(this.handleEnumerateDevicesSuccess)
-      .catch(this.handleEnumerateDevicesError);
-  }
+    const handleEnumerateDevicesSuccess = (deviceInfos) => {
+      const devices = deviceInfos.filter(d => d.kind === this.props.kind);
 
-  handleEnumerateDevicesSuccess(deviceInfos) {
-    const devices = deviceInfos.filter(d => d.kind === this.props.kind);
-
-    this.setState({
-      devices,
-      options: devices.map((d, i) => ({
-        label: d.label || `${this.props.kind} - ${i}`,
-        value: d.deviceId,
-      })),
-    });
-  }
+      this.setState({
+        devices,
+        options: devices.map((d, i) => ({
+          label: d.label || `${this.props.kind} - ${i}`,
+          value: d.deviceId,
+          key: _.uniqueId('device-option-'),
+        })),
+      });
+    };
 
-  handleEnumerateDevicesError(error) {
-    log('error', error);
+    navigator.mediaDevices
+      .enumerateDevices()
+      .then(handleEnumerateDevicesSuccess);
   }
 
   handleSelectChange(event) {
@@ -61,7 +61,7 @@ class DeviceSelector extends Component {
   }
 
   render() {
-    const { kind, handleDeviceChange, ...props } = this.props;
+    const { kind, handleDeviceChange, className, ...props } = this.props;
     const { options, value } = this.state;
 
     return (
@@ -70,13 +70,13 @@ class DeviceSelector extends Component {
         value={value}
         onChange={this.handleSelectChange}
         disabled={!options.length}
-        className={styles.select}
+        className={cx(styles.select, className)}
       >
         {
           options.length ?
-            options.map((option, i) => (
+            options.map(option => (
               <option
-                key={i}
+                key={option.key}
                 value={option.value}
               >
                 {option.label}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ffc72587bb2d4735ef46a0545c2fbcc38dc282f6
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
@@ -0,0 +1,64 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
+import styles from './styles';
+
+const intlMessages = defineMessages({
+  yes: {
+    id: 'app.audioModal.yes',
+    description: 'Hear yourself yes',
+  },
+  no: {
+    id: 'app.audioModal.no',
+    description: 'Hear yourself no',
+  },
+});
+
+const propTypes = {
+  handleYes: PropTypes.func.isRequired,
+  handleNo: PropTypes.func.isRequired,
+  intl: intlShape.isRequired,
+};
+
+class EchoTest extends Component {
+  constructor(props) {
+    super(props);
+
+    this.handleYes = props.handleYes.bind(this);
+    this.handleNo = props.handleNo.bind(this);
+  }
+
+  render() {
+    const {
+      intl,
+    } = this.props;
+
+    return (
+      <span>
+        <Button
+          className={styles.button}
+          label={intl.formatMessage(intlMessages.yes)}
+          icon={'thumbs_up'}
+          circle
+          color={'success'}
+          size={'jumbo'}
+          onClick={this.handleYes}
+        />
+        <Button
+          className={styles.button}
+          label={intl.formatMessage(intlMessages.no)}
+          icon={'thumbs_down'}
+          circle
+          color={'danger'}
+          size={'jumbo'}
+          onClick={this.handleNo}
+        />
+      </span>
+    );
+  }
+}
+
+export default injectIntl(EchoTest);
+
+EchoTest.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b196de52294feecebd641dbb0a58e310eee4ed47
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
@@ -0,0 +1,21 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.button {
+  &:focus {
+    outline: none !important;
+  }
+
+  &:first-child {
+    margin-right: 3rem;
+
+    @include mq($small-only) {
+      margin-right: 1rem;
+    }
+  }
+
+  span:last-child {
+    color: black;
+    font-size: 1rem;
+    font-weight: 600;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx
deleted file mode 100644
index fceea096a8adb822b117ea1345be759125d8479d..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import Button from '/imports/ui/components/button/component';
-
-class EnterAudio extends React.Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const {
-      intl,
-    } = this.props;
-
-    return (
-      <Button
-        label={intl.formatMessage(intlMessages.enterSessionLabel)}
-        size={'md'}
-        color={'primary'}
-        onClick={this.props.handleJoin}
-      />
-    );
-  }
-}
-
-const intlMessages = defineMessages({
-  enterSessionLabel: {
-    id: 'app.audio.enterSessionLabel',
-    description: 'enter session button label',
-  },
-});
-
-export default injectIntl(EnterAudio);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx
deleted file mode 100644
index 30a84bea882d683c03c822b1ceca7cfdcfb5a564..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { Component } from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import AudioService from '../service';
-import { withModalMounter } from '/imports/ui/components/modal/service';
-import EnterAudio from './component';
-
-class EnterAudioContainer extends Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const {
-      isFullAudio,
-      mountModal,
-    } = this.props;
-
-    const handleJoin = () => {
-      mountModal(null);
-      return isFullAudio ? AudioService.joinMicrophone() : AudioService.joinListenOnly();
-    };
-
-    return (
-      <EnterAudio handleJoin={handleJoin} />
-    );
-  }
-}
-
-export default withModalMounter(EnterAudioContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx
deleted file mode 100644
index 10fc993dbd49c4805dc67fe95fd2408e74ac2ed7..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import styles from '../audio-modal/styles.scss';
-import Button from '/imports/ui/components/button/component';
-import { withModalMounter } from '/imports/ui/components/modal/service';
-import { defineMessages, injectIntl } from 'react-intl';
-
-const intlMessages = defineMessages({
-  microphoneLabel: {
-    id: 'app.audioModal.microphoneLabel',
-    description: 'Join mic audio button label',
-  },
-  listenOnlyLabel: {
-    id: 'app.audioModal.listenOnlyLabel',
-    description: 'Join listen only audio button label',
-  },
-  closeLabel: {
-    id: 'app.audioModal.closeLabel',
-    description: 'close audio modal button label',
-  },
-  audioChoiceLabel: {
-    id: 'app.audioModal.audioChoiceLabel',
-    description: 'Join audio modal title',
-  },
-});
-
-class JoinAudio extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.handleClose = this.handleClose.bind(this);
-    this.openAudio = this.openAudio.bind(this);
-    this.openListen = this.openListen.bind(this);
-  }
-
-  handleClose() {
-    /* TODO: Refactor this to the outer component (audio-modal/container) */
-    this.props.mountModal(null);
-  }
-
-  openAudio() {
-    this.props.changeMenu(this.props.AUDIO_SETTINGS);
-  }
-
-  openListen() {
-    this.handleClose();
-    this.props.handleJoinListenOnly();
-  }
-
-  render() {
-    const { intl } = this.props;
-    return (
-      <div>
-        <div className={styles.closeBtnWrapper}>
-          <Button
-            className={styles.closeBtn}
-            label={intl.formatMessage(intlMessages.closeLabel)}
-            icon={'close'}
-            size={'lg'}
-            hideLabel
-            onClick={this.handleClose}
-          />
-        </div>
-
-        <div className={styles.title}>
-          {intl.formatMessage(intlMessages.audioChoiceLabel)}
-        </div>
-        <div className={styles.center}>
-          <Button
-            className={styles.audioBtn}
-            label={intl.formatMessage(intlMessages.microphoneLabel)}
-            icon={'unmute'}
-            circle
-            size={'jumbo'}
-            onClick={this.openAudio}
-          />
-
-          <span className={styles.verticalLine} />
-          <Button
-            className={styles.audioBtn}
-            label={intl.formatMessage(intlMessages.listenOnlyLabel)}
-            icon={'listen'}
-            circle
-            size={'jumbo'}
-            onClick={this.openListen}
-          />
-        </div>
-      </div>
-    );
-  }
-}
-
-export default withModalMounter(injectIntl(JoinAudio));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js
index 8859320184b66d72e89d4facddf420b508ae3460..694d302f3f3a695861ea6045983257137de9ca56 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/service.js
@@ -1,10 +1,12 @@
 import Users from '/imports/api/users';
 import Auth from '/imports/ui/services/auth';
-import AudioManager from '/imports/api/audio/client/manager';
+import AudioManager from '/imports/ui/services/audio-manager';
 import Meetings from '/imports/api/meetings';
 
-const init = () => {
+const init = (messages) => {
+  const meetingId = Auth.meetingID;
   const userId = Auth.userID;
+  const sessionToken = Auth.sessionToken;
   const User = Users.findOne({ userId });
   const username = User.name;
   const Meeting = Meetings.findOne({ meetingId: User.meetingId });
@@ -14,22 +16,34 @@ const init = () => {
   const microphoneLockEnforced = false;
 
   const userData = {
+    meetingId,
     userId,
+    sessionToken,
     username,
     voiceBridge,
     microphoneLockEnforced,
   };
 
-  AudioManager.init(userData);
+  AudioManager.init(userData, messages);
 };
 
-const exitAudio = () => AudioManager.exitAudio();
-const joinListenOnly = () => AudioManager.joinAudio(true);
-const joinMicrophone = () => AudioManager.joinAudio(false);
-
 export default {
   init,
-  exitAudio,
-  joinListenOnly,
-  joinMicrophone,
+  exitAudio: () => AudioManager.exitAudio(),
+  transferCall: () => AudioManager.transferCall(),
+  joinListenOnly: () => AudioManager.joinAudio({ isListenOnly: true }),
+  joinMicrophone: () => AudioManager.joinAudio(),
+  joinEchoTest: () => AudioManager.joinAudio({ isEchoTest: true }),
+  toggleMuteMicrophone: () => AudioManager.toggleMuteMicrophone(),
+  changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
+  changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId),
+  isConnected: () => AudioManager.isConnected,
+  isHangingUp: () => AudioManager.isHangingUp,
+  isMuted: () => AudioManager.isMuted,
+  isConnecting: () => AudioManager.isConnecting,
+  isListenOnly: () => AudioManager.isListenOnly,
+  inputDeviceId: () => AudioManager.inputDeviceId,
+  outputDeviceId: () => AudioManager.outputDeviceId,
+  isEchoTest: () => AudioManager.isEchoTest,
+  error: () => AudioManager.error,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index c10260a0a9d692ca892889de8fa09bf2827f85e4..7124c89d82e30f1a0d537b2695891098a3760ad5 100644
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -93,6 +93,21 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   vertical-align: middle;
   background: none;
   padding: 0 !important;
+
+  &:active {
+    &:focus {
+      outline: thin dotted;
+      outline: 5px auto -webkit-focus-ring-color;
+      outline-offset: -2px;
+    }
+  }
+
+  &[aria-disabled="true"] {
+    cursor: not-allowed;
+    opacity: .65;
+    box-shadow: none;
+    pointer-events: none;
+  }
 }
 
 .label {
diff --git a/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
index baa112c8891d90a11f886cbaf90b07e1a5f01186..b235b549c30f6275b39d295c3202f351cf21a812 100644
--- a/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
@@ -5,6 +5,7 @@
   display: flex;
   flex-direction: column;
   padding: ($line-height-computed / 2) $line-height-computed;
+  box-shadow : 0px 0px 15px rgba(0, 0, 0, 0.5);
 }
 
 .content {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2480edb5b44bae70e1cdfaf9b2172610af188c62
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -0,0 +1,226 @@
+import { Tracker } from 'meteor/tracker';
+import { makeCall } from '/imports/ui/services/api';
+import VertoBridge from '/imports/api/audio/client/bridge/verto';
+import SIPBridge from '/imports/api/audio/client/bridge/sip';
+import { notify } from '/imports/ui/services/notification';
+
+const MEDIA = Meteor.settings.public.media;
+const USE_SIP = MEDIA.useSIPAudio;
+const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
+
+const CALL_STATES = {
+  STARTED: 'started',
+  ENDED: 'ended',
+  FAILED: 'failed',
+};
+
+class AudioManager {
+  constructor() {
+    this._inputDevice = {
+      tracker: new Tracker.Dependency(),
+    };
+
+    this.defineProperties({
+      isMuted: false,
+      isConnected: false,
+      isConnecting: false,
+      isHangingUp: false,
+      isListenOnly: false,
+      isEchoTest: false,
+      error: null,
+      outputDeviceId: null,
+    });
+  }
+
+  init(userData, messages) {
+    this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData);
+    this.userData = userData;
+    this.messages = messages;
+  }
+
+  defineProperties(obj) {
+    Object.keys(obj).forEach((key) => {
+      const privateKey = `_${key}`;
+      this[privateKey] = {
+        value: obj[key],
+        tracker: new Tracker.Dependency(),
+      };
+
+      Object.defineProperty(this, key, {
+        set: (value) => {
+          this[privateKey].value = value;
+          this[privateKey].tracker.changed();
+        },
+        get: () => {
+          this[privateKey].tracker.depend();
+          return this[privateKey].value;
+        },
+      });
+    });
+  }
+
+  joinAudio(options = {}) {
+    const {
+      isListenOnly,
+      isEchoTest,
+    } = options;
+
+    if (!this.devicesInitialized) {
+      this.setDefaultInputDevice();
+      this.changeOutputDevice('default');
+      this.devicesInitialized = true;
+    }
+
+    this.isConnecting = true;
+    this.isMuted = false;
+    this.error = null;
+    this.isListenOnly = isListenOnly || false;
+    this.isEchoTest = isEchoTest || false;
+
+    const callOptions = {
+      isListenOnly: this.isListenOnly,
+      extension: isEchoTest ? ECHO_TEST_NUMBER : null,
+      inputStream: this.isListenOnly ? this.createListenOnlyStream() : this.inputStream,
+    };
+
+    return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
+  }
+
+  exitAudio() {
+    this.isHangingUp = true;
+    return this.bridge.exitAudio();
+  }
+
+  transferCall() {
+    this.onTransferStart();
+    return this.bridge.transferCall(this.onAudioJoin.bind(this));
+  }
+
+  toggleMuteMicrophone() {
+    makeCall('toggleSelfVoice').then(() => {
+      this.onToggleMicrophoneMute();
+    });
+  }
+
+  onAudioJoin() {
+    this.isConnecting = false;
+    this.isConnected = true;
+
+    if (!this.isEchoTest) {
+      this.notify(this.messages.info.JOINED_AUDIO);
+    }
+  }
+
+  onTransferStart() {
+    this.isEchoTest = false;
+    this.isConnecting = true;
+  }
+
+  onAudioExit() {
+    this.isConnected = false;
+    this.isConnecting = false;
+    this.isHangingUp = false;
+
+
+    if (!this.error && !this.isEchoTest) {
+      this.notify(this.messages.info.LEFT_AUDIO);
+    }
+    this.isEchoTest = false;
+  }
+
+  onToggleMicrophoneMute() {
+    this.isMuted = !this.isMuted;
+  }
+
+  callStateCallback(response) {
+    return new Promise((resolve) => {
+      const {
+        STARTED,
+        ENDED,
+        FAILED,
+      } = CALL_STATES;
+
+      const {
+        status,
+        error,
+        bridgeError,
+      } = response;
+
+      if (status === STARTED) {
+        this.onAudioJoin();
+        resolve(STARTED);
+      } else if (status === ENDED) {
+        this.onAudioExit();
+      } else if (status === FAILED) {
+        this.error = error;
+        this.notify(this.messages.error[error]);
+        console.error('Audio Error:', error, bridgeError);
+        this.onAudioExit();
+      }
+    });
+  }
+
+  createListenOnlyStream() {
+    if (this.listenOnlyAudioContext) {
+      this.listenOnlyAudioContext.close();
+    }
+
+    this.listenOnlyAudioContext = window.AudioContext ?
+                                  new window.AudioContext() :
+                                  new window.webkitAudioContext();
+
+    return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
+  }
+
+  setDefaultInputDevice() {
+    this.changeInputDevice();
+  }
+
+  async changeInputDevice(deviceId) {
+    try {
+      if (!deviceId) {
+        this.inputDevice = await await this.bridge.setDefaultInputDevice();
+        return;
+      }
+      this.inputDevice = await this.bridge.changeInputDevice(deviceId);
+    } catch(err) {
+      this.error = err;
+      this.notify('There was a problem getting the media devices');
+    }
+  }
+
+  async changeOutputDevice(deviceId) {
+    this.outputDeviceId = await this.bridge.changeOutputDevice(deviceId);
+  }
+
+  set inputDevice(value) {
+    Object.assign(this._inputDevice, value);
+    this._inputDevice.tracker.changed();
+  }
+
+  get inputStream() {
+    return this._inputDevice.stream;
+  }
+
+  get inputDeviceId() {
+    this._inputDevice.tracker.depend();
+    return this._inputDevice.id;
+  }
+
+  set userData(value) {
+    this._userData = value;
+  }
+
+  get userData() {
+    return this._userData;
+  }
+
+  notify(message) {
+    notify(message,
+           this.error ? 'error' : 'info',
+           this.isListenOnly ? 'audio_on' : 'unmute');
+  }
+}
+
+const audioManager = new AudioManager();
+export default audioManager;
diff --git a/bigbluebutton-html5/imports/ui/services/user/mapUser.js b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
index 9685a7c60addc8fc8a7daaae8658caef83779376..2acaf92df655c48eb4d0540b6764bb47525500cd 100644
--- a/bigbluebutton-html5/imports/ui/services/user/mapUser.js
+++ b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
@@ -22,7 +22,7 @@ const mapUser = (user) => {
     isPresenter: user.presenter,
     isModerator: user.role === ROLE_MODERATOR,
     isCurrent: user.userId === userId,
-    isVoiceUser: voiceUser.joined,
+    isVoiceUser: voiceUser ? voiceUser.joined : false,
     isMuted: voiceUser ? voiceUser.muted : false,
     isTalking: voiceUser ? voiceUser.talking : false,
     isListenOnly: voiceUser ? voiceUser.listenOnly : false,
diff --git a/bigbluebutton-html5/private/config/public/media.yaml b/bigbluebutton-html5/private/config/public/media.yaml
index 61f690757456087cc840c0fa3a19ea2d223f7468..23329f1e456b1db739173b5a88a89193635af40d 100644
--- a/bigbluebutton-html5/private/config/public/media.yaml
+++ b/bigbluebutton-html5/private/config/public/media.yaml
@@ -8,3 +8,9 @@ media:
   vertoPort: "8082"
   # specifies whether to use SIP.js for audio over mod_verto
   useSIPAudio: true
+  stunTurnServersFetchAddress: '/bigbluebutton/api/stuns'
+  mediaTag: '#remote-media'
+  callTransferTimeout: 5000
+  callHangupTimeout: 2000
+  callHangupMaximumRetries: 10
+  echoTestNumber: '9196'
diff --git a/bigbluebutton-html5/private/config/public/user.yaml b/bigbluebutton-html5/private/config/public/user.yaml
index df93f3b435eaa21ded5f864af8c48f088890b061..f3c874018c5cbd5a3bdae8886951be00a9617d9b 100644
--- a/bigbluebutton-html5/private/config/public/user.yaml
+++ b/bigbluebutton-html5/private/config/public/user.yaml
@@ -1,3 +1,4 @@
 user:
   role_moderator: 'MODERATOR'
   role_viewer: 'VIEWER'
+  role_presenter: 'PRESENTER'
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 67c053ab7d9129f42afe01572b8b9c201ef84cef..ceba85eaa22145f9153873464e7fb40e97c2c60a 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -202,6 +202,20 @@
     "app.audioModal.audioChoiceLabel": "How would you like to join the audio?",
     "app.audioModal.audioChoiceDesc": "Select how to join the audio in this meeting",
     "app.audioModal.closeLabel": "Close",
+    "app.audioModal.yes": "Yes",
+    "app.audioModal.no": "No",
+    "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?",
+    "app.audioModal.settingsTitle": "Change your audio settings",
+    "app.audioModal.connecting": "Connecting",
+    "app.audioModal.connectingEchoTest": "Connecting to echo test",
+    "app.audioManager.joinedAudio": "You have joined the audio conference",
+    "app.audioManager.joinedEcho": "You have joined the echo test",
+    "app.audioManager.leftAudio": "You have left the audio conference",
+    "app.audioManager.genericError": "Error: An error has occurred, please try again",
+    "app.audioManager.connectionError": "Error: Connection error",
+    "app.audioManager.requestTimeout": "Error: There was a timeout in the request",
+    "app.audioManager.invalidTarget": "Error: Tried to request something to an invalid target",
+    "app.audioManager.mediaError": "Error: There was an error getting your media devices",
     "app.audio.joinAudio": "Join Audio",
     "app.audio.leaveAudio": "Leave Audio",
     "app.audio.enterSessionLabel": "Enter Session",
@@ -212,6 +226,7 @@
     "app.audio.audioSettings.microphoneSourceLabel": "Microphone source",
     "app.audio.audioSettings.speakerSourceLabel": "Speaker source",
     "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
+    "app.audio.audioSettings.retryLabel": "Retry",
     "app.audio.listenOnly.backLabel": "Back",
     "app.audio.listenOnly.closeLabel": "Close",
     "app.error.kicked": "You have been kicked out of the meeting",
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 0ca1ad07a2d4fc0430bd0cc508234a2e364787af..5ef61e5b9d5e8b4f4681e51da3ddd8ccbfb2546b 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -156,7 +156,7 @@ webcamsOnlyForModerator=false
 #----------------------------------------------------
 # This URL is where the BBB client is accessible. When a user sucessfully
 # enters a name and password, she is redirected here to load the client.
-bigbluebutton.web.serverURL=http://192.168.246.131
+bigbluebutton.web.serverURL=http://10.130.218.89
 
 
 #----------------------------------------------------
@@ -170,7 +170,7 @@ defaultClientUrl=${bigbluebutton.web.serverURL}/client/BigBlueButton.html
 #defaultClientUrl=http://192.168.0.235/3rd-party.html
 
 # The url for where the guest will poll if approved to join or not.
-defaultGuestWaitURL=${bigbluebutton.web.serverURL}/bigbluebutton/api/guestWait
+defaultGuestWaitURL=${bigbluebutton.web.serverURL}/client/guest-wait.html
 
 # The default avatar image to display if nothing is passed on the JOIN API (avatarURL)
 # call. This avatar is displayed if the user isn't sharing the webcam and
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 947a85e243aeca8b951e3863846b5dbff7b4599d..9f6bf7a4024434dc65080e354ffe3b6d6ae9080e 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
@@ -1379,12 +1379,18 @@ class ApiController {
       }
 
       String guestWaitStatus = userSession.guestStatus
+
+      log.debug("GuestWaitStatus = " + guestWaitStatus)
+
       String msgKey = "guestAllowed"
       String msgValue = "Guest allowed to join meeting."
       String destUrl = clientURL + "?sessionToken=" + sessionToken
+      log.debug("destUrl = " + destUrl)
+
       if (guestWaitStatus.equals(GuestPolicy.WAIT)) {
         clientURL = paramsProcessorUtil.getDefaultGuestWaitURL();
         destUrl = clientURL + "?sessionToken=" + sessionToken
+        log.debug("GuestPolicy.WAIT - destUrl = " + destUrl)
         msgKey = "guestWait"
         msgValue = "Guest waiting for approval to join meeting."
         // We force the response to not do a redirect. Otherwise,
@@ -1392,8 +1398,9 @@ class ApiController {
         redirectClient = false
       } else if (guestWaitStatus.equals(GuestPolicy.DENY)) {
         destUrl = meeting.getLogoutUrl()
-        msgKey = "guestDeny"
+        msgKey = "guestDenied"
         msgValue = "Guest denied to join meeting."
+        log.debug("GuestPolicy.DENY - destUrl = " + destUrl)
       }
 
       if (redirectClient){
@@ -1403,18 +1410,18 @@ class ApiController {
         log.info("Successfully joined. Sending XML response.");
         response.addHeader("Cache-Control", "no-cache")
         withFormat {
-          xml {
-            render(contentType:"text/xml") {
-              response() {
-                returncode(RESP_CODE_SUCCESS)
-                messageKey(msgKey)
-                message(msgValue)
-                meeting_id() { mkp.yield(us.meetingID) }
-                user_id(us.internalUserId)
-                auth_token(us.authToken)
-                session_token(session[sessionToken])
-                guestStatus(guestWaitStatus)
-                url(destUrl)
+          json {
+            render(contentType:"application/json") {
+              response = {
+                returncode = RESP_CODE_SUCCESS
+                messageKey = msgKey
+                message = msgValue
+                meeting_id = us.meetingID
+                user_id = us.internalUserId
+                auth_token = us.authToken
+                session_token = session[sessionToken]
+                guestStatus = guestWaitStatus
+                url = destUrl
               }
             }
           }
@@ -1423,7 +1430,6 @@ class ApiController {
     }
   }
 
-
   /***********************************************
    * ENTER API
    ***********************************************/
@@ -2033,7 +2039,8 @@ class ApiController {
       fos.flush()
       fos.close()
 
-      processUploadedFile("TWO", meetingId, presId, presFilename, pres, current);
+      // Hardcode pre-uploaded presentation to the default presentation window
+      processUploadedFile("DEFAULT_PRESENTATION_POD", meetingId, presId, presFilename, pres, current);
     }
 
   }
@@ -2052,6 +2059,7 @@ class ApiController {
 
       if (presDownloadService.savePresentation(meetingId, newFilePath, address)) {
         def pres = new File(newFilePath)
+        // Hardcode pre-uploaded presentation to the default presentation window
         processUploadedFile("DEFAULT_PRESENTATION_POD", meetingId, presId, presFilename, pres, current);
       } else {
         log.error("Failed to download presentation=[${address}], meeting=[${meetingId}]")