From 172e31b78ea194f6a8d876c7f14207bd4fa403ad Mon Sep 17 00:00:00 2001
From: prlanzarin <prlanzarin@inf.ufrgs.br>
Date: Fri, 5 Jun 2020 16:11:36 +0000
Subject: [PATCH] Rework of the floor event

Now it`s called AudioFloorChanged* to properly reflect its role

Add fields to carry the floor state (boolean) along with the uID and vID to be able to send events for a floor takeover and a floor surrender
---
 .../UserConnectedToGlobalAudioMsgHdlr.scala   |  4 +-
 ...AudioFloorChangedVoiceConfEvtMsgHdlr.scala | 61 +++++++++++++++++++
 .../core/apps/voice/VoiceApp.scala            |  4 +-
 .../core/apps/voice/VoiceApp2x.scala          |  1 +
 .../core/models/VoiceUsers.scala              | 49 +++++++++++----
 .../senders/ReceivedJsonMsgHandlerActor.scala |  2 +
 .../core/running/MeetingActor.scala           |  5 +-
 .../core2/testdata/FakeUserGenerator.scala    | 10 +--
 .../FreeswitchConferenceEventListener.java    |  8 +++
 .../voice/IVoiceConferenceService.java        |  5 ++
 .../voice/events/AudioFloorChangedEvent.java  | 50 +++++++++++++++
 .../voice/freeswitch/ESLEventListener.java    | 23 +++++++
 .../freeswitch/VoiceConferenceService.scala   | 22 +++++++
 .../common2/msgs/VoiceConfMsgs.scala          | 18 ++++++
 .../freeswitch/esl/client/inbound/Client.java |  6 ++
 15 files changed, 249 insertions(+), 19 deletions(-)
 create mode 100644 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/AudioFloorChangedVoiceConfEvtMsgHdlr.scala
 create mode 100644 akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/AudioFloorChangedEvent.java

diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala
index bc938437c0..937ebd1cc0 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserConnectedToGlobalAudioMsgHdlr.scala
@@ -40,7 +40,9 @@ trait UserConnectedToGlobalAudioMsgHdlr {
         talking = false,
         listenOnly = true,
         "kms",
-        System.currentTimeMillis()
+        System.currentTimeMillis(),
+        floor = false,
+        lastFloorTime = "0",
       )
 
       VoiceUsers.add(liveMeeting.voiceUsers, vu)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/AudioFloorChangedVoiceConfEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/AudioFloorChangedVoiceConfEvtMsgHdlr.scala
new file mode 100644
index 0000000000..eaeea145a0
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/AudioFloorChangedVoiceConfEvtMsgHdlr.scala
@@ -0,0 +1,61 @@
+package org.bigbluebutton.core.apps.voice
+
+import org.bigbluebutton.common2.msgs._
+import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
+import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
+
+trait AudioFloorChangedVoiceConfEvtMsgHdlr {
+  this: BaseMeetingActor =>
+
+  val liveMeeting: LiveMeeting
+  val outGW: OutMsgRouter
+
+  def handleAudioFloorChangedVoiceConfEvtMsg(msg: AudioFloorChangedVoiceConfEvtMsg): Unit = {
+
+    def broadcastEvent(vu: VoiceUserState): Unit = {
+      val routing = Routing.addMsgToClientRouting(
+        MessageTypes.BROADCAST_TO_MEETING,
+        liveMeeting.props.meetingProp.intId,
+        vu.intId
+      )
+      val envelope = BbbCoreEnvelope(AudioFloorChangedEvtMsg.NAME, routing)
+      val header = BbbClientMsgHeader(
+        AudioFloorChangedEvtMsg.NAME,
+        liveMeeting.props.meetingProp.intId, vu.intId
+      )
+
+      val body = AudioFloorChangedEvtMsgBody(
+        voiceConf = msg.header.voiceConf,
+        intId = vu.intId,
+        voiceUserId = vu.voiceUserId,
+        floor = vu.floor,
+        lastFloorTime = msg.body.floorTimestamp
+      )
+
+      val event = AudioFloorChangedEvtMsg(header, body)
+      val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
+      outGW.send(msgEvent)
+    }
+
+    for {
+      oldFloorUser <- VoiceUsers.releasedFloor(
+        liveMeeting.voiceUsers,
+        msg.body.oldVoiceUserId,
+        floor = false
+      )
+    } yield {
+      broadcastEvent(oldFloorUser)
+    }
+
+    for {
+      newFloorUser <- VoiceUsers.becameFloor(
+        liveMeeting.voiceUsers,
+        msg.body.voiceUserId,
+        true,
+        msg.body.floorTimestamp
+      )
+    } yield {
+      broadcastEvent(newFloorUser)
+    }
+  }
+}
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
index 4a49d961f1..e0ba15cdc9 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp.scala
@@ -268,7 +268,9 @@ object VoiceApp extends SystemConfiguration {
       talking,
       listenOnly = isListenOnly,
       callingInto,
-      System.currentTimeMillis()
+      System.currentTimeMillis(),
+      floor = false,
+      lastFloorTime = "0"
     )
     VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
index c5f0662066..409f29112a 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
@@ -18,6 +18,7 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr
   with RecordingStartedVoiceConfEvtMsgHdlr
   with VoiceConfRunningEvtMsgHdlr
   with SyncGetVoiceUsersMsgHdlr
+  with AudioFloorChangedVoiceConfEvtMsgHdlr
   with VoiceConfCallStateEvtMsgHdlr
   with UserStatusVoiceConfEvtMsgHdlr {
 
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala
index 58cfb7dcdc..ee9d528fe1 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/models/VoiceUsers.scala
@@ -67,6 +67,29 @@ object VoiceUsers {
     }
   }
 
+  def becameFloor(users: VoiceUsers, voiceUserId: String, floor: Boolean, timestamp: String): Option[VoiceUserState] = {
+    for {
+      u <- findWithVoiceUserId(users, voiceUserId)
+    } yield {
+      val vu = u.modify(_.floor).setTo(floor)
+        .modify(_.lastFloorTime).setTo(timestamp)
+        .modify(_.lastStatusUpdateOn).setTo(System.currentTimeMillis())
+      users.save(vu)
+      vu
+    }
+  }
+
+  def releasedFloor(users: VoiceUsers, voiceUserId: String, floor: Boolean): Option[VoiceUserState] = {
+    for {
+      u <- findWithVoiceUserId(users, voiceUserId)
+    } yield {
+      val vu = u.modify(_.floor).setTo(floor)
+        .modify(_.lastStatusUpdateOn).setTo(System.currentTimeMillis())
+      users.save(vu)
+      vu
+    }
+  }
+
   def setLastStatusUpdate(users: VoiceUsers, user: VoiceUserState): VoiceUserState = {
     val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis())
     users.save(vu)
@@ -130,16 +153,18 @@ case class VoiceUser2x(
     voiceUserId: String
 )
 case class VoiceUserVO2x(
-    intId:       String,
-    voiceUserId: String,
-    callerName:  String,
-    callerNum:   String,
-    joined:      Boolean,
-    locked:      Boolean,
-    muted:       Boolean,
-    talking:     Boolean,
-    callingWith: String,
-    listenOnly:  Boolean
+    intId:         String,
+    voiceUserId:   String,
+    callerName:    String,
+    callerNum:     String,
+    joined:        Boolean,
+    locked:        Boolean,
+    muted:         Boolean,
+    talking:       Boolean,
+    callingWith:   String,
+    listenOnly:    Boolean,
+    floor:         Boolean,
+    lastFloorTime: String
 )
 
 case class VoiceUserState(
@@ -152,5 +177,7 @@ case class VoiceUserState(
     talking:            Boolean,
     listenOnly:         Boolean,
     calledInto:         String,
-    lastStatusUpdateOn: Long
+    lastStatusUpdateOn: Long,
+    floor:              Boolean,
+    lastFloorTime:      String
 )
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
index fcb3dcf925..7c45cc4cec 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
@@ -167,6 +167,8 @@ class ReceivedJsonMsgHandlerActor(
         routeGenericMsg[MuteMeetingCmdMsg](envelope, jsonNode)
       case IsMeetingMutedReqMsg.NAME =>
         routeGenericMsg[IsMeetingMutedReqMsg](envelope, jsonNode)
+      case AudioFloorChangedVoiceConfEvtMsg.NAME =>
+        routeVoiceMsg[AudioFloorChangedVoiceConfEvtMsg](envelope, jsonNode)
       case CheckRunningAndRecordingVoiceConfEvtMsg.NAME =>
         routeVoiceMsg[CheckRunningAndRecordingVoiceConfEvtMsg](envelope, jsonNode)
       case UserStatusVoiceConfEvtMsg.NAME =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
index e72b3f4bf8..736d847577 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
@@ -381,9 +381,10 @@ class MeetingActor(
       case m: UserTalkingInVoiceConfEvtMsg =>
         updateVoiceUserLastActivity(m.body.voiceUserId)
         handleUserTalkingInVoiceConfEvtMsg(m)
-      case m: VoiceConfCallStateEvtMsg        => handleVoiceConfCallStateEvtMsg(m)
+      case m: VoiceConfCallStateEvtMsg         => handleVoiceConfCallStateEvtMsg(m)
 
-      case m: RecordingStartedVoiceConfEvtMsg => handleRecordingStartedVoiceConfEvtMsg(m)
+      case m: RecordingStartedVoiceConfEvtMsg  => handleRecordingStartedVoiceConfEvtMsg(m)
+      case m: AudioFloorChangedVoiceConfEvtMsg => handleAudioFloorChangedVoiceConfEvtMsg(m)
       case m: MuteUserCmdMsg =>
         usersApp.handleMuteUserCmdMsg(m)
         updateUserLastActivity(m.body.mutedBy)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala
index 9c22ee0120..7b7f62a6d3 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/testdata/FakeUserGenerator.scala
@@ -60,19 +60,21 @@ object FakeUserGenerator {
   }
 
   def createFakeVoiceUser(user: RegisteredUser, callingWith: String, muted: Boolean, talking: Boolean,
-                          listenOnly: Boolean): VoiceUserState = {
+                          listenOnly: Boolean, floor: Boolean = false): VoiceUserState = {
     val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
+    val lastFloorTime = System.currentTimeMillis().toString();
     VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
-      callerNum = user.name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis())
+      callerNum = user.name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
   }
 
   def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
-                              listenOnly: Boolean): VoiceUserState = {
+                              listenOnly: Boolean, floor: Boolean = false): VoiceUserState = {
     val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
     val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
     val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
+    val lastFloorTime = System.currentTimeMillis().toString();
     VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
-      callerNum = name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis())
+      callerNum = name, muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
   }
 
   def createFakeWebcamStreamFor(userId: String, viewers: Set[String]): WebcamStream = {
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
index 0242b524e0..33b37f0e7f 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
@@ -89,6 +89,14 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
             vcs.deskShareRTMPBroadcastStopped(evt.getRoom(), evt.getBroadcastingStreamUrl(),
               evt.getVideoWidth(), evt.getVideoHeight(), evt.getTimestamp());
           }
+        } else if (event instanceof AudioFloorChangedEvent) {
+          AudioFloorChangedEvent evt = (AudioFloorChangedEvent) event;
+          vcs.audioFloorChanged(
+            evt.getRoom(),
+            evt.getVoiceUserId(),
+            evt.getOldVoiceUserId(),
+            evt.getFloorTimestamp()
+          );
         } else if (event instanceof VoiceConfRunningAndRecordingEvent) {
           VoiceConfRunningAndRecordingEvent evt = (VoiceConfRunningAndRecordingEvent) event;
           if (evt.running && ! evt.recording) {
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
index c60c269843..3dd8fc5af2 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
@@ -64,6 +64,11 @@ public interface IVoiceConferenceService {
                                      Integer videoHeight,
                                      String timestamp);
 
+  void audioFloorChanged(String room,
+                         String voiceUserId,
+                         String oldVoiceUserId,
+                         String floorTimestamp);
+
   void voiceConfRunningAndRecording(String room,
                                     Boolean isRunning,
                                     Boolean isRecording,
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/AudioFloorChangedEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/AudioFloorChangedEvent.java
new file mode 100644
index 0000000000..ede258c81a
--- /dev/null
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/AudioFloorChangedEvent.java
@@ -0,0 +1,50 @@
+/**
+* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+*
+* Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+*
+* This program is free software; you can redistribute it and/or modify it under the
+* terms of the GNU Lesser General Public License as published by the Free Software
+* Foundation; either version 3.0 of the License, or (at your option) any later
+* version.
+*
+* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License along
+* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+*
+*/
+package org.bigbluebutton.freeswitch.voice.events;
+
+public class AudioFloorChangedEvent extends VoiceConferenceEvent {
+
+	private final String voiceUserId;
+	private final String oldVoiceUserId;
+	private final String floorTimestamp;
+
+	public AudioFloorChangedEvent(
+			String room,
+			String voiceUserId,
+			String oldVoiceUserId,
+			String floorTimestamp
+		) {
+		super(room);
+		this.voiceUserId = voiceUserId;
+		this.oldVoiceUserId = oldVoiceUserId;
+		this.floorTimestamp = floorTimestamp;
+	}
+
+	public String getVoiceUserId() {
+		return voiceUserId;
+	}
+
+	public String getOldVoiceUserId() {
+		return oldVoiceUserId;
+	}
+
+	public String getFloorTimestamp() {
+		return floorTimestamp;
+	}
+}
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
index c42e80f7be..5711927f21 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
@@ -26,6 +26,7 @@ public class ESLEventListener implements IEslEventListener {
     private static final String STOP_RECORDING_EVENT = "stop-recording";
     private static final String CONFERENCE_CREATED_EVENT = "conference-create";
     private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy";
+    private static final String FLOOR_CHANGE_EVENT = "video-floor-change";
 
     private static final String SCREENSHARE_CONFERENCE_NAME_SUFFIX = "-SCREENSHARE";
 
@@ -197,6 +198,12 @@ public class ESLEventListener implements IEslEventListener {
         } else if (action.equals(CONFERENCE_DESTROYED_EVENT)) {
             VoiceConfRunningEvent pt = new VoiceConfRunningEvent(confName, false);
             conferenceEventListener.handleConferenceEvent(pt);
+        } else if (action.equals(FLOOR_CHANGE_EVENT)) {
+            String holderMemberId = this.getNewFloorHolderMemberIdFromEvent(event);
+            String oldHolderMemberId = this.getOldFloorHolderMemberIdFromEvent(event);
+            String floorTimestamp = event.getEventHeaders().get("Event-Date-Timestamp");
+            AudioFloorChangedEvent vFloor= new AudioFloorChangedEvent(confName, holderMemberId, oldHolderMemberId, floorTimestamp);
+            conferenceEventListener.handleConferenceEvent(vFloor);
         } else {
             log.warn("Unknown conference Action [" + action + "]");
         }
@@ -507,6 +514,22 @@ public class ESLEventListener implements IEslEventListener {
         return e.getEventHeaders().get("Path");
     }
 
+    private String getOldFloorHolderMemberIdFromEvent(EslEvent e) {
+        String oldFloorHolder = e.getEventHeaders().get("Old-ID");
+        if(oldFloorHolder == null || oldFloorHolder.equalsIgnoreCase("none")) {
+            oldFloorHolder= "";
+        }
+        return oldFloorHolder;
+    }
+
+    private String getNewFloorHolderMemberIdFromEvent(EslEvent e) {
+        String newHolder = e.getEventHeaders().get("New-ID");
+        if(newHolder == null || newHolder.equalsIgnoreCase("none")) {
+            newHolder = "";
+        }
+        return newHolder;
+    }
+
     // Distinguish between recording to a file:
     // /path/to/a/file.mp4
     // and broadcasting a stream:
diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
index c6da363f8c..2194688cc8 100755
--- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
+++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
@@ -276,6 +276,28 @@ class VoiceConferenceService(healthz: HealthzService,
     sender.publish(fromVoiceConfRedisChannel, json)
   }
 
+  def audioFloorChanged(
+      voiceConfId: String,
+      voiceUserId: String,
+      oldVoiceUserId: String,
+      floorTimestamp: String
+  ) {
+    val header = BbbCoreVoiceConfHeader(AudioFloorChangedVoiceConfEvtMsg.NAME, voiceConfId)
+    val body = AudioFloorChangedVoiceConfEvtMsgBody(
+      voiceConfId,
+      voiceUserId,
+      oldVoiceUserId,
+      floorTimestamp
+    );
+    val envelope = BbbCoreEnvelope(AudioFloorChangedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
+
+    val msg = new AudioFloorChangedVoiceConfEvtMsg(header, body)
+    val msgEvent = BbbCommonEnvCoreMsg(envelope, msg)
+
+    val json = JsonUtil.toJson(msgEvent)
+    sender.publish(fromVoiceConfRedisChannel, json)
+  }
+
   def voiceCallStateEvent(
       conf:             String,
       callSession:      String,
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
index a6d15a7ffa..7c9fa7b61b 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
@@ -495,6 +495,24 @@ object SyncGetVoiceUsersRespMsg { val NAME = "SyncGetVoiceUsersRespMsg" }
 case class SyncGetVoiceUsersRespMsg(header: BbbClientMsgHeader, body: SyncGetVoiceUsersRespMsgBody) extends BbbCoreMsg
 case class SyncGetVoiceUsersRespMsgBody(voiceUsers: Vector[VoiceConfUser])
 
+/**
+ * Received from FS that a user has become a floor holder
+ */
+object AudioFloorChangedVoiceConfEvtMsg { val NAME = "AudioFloorChangedVoiceConfEvtMsg" }
+case class AudioFloorChangedVoiceConfEvtMsg(
+    header: BbbCoreVoiceConfHeader,
+    body:   AudioFloorChangedVoiceConfEvtMsgBody
+) extends VoiceStandardMsg
+case class AudioFloorChangedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, oldVoiceUserId: String, floorTimestamp: String)
+
+/**
+ * Sent to a client that an user has become a floor holder
+ */
+
+object AudioFloorChangedEvtMsg { val NAME = "AudioFloorChangedEvtMsg" }
+case class AudioFloorChangedEvtMsg(header: BbbClientMsgHeader, body: AudioFloorChangedEvtMsgBody) extends BbbCoreMsg
+case class AudioFloorChangedEvtMsgBody(voiceConf: String, intId: String, voiceUserId: String, floor: Boolean, lastFloorTime: String)
+
 /**
  * Received from FS call state events.
  */
diff --git a/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java b/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
index dc5d0f6d46..e09669031f 100755
--- a/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
+++ b/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
@@ -519,6 +519,12 @@ public class Client
                                     } else if (eventFunc.equals("conference_loop_input")) {
                                         listener.conferenceEventAction(uniqueId, confName, confSize, eventAction, event);
                                         return;
+                                    } else if (eventFunc.equals("conference_member_set_floor_holder")) {
+                                        listener.conferenceEventAction(uniqueId, confName, confSize, eventAction, event);
+                                        return;
+                                    } else if (eventFunc.equals("conference_video_set_floor_holder")) {
+                                        listener.conferenceEventAction(uniqueId, confName, confSize, eventAction, event);
+                                        return;
                                     } else if (eventFunc.equals("stop_talking_handler")) {
                                         listener.conferenceEventAction(uniqueId, confName, confSize, eventAction, event);
                                         return;
-- 
GitLab