2222import net .java .sip .communicator .util .*;
2323import org .jitsi .jigasi .sip .*;
2424import org .jitsi .jigasi .util .*;
25+ import org .jitsi .xmpp .extensions .*;
2526import org .jitsi .xmpp .extensions .jitsimeet .*;
2627import org .jivesoftware .smack .*;
28+ import org .jivesoftware .smack .filter .*;
2729import org .jivesoftware .smack .iqrequest .*;
2830import org .jivesoftware .smack .packet .*;
31+ import org .jivesoftware .smackx .disco .*;
32+ import org .jivesoftware .smackx .disco .packet .*;
33+ import org .json .simple .parser .*;
2934import org .jxmpp .jid .*;
3035
3136import org .json .simple .*;
@@ -78,18 +83,45 @@ public class AudioModeration
7883 {
7984 initialAudioMutedExtension .setAudioMuted (false );
8085 }
81-
8286 /**
8387 * The call context used to create this conference, contains info as
8488 * room name and room password and other optional parameters.
8589 */
8690 private final CallContext callContext ;
8791
92+ /**
93+ * We keep track of the AV moderation component to be able to trust the incoming messages.
94+ */
95+ private String avModerationAddress = null ;
96+
97+ /**
98+ * Whether AV moderation is currently enabled.
99+ */
100+ private boolean avModerationEnabled = false ;
101+
102+ /**
103+ * Whether current jigasi provider is allowed to unmute itself. By default, AV moderation is not enabled,
104+ * and we are allowed to unmute.
105+ */
106+ private boolean isAllowedToUnmute = true ;
107+
108+ /**
109+ * We use the same instance of extension so we can remove and add it from the default presence set of
110+ * extensions.
111+ */
112+ private static final RaiseHandExtension lowerHandExtension = new RaiseHandExtension ();
113+
114+ /**
115+ * The listener for incoming messages with AV moderation commands.
116+ */
117+ private final AVModerationListener avModerationListener ;
118+
88119 public AudioModeration (JvbConference jvbConference , SipGatewaySession gatewaySession , CallContext ctx )
89120 {
90121 this .gatewaySession = gatewaySession ;
91122 this .jvbConference = jvbConference ;
92123 this .callContext = ctx ;
124+ this .avModerationListener = new AVModerationListener ();
93125 }
94126
95127 /**
@@ -115,16 +147,21 @@ public void clean()
115147 {
116148 XMPPConnection connection = jvbConference .getConnection ();
117149
150+ if (connection == null )
151+ {
152+ // if there is no connection nothing to clear
153+ return ;
154+ }
155+
118156 if (muteIqHandler != null )
119157 {
120158 // we need to remove it from the connection, or we break some Smack
121159 // weak references map where the key is connection and the value
122160 // holds a connection and we leak connection/conferences.
123- if (connection != null )
124- {
125- connection .unregisterIQRequestHandler (muteIqHandler );
126- }
161+ connection .unregisterIQRequestHandler (muteIqHandler );
127162 }
163+
164+ connection .removeAsyncStanzaListener (this .avModerationListener );
128165 }
129166
130167 /**
@@ -275,6 +312,12 @@ void setChatRoomAudioMuted(boolean muted)
275312 {
276313 // remove the initial extension otherwise it will overwrite our new setting
277314 ((ChatRoomJabberImpl ) mucRoom ).removePresencePacketExtensions (initialAudioMutedExtension );
315+
316+ if (!muted )
317+ {
318+ // if we are unmuting make sure our raise hand is always lowered
319+ ((ChatRoomJabberImpl ) mucRoom ).addPresencePacketExtensions (lowerHandExtension );
320+ }
278321 }
279322
280323 AudioMutedExtension audioMutedExtension = new AudioMutedExtension ();
@@ -300,6 +343,23 @@ public boolean requestAudioMuteByJicofo(boolean bMuted)
300343 {
301344 ChatRoom mucRoom = this .jvbConference .getJvbRoom ();
302345
346+ if (!bMuted && this .avModerationEnabled && !isAllowedToUnmute )
347+ {
348+ OperationSetJitsiMeetTools jitsiMeetTools
349+ = this .jvbConference .getXmppProvider ().getOperationSet (OperationSetJitsiMeetTools .class );
350+
351+ if (mucRoom instanceof ChatRoomJabberImpl )
352+ {
353+ // remove the default value which is lowering the hand
354+ ((ChatRoomJabberImpl ) mucRoom ).removePresencePacketExtensions (lowerHandExtension );
355+ }
356+
357+ // let's raise hand
358+ jitsiMeetTools .sendPresenceExtension (mucRoom , new RaiseHandExtension ().setRaisedHandValue (true ));
359+
360+ return false ;
361+ }
362+
303363 StanzaCollector collector = null ;
304364 try
305365 {
@@ -356,7 +416,8 @@ && isMutingSupported()
356416 && sipCall != null
357417 && sipCall .getCallState () == CallState .CALL_IN_PROGRESS )
358418 {
359- if (this .requestAudioMuteByJicofo (startAudioMuted ))
419+ // we do not want to process start muted if AV moderation is enabled, as we are already muted
420+ if (!this .avModerationEnabled && this .requestAudioMuteByJicofo (true ))
360421 {
361422 mute ();
362423 }
@@ -382,7 +443,9 @@ public void mute()
382443
383444 try
384445 {
385- logger .info (this .callContext + " Sending mute request " );
446+ logger .info (this .callContext + " Sending mute request avModeration:" + this .avModerationEnabled
447+ + " allowed to unmute:" + this .isAllowedToUnmute );
448+
386449 this .gatewaySession .sendJson (callPeer , SipInfoJsonProtocol .createSIPJSONAudioMuteRequest (true ));
387450 }
388451 catch (Exception ex )
@@ -391,6 +454,56 @@ public void mute()
391454 }
392455 }
393456
457+ /**
458+ * The xmpp provider for JvbConference has registered after connecting.
459+ */
460+ public void xmppProviderRegistered ()
461+ {
462+ // we are here in the RegisterThread, and it is safe to query and wait
463+ // Uses disco info to discover the AV moderation address.
464+ if (this .callContext .getDomain () != null )
465+ {
466+ try
467+ {
468+ long startQuery = System .currentTimeMillis ();
469+ DiscoverInfo info = ServiceDiscoveryManager .getInstanceFor (this .jvbConference .getConnection ())
470+ .discoverInfo (JidCreate .domainBareFrom (this .callContext .getDomain ()));
471+
472+ DiscoverInfo .Identity avIdentity =
473+ info .getIdentities ().stream ().
474+ filter (di -> di .getCategory ().equals ("component" ) && di .getType ().equals ("av_moderation" ))
475+ .findFirst ().orElse (null );
476+
477+ if (avIdentity != null )
478+ {
479+ this .avModerationAddress = avIdentity .getName ();
480+ logger .info (String .format ("%s Discovered %s for %oms." ,
481+ this .callContext , this .avModerationAddress , System .currentTimeMillis () - startQuery ));
482+ }
483+ }
484+ catch (Exception e )
485+ {
486+ logger .error ("Error querying for av moderation address" , e );
487+ }
488+ }
489+
490+ if (this .avModerationAddress != null )
491+ {
492+ try
493+ {
494+ this .jvbConference .getConnection ().addAsyncStanzaListener (
495+ this .avModerationListener ,
496+ new AndFilter (
497+ MessageTypeFilter .NORMAL ,
498+ FromMatchesFilter .create (JidCreate .domainBareFrom (this .avModerationAddress ))));
499+ }
500+ catch (Exception e )
501+ {
502+ logger .error ("Error adding AV moderation listener" , e );
503+ }
504+ }
505+ }
506+
394507 /**
395508 * Handles mute requests received by jicofo if enabled.
396509 */
@@ -442,4 +555,111 @@ private IQ handleMuteIq(MuteIq muteIq)
442555 return IQ .createResultIQ (muteIq );
443556 }
444557 }
558+
559+ /**
560+ * Added to presence to raise hand.
561+ */
562+ private static class RaiseHandExtension
563+ extends AbstractPacketExtension
564+ {
565+ /**
566+ * The namespace of this packet extension.
567+ */
568+ public static final String NAMESPACE = "jabber:client" ;
569+
570+ /**
571+ * XML element name of this packet extension.
572+ */
573+ public static final String ELEMENT_NAME = "jitsi_participant_raisedHand" ;
574+
575+ /**
576+ * Creates a {@link org.jitsi.xmpp.extensions.jitsimeet.TranslationLanguageExtension} instance.
577+ */
578+ public RaiseHandExtension ()
579+ {
580+ super (NAMESPACE , ELEMENT_NAME );
581+ }
582+
583+ /**
584+ * Sets user's audio muted status.
585+ *
586+ * @param value <tt>true</tt> or <tt>false</tt> which indicates audio
587+ * muted status of the user.
588+ */
589+ public ExtensionElement setRaisedHandValue (Boolean value )
590+ {
591+ setText (value ? value .toString () : null );
592+
593+ return this ;
594+ }
595+ }
596+
597+ /**
598+ * Listens for incoming messages with AV moderation commands.
599+ */
600+ private class AVModerationListener
601+ implements StanzaListener
602+ {
603+ @ Override
604+ public void processStanza (Stanza packet )
605+ {
606+ JsonMessageExtension jsonMsg = packet .getExtension (
607+ JsonMessageExtension .ELEMENT_NAME , JsonMessageExtension .NAMESPACE );
608+
609+ if (jsonMsg == null )
610+ {
611+ return ;
612+ }
613+
614+ try
615+ {
616+ Object o = new JSONParser ().parse (jsonMsg .getJson ());
617+
618+ if (o instanceof JSONObject )
619+ {
620+ JSONObject data = (JSONObject ) o ;
621+
622+ if (data .get ("type" ).equals ("av_moderation" ))
623+ {
624+ Object enabledObj = data .get ("enabled" );
625+ Object approvedObj = data .get ("approved" );
626+ Object removedObj = data .get ("removed" );
627+ Object mediaTypeObj = data .get ("mediaType" );
628+
629+ // we are interested only in audio moderation
630+ if (mediaTypeObj == null || !mediaTypeObj .equals ("audio" ))
631+ {
632+ return ;
633+ }
634+
635+ if (enabledObj != null )
636+ {
637+ avModerationEnabled = (Boolean ) enabledObj ;
638+ logger .info (callContext + " AV moderation has been enabled:" + avModerationEnabled );
639+
640+ // we will receive separate message when we are allowed to unmute
641+ isAllowedToUnmute = false ;
642+
643+ gatewaySession .sendJson (
644+ SipInfoJsonProtocol .createAVModerationEnabledNotification (avModerationEnabled ));
645+ }
646+ else if (removedObj != null && (Boolean ) removedObj )
647+ {
648+ isAllowedToUnmute = false ;
649+ gatewaySession .sendJson (SipInfoJsonProtocol .createAVModerationDeniedNotification ());
650+ }
651+ else if (approvedObj != null && (Boolean ) approvedObj )
652+ {
653+ isAllowedToUnmute = true ;
654+ gatewaySession .sendJson (SipInfoJsonProtocol .createAVModerationApprovedNotification ());
655+ }
656+ }
657+ }
658+ }
659+ catch (Exception e )
660+ {
661+ logger .error (callContext + " Error parsing" , e );
662+ }
663+ }
664+ }
445665}
0 commit comments