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; + } }