From 0798301d205ce0375c81bfe2cc8b66eb5bceb626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Tue, 14 Apr 2020 08:34:36 -0500 Subject: [PATCH] Fix remote muting jigasi participants (#258) * fix: NPE when sip participants is leaving the call. SEVERE: [108354] org.jitsi.jigasi.util.Util.lambda$injectSoundFile$0().295 Error playing:sounds/ParticipantLeft.opus org.jitsi.service.neomedia.TransmissionFailedException: java.lang.NullPointerException at org.jitsi.impl.neomedia.MediaStreamImpl.injectPacket(MediaStreamImpl.java:3775) at org.jitsi.jigasi.SoundNotificationManager.injectSoundFileInStream(SoundNotificationManager.java:349) at org.jitsi.jigasi.SoundNotificationManager.lambda$injectSoundFile$0(SoundNotificationManager.java:290) at java.lang.Thread.run(Thread.java:748) Caused by: java.lang.NullPointerException at org.jitsi.impl.neomedia.MediaStreamImpl.injectPacket(MediaStreamImpl.java:3767) ... 3 more * fix: Uses private method to get connection. * fix: Muting jigasi participants and handling start muted. * fix: Lock when initializing providers. "org.jitsi.impl.osgi.framework.AsyncExecutor" #14 daemon prio=5 os_prio=31 tid=0x00007ffe34c30000 nid=0xa703 in Object.wait() [0x000070000913c000] java.lang.Thread.State: RUNNABLE at org.jivesoftware.smack.SmackConfiguration.getVersion(SmackConfiguration.java:96) at org.jivesoftware.smack.provider.ProviderManager.(ProviderManager.java:122) at org.jitsi.xmpp.extensions.jitsimeet.MediaPresenceExtension.registerExtensions(MediaPresenceExtension.java:59) at org.jitsi.jigasi.JigasiBundleActivator.serviceChanged(JigasiBundleActivator.java:284) at org.jitsi.impl.osgi.framework.launch.EventDispatcher$Command.run(EventDispatcher.java:128) at org.jitsi.impl.osgi.framework.AsyncExecutor.runInThread(AsyncExecutor.java:122) at org.jitsi.impl.osgi.framework.AsyncExecutor.access$000(AsyncExecutor.java:28) at org.jitsi.impl.osgi.framework.AsyncExecutor$1.run(AsyncExecutor.java:231) "org.jitsi.impl.osgi.framework.AsyncExecutor" #13 daemon prio=5 os_prio=31 tid=0x00007ffe339a4800 nid=0xa803 in Object.wait() [0x0000700009038000] java.lang.Thread.State: RUNNABLE at org.jivesoftware.smack.initializer.UrlInitializer.initialize(UrlInitializer.java:54) at org.jivesoftware.smack.SmackInitialization.loadSmackClass(SmackInitialization.java:237) at org.jivesoftware.smack.SmackInitialization.parseClassesToLoad(SmackInitialization.java:198) at org.jivesoftware.smack.SmackInitialization.processConfigFile(SmackInitialization.java:168) at org.jivesoftware.smack.SmackInitialization.processConfigFile(SmackInitialization.java:153) at org.jivesoftware.smack.SmackInitialization.(SmackInitialization.java:119) at org.jivesoftware.smack.SmackConfiguration.getVersion(SmackConfiguration.java:96) at org.jivesoftware.smack.AbstractXMPPConnection.(AbstractXMPPConnection.java:109) at org.jitsi.jigasi.xmpp.CallControlMucActivator.(CallControlMucActivator.java:83) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.lang.Class.newInstance(Class.java:442) at org.jitsi.impl.osgi.framework.BundleImpl.start(BundleImpl.java:305) at org.jitsi.impl.osgi.framework.launch.FrameworkImpl.startLevelChanged(FrameworkImpl.java:472) at org.jitsi.impl.osgi.framework.startlevel.FrameworkStartLevelImpl$Command.run(FrameworkStartLevelImpl.java:137) at org.jitsi.impl.osgi.framework.AsyncExecutor.runInThread(AsyncExecutor.java:122) at org.jitsi.impl.osgi.framework.AsyncExecutor.access$000(AsyncExecutor.java:28) at org.jitsi.impl.osgi.framework.AsyncExecutor$1.run(AsyncExecutor.java:231) "AccountManager.loadStoredAccounts" #23 daemon prio=5 os_prio=31 tid=0x00007ffe33a8d800 nid=0x29107 in Object.wait() [0x0000700009d6d000] java.lang.Thread.State: RUNNABLE at org.jitsi.xmpp.extensions.jingle.JingleIQProvider.(JingleIQProvider.java:44) at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderServiceJabberImpl.initialize(ProtocolProviderServiceJabberImpl.java:1800) - locked <0x00000006c002e0d8> (a java.lang.Object) at net.java.sip.communicator.impl.protocol.jabber.ProtocolProviderFactoryJabberImpl.createService(ProtocolProviderFactoryJabberImpl.java:173) at net.java.sip.communicator.service.protocol.ProtocolProviderFactory.loadAccount(ProtocolProviderFactory.java:1010) at net.java.sip.communicator.service.protocol.AccountManager.doLoadStoredAccounts(AccountManager.java:218) at net.java.sip.communicator.service.protocol.AccountManager.loadStoredAccounts(AccountManager.java:468) at net.java.sip.communicator.service.protocol.AccountManager.runInLoadStoredAccountsThread(AccountManager.java:585) at net.java.sip.communicator.service.protocol.AccountManager.access$100(AccountManager.java:37) at net.java.sip.communicator.service.protocol.AccountManager$2.run(AccountManager.java:510) * ref: Simplifies code. * fix: Sets JitsiMeetTools RequestListener when setting up sip call. And removes it when call is ended. * fix: Show correct presence status in meeting when remotely muted. * fix: Sending SIP Info messages only on established call. --- .../jitsi/jigasi/AbstractGatewaySession.java | 11 ++ .../jitsi/jigasi/JigasiBundleActivator.java | 29 ++-- .../java/org/jitsi/jigasi/JvbConference.java | 77 ++++++++- .../org/jitsi/jigasi/SipGatewaySession.java | 156 +++++++++++------- .../jigasi/SoundNotificationManager.java | 13 +- .../jigasi/TranscriptionGatewaySession.java | 5 + 6 files changed, 204 insertions(+), 87 deletions(-) diff --git a/src/main/java/org/jitsi/jigasi/AbstractGatewaySession.java b/src/main/java/org/jitsi/jigasi/AbstractGatewaySession.java index 89bc798e7..181f0e018 100644 --- a/src/main/java/org/jitsi/jigasi/AbstractGatewaySession.java +++ b/src/main/java/org/jitsi/jigasi/AbstractGatewaySession.java @@ -207,6 +207,12 @@ abstract void onJvbConferenceWillStop(JvbConference jvbConference, */ public void onJvbCallEnded() {} + /** + * Method called by JvbConference to notify JVB call has been + * established. + */ + public void onJvbCallEstablished() {} + /** * Cancels current session by leaving the muc room */ @@ -410,6 +416,11 @@ public String getFocusResourceAddr() return focusResourceAddr; } + /** + * If muting is supported will mute the participant. + */ + public abstract void mute(); + /** * Whether the gateway implementation supports call resuming. Where we can * keep the gateway session while the xmpp call is been disconnected or diff --git a/src/main/java/org/jitsi/jigasi/JigasiBundleActivator.java b/src/main/java/org/jitsi/jigasi/JigasiBundleActivator.java index ee097f6e3..354d06882 100644 --- a/src/main/java/org/jitsi/jigasi/JigasiBundleActivator.java +++ b/src/main/java/org/jitsi/jigasi/JigasiBundleActivator.java @@ -183,6 +183,15 @@ public void start(final BundleContext bundleContext) if(isSipEnabled()) { + MuteIqProvider.registerMuteIqProvider(); + + // recording status, to detect recording start/stop + ProviderManager.addExtensionProvider( + RecordingStatus.ELEMENT_NAME, + RecordingStatus.NAMESPACE, + new DefaultPacketExtensionProvider<>(RecordingStatus.class) + ); + logger.info("initialized SipGateway"); sipGateway = new SipGateway(bundleContext) { @@ -222,9 +231,14 @@ void notifyCallEnded(CallContext callContext) else { logger.info("skipped initialization of TranscriptionGateway"); - } + // Register Jitsi Meet media presence extension. + MediaPresenceExtension.registerExtensions(); + + // Register Rayo IQs + new RayoIqProvider().registerRayoIQs(); + bundleContext.addServiceListener(this); Collection> refs @@ -273,19 +287,6 @@ public void serviceChanged(ServiceEvent serviceEvent) return; } - // Register Jitsi Meet media presence extension. - MediaPresenceExtension.registerExtensions(); - - // Register Rayo IQs - new RayoIqProvider().registerRayoIQs(); - - // recording status, to detect recording start/stop - ProviderManager.addExtensionProvider( - RecordingStatus.ELEMENT_NAME, - RecordingStatus.NAMESPACE, - new DefaultPacketExtensionProvider<>(RecordingStatus.class) - ); - ProtocolProviderService pps = (ProtocolProviderService) service; if (sipGateway != null && sipGateway.getSipProvider() == null && ProtocolNames.SIP.equals(pps.getProtocolName())) diff --git a/src/main/java/org/jitsi/jigasi/JvbConference.java b/src/main/java/org/jitsi/jigasi/JvbConference.java index f9ff5617d..12411e853 100644 --- a/src/main/java/org/jitsi/jigasi/JvbConference.java +++ b/src/main/java/org/jitsi/jigasi/JvbConference.java @@ -36,6 +36,7 @@ import org.jitsi.xmpp.extensions.rayo.*; import org.jivesoftware.smack.*; import org.jivesoftware.smack.bosh.*; +import org.jivesoftware.smack.iqrequest.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smackx.nick.packet.*; import org.jxmpp.jid.*; @@ -582,14 +583,12 @@ public synchronized void registrationStateChanged( // Join the MUC joinConferenceRoom(); + XMPPConnection connection = getConnection(); if (xmppProvider != null - && xmppProvider instanceof ProtocolProviderServiceJabberImpl - && ((ProtocolProviderServiceJabberImpl) xmppProvider) - .getConnection() instanceof XMPPBOSHConnection) + && connection != null + && connection instanceof XMPPBOSHConnection) { - Object sessionId = Util.getConnSessionId( - ((ProtocolProviderServiceJabberImpl) xmppProvider) - .getConnection()); + Object sessionId = Util.getConnSessionId(connection); if (sessionId != null) { logger.error(this.callContext + " Registered bosh sid: " @@ -643,6 +642,15 @@ public boolean isInTheRoom() return mucRoom != null && mucRoom.isJoined(); } + /** + * Indicates whether this conference has been started. + * @return true is this conference is started, false otherwise. + */ + public boolean isStarted() + { + return started; + } + private void joinConferenceRoom() { // Advertise gateway feature before joining @@ -747,6 +755,11 @@ private void joinConferenceRoom() "is not an instance of ChatRoomJabberImpl"); } + if (JigasiBundleActivator.isSipStartMutedEnabled()) + { + getConnection().registerIQRequestHandler(new MuteIqHandler()); + } + // we invite focus and wait for its response // to be sure that if it is not in the room, the focus will be the // first to join, mimic the web behaviour @@ -1158,6 +1171,7 @@ public synchronized void callStateChanged(CallChangeEvent evt) if (jvbCall.getCallState() == CallState.CALL_IN_PROGRESS) { logger.info(callContext + " JVB conference call IN_PROGRESS."); + gatewaySession.onJvbCallEstablished(); } else if(jvbCall.getCallState() == CallState.CALL_ENDED) { @@ -1407,8 +1421,7 @@ private void inviteFocus(final EntityBareJid roomIdentifier) StanzaCollector collector = null; try { - collector = ((ProtocolProviderServiceJabberImpl) xmppProvider) - .getConnection() + collector = getConnection() .createStanzaCollectorAndSend(focusInviteIQ); collector.nextResultOrThrow(); } @@ -1687,4 +1700,52 @@ private XMPPConnection getConnection() return null; } + + /** + * Handles mute requests received by jicofo if enabled. + */ + private class MuteIqHandler + extends AbstractIqRequestHandler + { + MuteIqHandler() + { + super( + MuteIq.ELEMENT_NAME, + MuteIq.NAMESPACE, + IQ.Type.set, + Mode.sync); + } + + @Override + public IQ handleIQRequest(IQ iqRequest) + { + return handleMuteIq((MuteIq) iqRequest); + } + + /** + * Handles the incoming mute request only if it is from the focus. + * @param muteIq the incoming iq. + * @return the result iq. + */ + private IQ handleMuteIq(MuteIq muteIq) + { + Boolean doMute = muteIq.getMute(); + Jid from = muteIq.getFrom(); + + if (doMute == null + || !from.getResourceOrEmpty().equals( + gatewaySession.getFocusResourceAddr())) + { + return IQ.createErrorResponse(muteIq, XMPPError.getBuilder( + XMPPError.Condition.item_not_found)); + } + + if (doMute) + { + gatewaySession.mute(); + } + + return IQ.createResultIQ(muteIq); + } + } } diff --git a/src/main/java/org/jitsi/jigasi/SipGatewaySession.java b/src/main/java/org/jitsi/jigasi/SipGatewaySession.java index d6dc01ada..35abfc072 100644 --- a/src/main/java/org/jitsi/jigasi/SipGatewaySession.java +++ b/src/main/java/org/jitsi/jigasi/SipGatewaySession.java @@ -22,7 +22,6 @@ import net.java.sip.communicator.service.protocol.media.*; import net.java.sip.communicator.util.Logger; import org.jitsi.impl.neomedia.*; -import org.jitsi.jigasi.JigasiBundleActivator; import org.jitsi.jigasi.stats.*; import org.jitsi.jigasi.util.*; import org.jitsi.service.neomedia.*; @@ -594,6 +593,8 @@ private void sipCallEnded() statsHandler = null; } + jitsiMeetTools.removeRequestListener(SipGatewaySession.this); + if (peerStateListener != null) peerStateListener.unregister(); @@ -644,7 +645,10 @@ public void onJoinJitsiMeetRequest( @Override public void onSessionStartMuted(boolean[] startMutedFlags) { - this.startAudioMuted = startMutedFlags[0]; + if (isMutingSupported()) + { + this.startAudioMuted = startMutedFlags[0]; + } } /** @@ -661,7 +665,6 @@ public void onJSONReceived(CallPeer callPeer, { try { - if (callPeer.getCall() != this.sipCall) { if (logger.isTraceEnabled()) @@ -671,13 +674,13 @@ public void onJSONReceived(CallPeer callPeer, return; } - if (jsonObject.containsKey("type") == false) + if (!jsonObject.containsKey("type")) { logger.error("Unknown json object type!"); return; } - if (jsonObject.containsKey("id") == false) + if (!jsonObject.containsKey("id")) { logger.error("Unknown json object id!"); return; @@ -686,16 +689,15 @@ public void onJSONReceived(CallPeer callPeer, String id = (String)jsonObject.get("id"); String type = (String)jsonObject.get("type"); - if (type.equalsIgnoreCase("muteResponse") == true) + if (type.equalsIgnoreCase("muteResponse")) { - if (jsonObject.containsKey("status") == false) + if (!jsonObject.containsKey("status")) { logger.error("muteResponse without status!"); return; } - if ( ((String)jsonObject.get("status")) - .equalsIgnoreCase("OK") == true) + if (((String) jsonObject.get("status")).equalsIgnoreCase("OK")) { JSONObject data = (JSONObject) jsonObject.get("data"); @@ -705,14 +707,14 @@ public void onJSONReceived(CallPeer callPeer, this.jvbConference.setChatRoomAudioMuted(bMute); } } - else if (type.equalsIgnoreCase("muteRequest") == true) + else if (type.equalsIgnoreCase("muteRequest")) { JSONObject data = (JSONObject) jsonObject.get("data"); boolean bAudioMute = (boolean)data.get("audio"); // Send request to jicofo - if (jvbConference.requestAudioMute(bAudioMute) == true) + if (jvbConference.requestAudioMute(bAudioMute)) { // Send response through sip respondRemoteAudioMute(bAudioMute, @@ -765,8 +767,8 @@ private JSONObject createSIPJSONAudioMuteRequest(boolean bMuted) { JSONObject muteSettingsJson = new JSONObject(); muteSettingsJson.put("audio", bMuted); - JSONObject muteRequestJson = createSIPJSON("muteRequest", muteSettingsJson, null); - return muteRequestJson; + + return createSIPJSON("muteRequest", muteSettingsJson, null); } /** @@ -783,8 +785,9 @@ private JSONObject createSIPJSONAudioMuteResponse(boolean bMuted, { JSONObject muteSettingsJson = new JSONObject(); muteSettingsJson.put("audio", bMuted); - JSONObject muteResponseJson = createSIPJSON("muteResponse", muteSettingsJson, id); - muteResponseJson.put("status", bSucceeded == true ? "OK" : "FAILED"); + JSONObject muteResponseJson + = createSIPJSON("muteResponse", muteSettingsJson, id); + muteResponseJson.put("status", bSucceeded ? "OK" : "FAILED"); return muteResponseJson; } @@ -795,9 +798,8 @@ private JSONObject createSIPJSONAudioMuteResponse(boolean bMuted, * @param callPeer CallPeer to send JSON to. * @throws OperationFailedException */ - private void requestRemoteAudioMute(boolean bMuted, - CallPeer callPeer) - throws OperationFailedException + private void requestRemoteAudioMute(boolean bMuted, CallPeer callPeer) + throws OperationFailedException { // Mute audio JSONObject muteRequestJson = createSIPJSONAudioMuteRequest(bMuted); @@ -822,7 +824,7 @@ private void respondRemoteAudioMute(boolean bMuted, boolean bSucceeded, CallPeer callPeer, String id) - throws OperationFailedException + throws OperationFailedException { JSONObject muteResponseJson = createSIPJSONAudioMuteResponse(bMuted, bSucceeded, id); @@ -830,9 +832,9 @@ private void respondRemoteAudioMute(boolean bMuted, jitsiMeetTools.sendJSON(callPeer, muteResponseJson, new HashMap() {{ - put("VIA", (Object)("SIP.INFO")); + put("VIA", "SIP.INFO"); }}); - } + } /** * Initializes the sip call listeners. @@ -851,6 +853,7 @@ private void initSipCall(String sipCallIdentifier) DEFAULT_STATS_REMOTE_ID + "-" + sipCallIdentifier); } sipCall.addCallChangeListener(statsHandler); + jitsiMeetTools.addRequestListener(this); if (mediaDroppedThresholdMs != -1) { @@ -931,8 +934,6 @@ private void waitForRoomName() waitThread = new WaitForJvbRoomNameThread(); - jitsiMeetTools.addRequestListener(this); - waitThread.start(); } @@ -1195,6 +1196,76 @@ public boolean hasCallResumeSupport() return true; } + /** + * When + */ + @Override + public void onJvbCallEstablished() + { + maybeProcessStartMuted(); + } + + /** + * Processes start muted in case: + * - we had received that flag + * - start muted is enabled through the flag + * - jvb call is in progress as we will be muting the channels + * - sip call is in progress we will be sending SIP Info messages + */ + private void maybeProcessStartMuted() + { + if (this.startAudioMuted + && isMutingSupported() + && jvbConferenceCall != null + && jvbConferenceCall.getCallState() == CallState.CALL_IN_PROGRESS + && sipCall != null + && sipCall.getCallState() == CallState.CALL_IN_PROGRESS) + { + if (jvbConference.requestAudioMute(startAudioMuted)) + { + mute(); + } + + // in case we reconnect start muted maybe no-longer set + this.startAudioMuted = false; + } + } + + /** + * Sends mute request to be remotely muted. + * This is a SIP Info message to the IVR so the user will be notified of it + * When we receive confirmation for the announcement we will update + * our presence status in the conference. + */ + public void mute() + { + if (!isMutingSupported()) + return; + + // Notify peer + CallPeer callPeer = sipCall.getCallPeers().next(); + + try + { + logger.info( + SipGatewaySession.this.callContext + " Sending mute request "); + requestRemoteAudioMute(true, callPeer); + } + catch (Exception ex) + { + logger.error(ex.getMessage()); + } + } + + /** + * Muting is supported when it is enabled by configuration. + * @return true if mute support is enabled. + */ + public boolean isMutingSupported() + { + return JigasiBundleActivator.isSipStartMutedEnabled(); + } + /** * PeriodicRunnable that will check incoming RTP and if needed to hangup. */ @@ -1312,6 +1383,8 @@ void handleCallState(Call call, CallPeerChangeEvent cause) logger.info(SipGatewaySession.this.callContext + " SIP call format used: " + Util.getFirstPeerMediaFormat(call)); + + maybeProcessStartMuted(); } else if(call.getCallState() == CallState.CALL_ENDED) { @@ -1386,39 +1459,6 @@ public void peerStateChanged(final CallPeerChangeEvent evt) if (jvbConference != null) jvbConference.setPresenceStatus(stateString); - if (JigasiBundleActivator.isSipStartMutedEnabled() == true) - { - if (CallPeerState.CONNECTED.equals(callPeerState) == true) - { - // After CallPeer is in CONNECTED state handle startmuted flags - jitsiMeetTools.addRequestListener(SipGatewaySession.this); - - if (SipGatewaySession.this.startAudioMuted == true) - { - // Send request to jicofo - if (jvbConference.requestAudioMute(startAudioMuted) == true) - { - // Notify peer - CallPeer callPeer = evt.getSourceCallPeer(); - - try - { - requestRemoteAudioMute(startAudioMuted, callPeer); - } - catch (Exception ex) - { - logger.error(ex.getMessage()); - } - } - } - } - - if (CallPeerState.DISCONNECTED.equals(callPeerState) == true) - { - jitsiMeetTools.removeRequestListener(SipGatewaySession.this); - } - } - soundNotificationManager.process(callPeerState); } @@ -1487,10 +1527,6 @@ public void run() { Thread.currentThread().interrupt(); } - finally - { - jitsiMeetTools.removeRequestListener(SipGatewaySession.this); - } } } diff --git a/src/main/java/org/jitsi/jigasi/SoundNotificationManager.java b/src/main/java/org/jitsi/jigasi/SoundNotificationManager.java index 21ebfa0da..6b04b4333 100644 --- a/src/main/java/org/jitsi/jigasi/SoundNotificationManager.java +++ b/src/main/java/org/jitsi/jigasi/SoundNotificationManager.java @@ -531,7 +531,12 @@ public void notifyChatRoomMemberJoined(ChatRoomMember member) */ public void notifyChatRoomMemberLeft(ChatRoomMember member) { - playParticipantLeftNotification(); + // if this is the sip hanging up (stopping) skip playing + if (gatewaySession.jvbConference.isStarted() + && gatewaySession.getSipCall() != null) + { + playParticipantLeftNotification(); + } } /** @@ -603,7 +608,7 @@ private void playParticipantJoinedNotification() if (getParticipantJoinedRateLimiter().on() == false) { Call sipCall = gatewaySession.getSipCall(); - + if (sipCall != null) { injectSoundFile(sipCall, PARTICIPANT_JOINED); @@ -704,7 +709,6 @@ private class SoundRateLimiter implements RateLimiter /** * SoundRateLimiter constructor. * - * @param timePoint Initial start timepoint. * @param maxTimeout Timeout in milliseconds to block notification. */ SoundRateLimiter(long maxTimeout) @@ -720,8 +724,7 @@ private class SoundRateLimiter implements RateLimiter */ public boolean on() { - if (this.startTimePoint - .compareAndSet(null, Instant.now()) == true) + if (this.startTimePoint.compareAndSet(null, Instant.now())) { return false; } diff --git a/src/main/java/org/jitsi/jigasi/TranscriptionGatewaySession.java b/src/main/java/org/jitsi/jigasi/TranscriptionGatewaySession.java index 085d76c13..c13a6c8f5 100644 --- a/src/main/java/org/jitsi/jigasi/TranscriptionGatewaySession.java +++ b/src/main/java/org/jitsi/jigasi/TranscriptionGatewaySession.java @@ -697,4 +697,9 @@ public boolean hasCallResumeSupport() { return false; } + + /** + * {@inheritDoc} + */ + public void mute(){} }