Merge pull request #20921 from prlanzarin/u27/fix/tlo-unhold-unmute-delay

fix(audio): change unmute/unhold flow to work around FS unmute stutter
This commit is contained in:
Paulo Lanzarin 2024-08-15 12:37:29 -03:00 committed by GitHub
commit 7371aa6c18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 147 additions and 55 deletions

View File

@ -1,9 +1,10 @@
package org.bigbluebutton
import org.bigbluebutton.common2.msgs.{ BbbCommonEnvCoreMsg, BbbCoreEnvelope, BbbCoreHeaderWithMeetingId, MessageTypes, MuteUserInVoiceConfSysMsg, MuteUserInVoiceConfSysMsgBody, Routing }
import org.bigbluebutton.common2.msgs.{ BbbCommonEnvCoreMsg, BbbCoreEnvelope, BbbCoreHeaderWithMeetingId, MessageTypes, Routing }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.{ MeetingStatus2x }
import org.bigbluebutton.core.apps.webcam.CameraHdlrHelpers
import org.bigbluebutton.core.apps.voice.VoiceApp
import org.bigbluebutton.core.models.{
Roles,
Users2x,
@ -16,19 +17,19 @@ import org.bigbluebutton.core.models.{
object LockSettingsUtil {
private def muteUserInVoiceConf(liveMeeting: LiveMeeting, outGW: OutMsgRouter, vu: VoiceUserState, mute: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, vu.intId)
val envelope = BbbCoreEnvelope(MuteUserInVoiceConfSysMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(MuteUserInVoiceConfSysMsg.NAME, liveMeeting.props.meetingProp.intId)
val body = MuteUserInVoiceConfSysMsgBody(liveMeeting.props.voiceProp.voiceConf, vu.voiceUserId, mute)
val event = MuteUserInVoiceConfSysMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
private def muteUserInVoiceConf(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
vu: VoiceUserState, mute: Boolean
)(implicit context: akka.actor.ActorContext): Unit = {
VoiceApp.muteUserInVoiceConf(liveMeeting, outGW, vu.intId, mute)
}
private def applyMutingOfUsers(disableMic: Boolean, liveMeeting: LiveMeeting, outGW: OutMsgRouter): Unit = {
private def applyMutingOfUsers(
disableMic: Boolean,
liveMeeting: LiveMeeting,
outGW: OutMsgRouter
)(implicit context: akka.actor.ActorContext): Unit = {
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
Users2x.findWithIntId(liveMeeting.users2x, vu.intId).foreach { user =>
if (user.role == Roles.VIEWER_ROLE && !vu.listenOnly && user.locked) {
@ -44,12 +45,20 @@ object LockSettingsUtil {
}
}
def enforceLockSettingsForAllVoiceUsers(liveMeeting: LiveMeeting, outGW: OutMsgRouter): Unit = {
def enforceLockSettingsForAllVoiceUsers(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter
)(implicit context: akka.actor.ActorContext): Unit = {
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
applyMutingOfUsers(permissions.disableMic, liveMeeting, outGW)
}
def enforceLockSettingsForVoiceUser(voiceUser: VoiceUserState, liveMeeting: LiveMeeting, outGW: OutMsgRouter): Unit = {
def enforceLockSettingsForVoiceUser(
voiceUser: VoiceUserState,
liveMeeting: LiveMeeting,
outGW: OutMsgRouter
)(implicit context: akka.actor.ActorContext): Unit = {
val permissions = MeetingStatus2x.getPermissions(liveMeeting.status)
if (permissions.disableMic) {
Users2x.findWithIntId(liveMeeting.users2x, voiceUser.intId).foreach { user =>
@ -65,7 +74,11 @@ object LockSettingsUtil {
}
}
private def enforceListenOnlyUserIsMuted(intUserId: String, liveMeeting: LiveMeeting, outGW: OutMsgRouter): Unit = {
private def enforceListenOnlyUserIsMuted(
intUserId: String,
liveMeeting: LiveMeeting,
outGW: OutMsgRouter
)(implicit context: akka.actor.ActorContext): Unit = {
val voiceUser = VoiceUsers.findWithIntId(liveMeeting.voiceUsers, intUserId)
voiceUser.foreach { vu =>
// Make sure that listen only user is muted. (ralam dec 6, 2019

View File

@ -43,6 +43,7 @@ trait SystemConfiguration {
lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true)
lazy val dialInApprovalAudioPath = Try(config.getString("voiceConf.dialInApprovalAudioPath")).getOrElse("ivr/ivr-please_hold_while_party_contacted.wav")
lazy val toggleListenOnlyAfterMuteTimer = Try(config.getInt("voiceConf.toggleListenOnlyAfterMuteTimer")).getOrElse(4)
lazy val transparentListenOnlyThreshold = Try(config.getInt("voiceConf.transparentListenOnlyThreshold")).getOrElse(0)
lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.users
import org.bigbluebutton.common2.msgs.MuteUserCmdMsg
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.apps.voice.VoiceApp
import org.bigbluebutton.core.models.{ Roles, Users2x, VoiceUsers }
import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x
@ -51,13 +52,12 @@ trait MuteUserCmdMsgHdlr extends RightsManagementTrait {
} else {
if (u.muted != msg.body.mute) {
log.info("Send mute user request. meetingId=" + meetingId + " userId=" + u.intId + " user=" + u)
val event = MsgBuilder.buildMuteUserInVoiceConfSysMsg(
meetingId,
voiceConf,
u.voiceUserId,
VoiceApp.muteUserInVoiceConf(
liveMeeting,
outGW,
u.intId,
msg.body.mute
)
outGW.send(event)
}
}
}

View File

@ -138,7 +138,7 @@ object VoiceApp extends SystemConfiguration {
// If the user is muted or unmuted with an unheld channel, broadcast
// the event right away.
// If the user is unmuted, but channel is held, we need to wait for the
// If the user is unmuted, but channel is held, we need to wait for the
// channel to be active again to broadcast the event. See
// VoiceApp.handleChannelHoldChanged for this second case.
if (muted || (!muted && !mutedUser.hold)) {
@ -149,7 +149,6 @@ object VoiceApp extends SystemConfiguration {
outGW
)
}
}
}
@ -260,7 +259,7 @@ object VoiceApp extends SystemConfiguration {
callingInto: String,
hold: Boolean,
uuid: String = "unused"
): Unit = {
)(implicit context: akka.actor.ActorContext): Unit = {
def broadcastEvent(voiceUserState: VoiceUserState): Unit = {
val routing = Routing.addMsgToClientRouting(
@ -323,8 +322,28 @@ object VoiceApp extends SystemConfiguration {
hold,
uuid
)
val prevTransparentLOStatus = VoiceHdlrHelpers.transparentListenOnlyAllowed(
liveMeeting
)
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
val newTransparentLOStatus = VoiceHdlrHelpers.transparentListenOnlyAllowed(
liveMeeting
)
if (prevTransparentLOStatus != newTransparentLOStatus) {
// If the transparent listen only mode was activated or deactivated
// we need to update the listen only mode for all users in the meeting
// that are not muted.
handleTransparentLOModeChange(
liveMeeting,
outGW,
newTransparentLOStatus
)
}
broadcastEvent(voiceUserState)
if (liveMeeting.props.meetingProp.isBreakout) {
@ -473,6 +492,22 @@ object VoiceApp extends SystemConfiguration {
}
}
def handleTransparentLOModeChange(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
allowed: Boolean,
)(implicit context: akka.actor.ActorContext): Unit = {
VoiceUsers.findAllMutedVoiceUsers(liveMeeting.voiceUsers) foreach { vu =>
toggleListenOnlyMode(
liveMeeting,
outGW,
vu.intId,
vu.callerNum,
allowed
)
}
}
def toggleListenOnlyMode(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
@ -482,6 +517,16 @@ object VoiceApp extends SystemConfiguration {
delay: Int = 0
)(implicit context: ActorContext): Unit = {
implicit def executionContext = context.system.dispatcher
val allowed = VoiceHdlrHelpers.transparentListenOnlyAllowed(liveMeeting)
// Guarantee there are no other tasks for this channel
removeToggleListenOnlyTask(userId)
// If the meeting has not yet hit the minium amount of duplex channels
// for transparent listen only to be enabled, we don't need to do anything
if (!allowed && enabled) {
return
}
def broacastEvent(): Unit = {
val event = MsgBuilder.buildToggleListenOnlyModeSysMsg(
liveMeeting.props.meetingProp.intId,
@ -493,9 +538,6 @@ object VoiceApp extends SystemConfiguration {
outGW.send(event)
}
// Guarantee there are no other tasks for this channel
removeToggleListenOnlyTask(userId)
if (enabled && delay > 0) {
// If we are enabling listen only mode, we wait a bit before actually
// dispatching the command - the idea is that recently muted users
@ -571,4 +613,48 @@ object VoiceApp extends SystemConfiguration {
case _ =>
}
}
def muteUserInVoiceConf(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
userId: String,
muted: Boolean
)(implicit context: akka.actor.ActorContext): Unit = {
for {
u <- VoiceUsers.findWithIntId(
liveMeeting.voiceUsers,
userId
)
} yield {
if (u.muted != muted) {
val muteEvent = MsgBuilder.buildMuteUserInVoiceConfSysMsg(
liveMeeting.props.meetingProp.intId,
liveMeeting.props.voiceProp.voiceConf,
u.voiceUserId,
muted
)
// If we're unmuting, trigger a channel unhold -> toggle listen only
// mode -> unmute
if (!muted) {
holdChannelInVoiceConf(
liveMeeting,
outGW,
u.uuid,
muted
)
toggleListenOnlyMode(
liveMeeting,
outGW,
u.intId,
u.callerNum,
muted,
0
)
}
outGW.send(muteEvent)
}
}
}
}

View File

@ -50,4 +50,14 @@ object VoiceHdlrHelpers extends SystemConfiguration {
case _ => false
}
}
def transparentListenOnlyAllowed(liveMeeting: LiveMeeting): Boolean = {
// Transparent listen only meeting-wide activation threshold.
// Threshold is the number of muted duplex audio channels in a meeting.
// 0 means no threshold, all users are subject to it
val mutedDuplexChannels = VoiceUsers.findAllMutedVoiceUsers(liveMeeting.voiceUsers).length
val threshold = transparentListenOnlyThreshold
(threshold == 0) || (mutedDuplexChannels >= threshold)
}
}

View File

@ -33,6 +33,7 @@ object VoiceUsers {
def findAllListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == true)
def findAllFreeswitchCallers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.calledInto == "freeswitch")
def findAllKurentoCallers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.calledInto == "kms")
def findAllMutedVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.muted == true && u.listenOnly == false)
def findAllBannedCallers(users: VoiceUsers): Vector[VoiceUserState] = users.bannedUsers.values.toVector

View File

@ -6,6 +6,7 @@ import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
import org.bigbluebutton.core.apps.voice.VoiceApp
trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
@ -57,8 +58,8 @@ trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
if (!vu.listenOnly) {
Users2x.findWithIntId(liveMeeting.users2x, vu.intId) match {
case Some(u) => if (!u.presenter) muteUserInVoiceConf(vu, muted)
case None => muteUserInVoiceConf(vu, muted)
case Some(u) => if (!u.presenter) VoiceApp.muteUserInVoiceConf(liveMeeting, outGW, vu.intId, muted)
case None => VoiceApp.muteUserInVoiceConf(liveMeeting, outGW, vu.intId, muted)
}
}
}
@ -82,17 +83,4 @@ trait MuteAllExceptPresentersCmdMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def muteUserInVoiceConf(vu: VoiceUserState, mute: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, vu.intId)
val envelope = BbbCoreEnvelope(MuteUserInVoiceConfSysMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(MuteUserInVoiceConfSysMsg.NAME, props.meetingProp.intId)
val body = MuteUserInVoiceConfSysMsgBody(props.voiceProp.voiceConf, vu.voiceUserId, mute)
val event = MuteUserInVoiceConfSysMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
}

View File

@ -6,6 +6,7 @@ import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
import org.bigbluebutton.core2.MeetingStatus2x
import org.bigbluebutton.core2.message.senders.{ MsgBuilder }
import org.bigbluebutton.core.apps.voice.VoiceApp
trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
this: MeetingActor =>
@ -30,19 +31,6 @@ trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
BbbCommonEnvCoreMsg(envelope, event)
}
def muteUserInVoiceConf(vu: VoiceUserState, mute: Boolean): Unit = {
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId, vu.intId)
val envelope = BbbCoreEnvelope(MuteUserInVoiceConfSysMsg.NAME, routing)
val header = BbbCoreHeaderWithMeetingId(MuteUserInVoiceConfSysMsg.NAME, props.meetingProp.intId)
val body = MuteUserInVoiceConfSysMsgBody(props.voiceProp.voiceConf, vu.voiceUserId, mute)
val event = MuteUserInVoiceConfSysMsg(header, body)
val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
outGW.send(msgEvent)
}
if (msg.body.mute != MeetingStatus2x.isMeetingMuted(liveMeeting.status)) {
if (msg.body.mute) {
val notifyEvent = MsgBuilder.buildNotifyAllInMeetingEvtMsg(
@ -79,7 +67,7 @@ trait MuteMeetingCmdMsgHdlr extends RightsManagementTrait {
if (muted) {
VoiceUsers.findAll(liveMeeting.voiceUsers) foreach { vu =>
if (!vu.listenOnly) {
muteUserInVoiceConf(vu, muted)
VoiceApp.muteUserInVoiceConf(liveMeeting, outGW, vu.intId, muted)
}
}
}

View File

@ -97,6 +97,11 @@ voiceConf {
# Time (seconds) to wait before requesting an audio channel hold after
# muting a user. Used in the experimental, transparent listen only mode.
toggleListenOnlyAfterMuteTimer = 4
# Transparent listen only meeting-wide activation threshold.
# Threshold is the number of muted duplex audio channels in a meeting.
# 0 = disabled
transparentListenOnlyThreshold = 0
}
recording {