diff --git a/src/main/java/org/jitsi/jigasi/AudioModeration.java b/src/main/java/org/jitsi/jigasi/AudioModeration.java
index 77d04aca8..68c3bfd80 100644
--- a/src/main/java/org/jitsi/jigasi/AudioModeration.java
+++ b/src/main/java/org/jitsi/jigasi/AudioModeration.java
@@ -22,10 +22,15 @@
import net.java.sip.communicator.util.*;
import org.jitsi.jigasi.sip.*;
import org.jitsi.jigasi.util.*;
+import org.jitsi.xmpp.extensions.*;
import org.jitsi.xmpp.extensions.jitsimeet.*;
import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.iqrequest.*;
import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.disco.*;
+import org.jivesoftware.smackx.disco.packet.*;
+import org.json.simple.parser.*;
import org.jxmpp.jid.*;
import org.json.simple.*;
@@ -78,18 +83,45 @@ public class AudioModeration
{
initialAudioMutedExtension.setAudioMuted(false);
}
-
/**
* The call context used to create this conference, contains info as
* room name and room password and other optional parameters.
*/
private final CallContext callContext;
+ /**
+ * We keep track of the AV moderation component to be able to trust the incoming messages.
+ */
+ private String avModerationAddress = null;
+
+ /**
+ * Whether AV moderation is currently enabled.
+ */
+ private boolean avModerationEnabled = false;
+
+ /**
+ * Whether current jigasi provider is allowed to unmute itself. By default, AV moderation is not enabled,
+ * and we are allowed to unmute.
+ */
+ private boolean isAllowedToUnmute = true;
+
+ /**
+ * We use the same instance of extension so we can remove and add it from the default presence set of
+ * extensions.
+ */
+ private static final RaiseHandExtension lowerHandExtension = new RaiseHandExtension();
+
+ /**
+ * The listener for incoming messages with AV moderation commands.
+ */
+ private final AVModerationListener avModerationListener;
+
public AudioModeration(JvbConference jvbConference, SipGatewaySession gatewaySession, CallContext ctx)
{
this.gatewaySession = gatewaySession;
this.jvbConference = jvbConference;
this.callContext = ctx;
+ this.avModerationListener = new AVModerationListener();
}
/**
@@ -115,16 +147,21 @@ public void clean()
{
XMPPConnection connection = jvbConference.getConnection();
+ if (connection == null)
+ {
+ // if there is no connection nothing to clear
+ return;
+ }
+
if (muteIqHandler != null)
{
// we need to remove it from the connection, or we break some Smack
// weak references map where the key is connection and the value
// holds a connection and we leak connection/conferences.
- if (connection != null)
- {
- connection.unregisterIQRequestHandler(muteIqHandler);
- }
+ connection.unregisterIQRequestHandler(muteIqHandler);
}
+
+ connection.removeAsyncStanzaListener(this.avModerationListener);
}
/**
@@ -275,6 +312,12 @@ void setChatRoomAudioMuted(boolean muted)
{
// remove the initial extension otherwise it will overwrite our new setting
((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(initialAudioMutedExtension);
+
+ if (!muted)
+ {
+ // if we are unmuting make sure our raise hand is always lowered
+ ((ChatRoomJabberImpl) mucRoom).addPresencePacketExtensions(lowerHandExtension);
+ }
}
AudioMutedExtension audioMutedExtension = new AudioMutedExtension();
@@ -300,6 +343,23 @@ public boolean requestAudioMuteByJicofo(boolean bMuted)
{
ChatRoom mucRoom = this.jvbConference.getJvbRoom();
+ if (!bMuted && this.avModerationEnabled && !isAllowedToUnmute)
+ {
+ OperationSetJitsiMeetTools jitsiMeetTools
+ = this.jvbConference.getXmppProvider().getOperationSet(OperationSetJitsiMeetTools.class);
+
+ if (mucRoom instanceof ChatRoomJabberImpl)
+ {
+ // remove the default value which is lowering the hand
+ ((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(lowerHandExtension);
+ }
+
+ // let's raise hand
+ jitsiMeetTools.sendPresenceExtension(mucRoom, new RaiseHandExtension().setRaisedHandValue(true));
+
+ return false;
+ }
+
StanzaCollector collector = null;
try
{
@@ -356,7 +416,8 @@ && isMutingSupported()
&& sipCall != null
&& sipCall.getCallState() == CallState.CALL_IN_PROGRESS)
{
- if (this.requestAudioMuteByJicofo(startAudioMuted))
+ // we do not want to process start muted if AV moderation is enabled, as we are already muted
+ if (!this.avModerationEnabled && this.requestAudioMuteByJicofo(true))
{
mute();
}
@@ -382,7 +443,9 @@ public void mute()
try
{
- logger.info(this.callContext + " Sending mute request ");
+ logger.info(this.callContext + " Sending mute request avModeration:" + this.avModerationEnabled
+ + " allowed to unmute:" + this.isAllowedToUnmute);
+
this.gatewaySession.sendJson(callPeer, SipInfoJsonProtocol.createSIPJSONAudioMuteRequest(true));
}
catch (Exception ex)
@@ -391,6 +454,56 @@ public void mute()
}
}
+ /**
+ * The xmpp provider for JvbConference has registered after connecting.
+ */
+ public void xmppProviderRegistered()
+ {
+ // we are here in the RegisterThread, and it is safe to query and wait
+ // Uses disco info to discover the AV moderation address.
+ if (this.callContext.getDomain() != null)
+ {
+ try
+ {
+ long startQuery = System.currentTimeMillis();
+ DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(this.jvbConference.getConnection())
+ .discoverInfo(JidCreate.domainBareFrom(this.callContext.getDomain()));
+
+ DiscoverInfo.Identity avIdentity =
+ info.getIdentities().stream().
+ filter(di -> di.getCategory().equals("component") && di.getType().equals("av_moderation"))
+ .findFirst().orElse(null);
+
+ if (avIdentity != null)
+ {
+ this.avModerationAddress = avIdentity.getName();
+ logger.info(String.format("%s Discovered %s for %oms.",
+ this.callContext, this.avModerationAddress, System.currentTimeMillis() - startQuery));
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error("Error querying for av moderation address", e);
+ }
+ }
+
+ if (this.avModerationAddress != null)
+ {
+ try
+ {
+ this.jvbConference.getConnection().addAsyncStanzaListener(
+ this.avModerationListener,
+ new AndFilter(
+ MessageTypeFilter.NORMAL,
+ FromMatchesFilter.create(JidCreate.domainBareFrom(this.avModerationAddress))));
+ }
+ catch(Exception e)
+ {
+ logger.error("Error adding AV moderation listener", e);
+ }
+ }
+ }
+
/**
* Handles mute requests received by jicofo if enabled.
*/
@@ -442,4 +555,111 @@ private IQ handleMuteIq(MuteIq muteIq)
return IQ.createResultIQ(muteIq);
}
}
+
+ /**
+ * Added to presence to raise hand.
+ */
+ private static class RaiseHandExtension
+ extends AbstractPacketExtension
+ {
+ /**
+ * The namespace of this packet extension.
+ */
+ public static final String NAMESPACE = "jabber:client";
+
+ /**
+ * XML element name of this packet extension.
+ */
+ public static final String ELEMENT_NAME = "jitsi_participant_raisedHand";
+
+ /**
+ * Creates a {@link org.jitsi.xmpp.extensions.jitsimeet.TranslationLanguageExtension} instance.
+ */
+ public RaiseHandExtension()
+ {
+ super(NAMESPACE, ELEMENT_NAME);
+ }
+
+ /**
+ * Sets user's audio muted status.
+ *
+ * @param value true or false which indicates audio
+ * muted status of the user.
+ */
+ public ExtensionElement setRaisedHandValue(Boolean value)
+ {
+ setText(value ? value.toString() : null);
+
+ return this;
+ }
+ }
+
+ /**
+ * Listens for incoming messages with AV moderation commands.
+ */
+ private class AVModerationListener
+ implements StanzaListener
+ {
+ @Override
+ public void processStanza(Stanza packet)
+ {
+ JsonMessageExtension jsonMsg = packet.getExtension(
+ JsonMessageExtension.ELEMENT_NAME, JsonMessageExtension.NAMESPACE);
+
+ if (jsonMsg == null)
+ {
+ return;
+ }
+
+ try
+ {
+ Object o = new JSONParser().parse(jsonMsg.getJson());
+
+ if (o instanceof JSONObject)
+ {
+ JSONObject data = (JSONObject) o;
+
+ if (data.get("type").equals("av_moderation"))
+ {
+ Object enabledObj = data.get("enabled");
+ Object approvedObj = data.get("approved");
+ Object removedObj = data.get("removed");
+ Object mediaTypeObj = data.get("mediaType");
+
+ // we are interested only in audio moderation
+ if (mediaTypeObj == null || !mediaTypeObj.equals("audio"))
+ {
+ return;
+ }
+
+ if (enabledObj != null)
+ {
+ avModerationEnabled = (Boolean) enabledObj;
+ logger.info(callContext + " AV moderation has been enabled:" + avModerationEnabled);
+
+ // we will receive separate message when we are allowed to unmute
+ isAllowedToUnmute = false;
+
+ gatewaySession.sendJson(
+ SipInfoJsonProtocol.createAVModerationEnabledNotification(avModerationEnabled));
+ }
+ else if (removedObj != null && (Boolean) removedObj)
+ {
+ isAllowedToUnmute = false;
+ gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationDeniedNotification());
+ }
+ else if (approvedObj != null && (Boolean) approvedObj)
+ {
+ isAllowedToUnmute = true;
+ gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationApprovedNotification());
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error(callContext + " Error parsing", e);
+ }
+ }
+ }
}
diff --git a/src/main/java/org/jitsi/jigasi/JvbConference.java b/src/main/java/org/jitsi/jigasi/JvbConference.java
index fac25afe1..27113b6e9 100644
--- a/src/main/java/org/jitsi/jigasi/JvbConference.java
+++ b/src/main/java/org/jitsi/jigasi/JvbConference.java
@@ -648,6 +648,8 @@ public synchronized void registrationStateChanged(
&& mucRoom == null
&& evt.getNewState() == RegistrationState.REGISTERED)
{
+ this.getAudioModeration().xmppProviderRegistered();
+
// Join the MUC
joinConferenceRoom();
diff --git a/src/main/java/org/jitsi/jigasi/SipGatewaySession.java b/src/main/java/org/jitsi/jigasi/SipGatewaySession.java
index c8198eafc..d4986bd27 100644
--- a/src/main/java/org/jitsi/jigasi/SipGatewaySession.java
+++ b/src/main/java/org/jitsi/jigasi/SipGatewaySession.java
@@ -1134,7 +1134,7 @@ public void sendJson(CallPeer callPeer, JSONObject jsonObject)
* @param jsonObject JSONObject to be sent.
* @throws OperationFailedException failed sending the json.
*/
- private void sendJson(JSONObject jsonObject)
+ public void sendJson(JSONObject jsonObject)
throws OperationFailedException
{
this.sendJson(sipCall.getCallPeers().next(), jsonObject);
diff --git a/src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java b/src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java
index 344fe895e..01a30eb3e 100644
--- a/src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java
+++ b/src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java
@@ -61,6 +61,9 @@ public static class MESSAGE_TYPE
public static final int LOBBY_LEFT = 5;
public static final int LOBBY_ALLOWED_JOIN = 6;
public static final int LOBBY_REJECTED_JOIN = 7;
+ public static final int AV_MODERATION_ENABLED = 8;
+ public static final int AV_MODERATION_APPROVED = 9;
+ public static final int AV_MODERATION_DENIED = 10;
}
private static class MESSAGE_HEADER
@@ -247,4 +250,47 @@ public static JSONObject createSIPJSONAudioMuteRequest(boolean muted)
return createSIPJSON("muteRequest", muteSettingsJson, null);
}
+
+ /**
+ * Creates new JSONObject to notify that AV moderation is enabled/disabled.
+ *
+ * @return JSONObject representing a message to be sent over SIP.
+ */
+ public static JSONObject createAVModerationEnabledNotification(boolean value)
+ {
+ JSONObject obj = new JSONObject();
+
+ obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_ENABLED);
+ obj.put(MESSAGE_HEADER.MESSAGE_DATA, value);
+
+ return obj;
+ }
+
+ /**
+ * Creates new JSONObject to notify that AV moderation is enabled/disabled.
+ *
+ * @return JSONObject representing a message to be sent over SIP.
+ */
+ public static JSONObject createAVModerationApprovedNotification()
+ {
+ JSONObject obj = new JSONObject();
+
+ obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_APPROVED);
+
+ return obj;
+ }
+
+ /**
+ * Creates new JSONObject to notify that AV moderation is enabled/disabled.
+ *
+ * @return JSONObject representing a message to be sent over SIP.
+ */
+ public static JSONObject createAVModerationDeniedNotification()
+ {
+ JSONObject obj = new JSONObject();
+
+ obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_DENIED);
+
+ return obj;
+ }
}