feat(audio): channel threshold for transparent listen only activation

Transparent listen only is currently only worth it for meetings with a
number of duplex audio channels larger than a certain value (dependant
on system performance). That is due to the fact that global audio
bridges created for the mechanism also use significant CPU (roughly the
same as an unheld duplex channel), which means it's cost is usually
offset only once there are enough potential channels to be held in a
conference.

This commit adds a new optional feature that introduces some dynamicity
for the mechanism: it'll only be triggered after at least
@voiceConf.transparentListenOnlyThreshold number of muted duplex
audio channels are present in a conference.
The default is 0 (always trigger transparent listen only if the general
mechanism is activated).
This commit is contained in:
prlanzarin 2024-03-21 15:57:16 -03:00
parent b4a11facd4
commit e6e1f28036
5 changed files with 64 additions and 4 deletions

View File

@ -43,6 +43,7 @@ trait SystemConfiguration {
lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true) 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 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 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) lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)

View File

@ -260,7 +260,7 @@ object VoiceApp extends SystemConfiguration {
callingInto: String, callingInto: String,
hold: Boolean, hold: Boolean,
uuid: String = "unused" uuid: String = "unused"
): Unit = { )(implicit context: akka.actor.ActorContext): Unit = {
def broadcastEvent(voiceUserState: VoiceUserState): Unit = { def broadcastEvent(voiceUserState: VoiceUserState): Unit = {
val routing = Routing.addMsgToClientRouting( val routing = Routing.addMsgToClientRouting(
@ -323,8 +323,28 @@ object VoiceApp extends SystemConfiguration {
hold, hold,
uuid uuid
) )
val prevTransparentLOStatus = VoiceHdlrHelpers.transparentListenOnlyAllowed(
liveMeeting
)
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState) 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) broadcastEvent(voiceUserState)
if (liveMeeting.props.meetingProp.isBreakout) { if (liveMeeting.props.meetingProp.isBreakout) {
@ -473,6 +493,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( def toggleListenOnlyMode(
liveMeeting: LiveMeeting, liveMeeting: LiveMeeting,
outGW: OutMsgRouter, outGW: OutMsgRouter,
@ -482,6 +518,16 @@ object VoiceApp extends SystemConfiguration {
delay: Int = 0 delay: Int = 0
)(implicit context: ActorContext): Unit = { )(implicit context: ActorContext): Unit = {
implicit def executionContext = context.system.dispatcher 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 = { def broacastEvent(): Unit = {
val event = MsgBuilder.buildToggleListenOnlyModeSysMsg( val event = MsgBuilder.buildToggleListenOnlyModeSysMsg(
liveMeeting.props.meetingProp.intId, liveMeeting.props.meetingProp.intId,
@ -493,9 +539,6 @@ object VoiceApp extends SystemConfiguration {
outGW.send(event) outGW.send(event)
} }
// Guarantee there are no other tasks for this channel
removeToggleListenOnlyTask(userId)
if (enabled && delay > 0) { if (enabled && delay > 0) {
// If we are enabling listen only mode, we wait a bit before actually // If we are enabling listen only mode, we wait a bit before actually
// dispatching the command - the idea is that recently muted users // dispatching the command - the idea is that recently muted users

View File

@ -50,4 +50,14 @@ object VoiceHdlrHelpers extends SystemConfiguration {
case _ => false 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 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 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 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 def findAllBannedCallers(users: VoiceUsers): Vector[VoiceUserState] = users.bannedUsers.values.toVector

View File

@ -97,6 +97,11 @@ voiceConf {
# Time (seconds) to wait before requesting an audio channel hold after # Time (seconds) to wait before requesting an audio channel hold after
# muting a user. Used in the experimental, transparent listen only mode. # muting a user. Used in the experimental, transparent listen only mode.
toggleListenOnlyAfterMuteTimer = 4 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 { recording {