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}]")