Эх сурвалжийг харах

Add listener for offer messages

Unlike the WebRtcMessageListener, which is bound to a specific peer
connection, an OfferMessageListener listens to all offer messages, no
matter which peer connection they are bound to. This can be used, for
example, to create a new peer connection when a remote offer for which
there is no previous connection is received.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Daniel Calviño Sánchez 2 жил өмнө
parent
commit
d42fe61e89

+ 11 - 5
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -266,6 +266,13 @@ public class CallActivity extends CallBaseActivity {
 
     private CallActivitySignalingMessageReceiver signalingMessageReceiver = new CallActivitySignalingMessageReceiver();
 
+    private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() {
+        @Override
+        public void onOffer(String sessionId, String roomType, String sdp, String nick) {
+            getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, roomType, false);
+        }
+    };
+
     private ExternalSignalingServer externalSignalingServer;
     private MagicWebSocketInstance webSocketClient;
     private WebSocketConnectionHelper webSocketConnectionHelper;
@@ -522,6 +529,8 @@ public class CallActivity extends CallBaseActivity {
         sdpConstraints.optional.add(new MediaConstraints.KeyValuePair("internalSctpDataChannels", "true"));
         sdpConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
 
+        signalingMessageReceiver.addListener(offerMessageListener);
+
         if (!isVoiceOnlyCall) {
             cameraInitialization();
         }
@@ -1206,6 +1215,8 @@ public class CallActivity extends CallBaseActivity {
 
     @Override
     public void onDestroy() {
+        signalingMessageReceiver.removeListener(offerMessageListener);
+
         if (localStream != null) {
             localStream.dispose();
             localStream = null;
@@ -1672,11 +1683,6 @@ public class CallActivity extends CallBaseActivity {
                 return;
             }
 
-            if ("offer".equals(type)) {
-                getOrCreatePeerConnectionWrapperForSessionIdAndType(ncSignalingMessage.getFrom(),
-                                                                    ncSignalingMessage.getRoomType(), false);
-            }
-
             signalingMessageReceiver.process(ncSignalingMessage);
         } else {
             Log.e(TAG, "unexpected RoomType while processing NCSignalingMessage");

+ 53 - 0
app/src/main/java/com/nextcloud/talk/signaling/OfferMessageNotifier.java

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.signaling;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Helper class to register and notify OfferMessageListeners.
+ *
+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
+ * a SignalingMessageReceiver rather than against an OfferMessageNotifier.
+ */
+class OfferMessageNotifier {
+
+    private final Set<SignalingMessageReceiver.OfferMessageListener> offerMessageListeners = new LinkedHashSet<>();
+
+    public synchronized void addListener(SignalingMessageReceiver.OfferMessageListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("OfferMessageListener can not be null");
+        }
+
+        offerMessageListeners.add(listener);
+    }
+
+    public synchronized void removeListener(SignalingMessageReceiver.OfferMessageListener listener) {
+        offerMessageListeners.remove(listener);
+    }
+
+    public synchronized void notifyOffer(String sessionId, String roomType, String sdp, String nick) {
+        for (SignalingMessageReceiver.OfferMessageListener listener : new ArrayList<>(offerMessageListeners)) {
+            listener.onOffer(sessionId, roomType, sdp, nick);
+        }
+    }
+}

+ 43 - 0
app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java

@@ -26,6 +26,14 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
 /**
  * Hub to register listeners for signaling messages of different kinds.
  *
+ * In general, if a listener is added while an event is being handled the new listener will not receive that event.
+ * An exception to that is adding a WebRtcMessageListener when handling an offer in an OfferMessageListener; in that
+ * case the "onOffer()" method of the WebRtcMessageListener will be called for that same offer.
+ *
+ * Similarly, if a listener is removed while an event is being handled the removed listener will still receive that
+ * event. Again the exception is removing a WebRtcMessageListener when handling an offer in an OfferMessageListener; in
+ * that case the "onOffer()" method of the WebRtcMessageListener will not be called for that offer.
+ *
  * Adding and removing listeners, as well as notifying them is internally synchronized. This should be kept in mind
  * if listeners are added or removed when handling an event to prevent deadlocks (nevertheless, just adding or
  * removing a listener in the same thread handling the event is fine, and in most cases it will be fine too if done
@@ -36,6 +44,19 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
  */
 public abstract class SignalingMessageReceiver {
 
+    /**
+     * Listener for WebRTC offers.
+     *
+     * Unlike the WebRtcMessageListener, which is bound to a specific peer connection, an OfferMessageListener listens
+     * to all offer messages, no matter which peer connection they are bound to. This can be used, for example, to
+     * create a new peer connection when a remote offer for which there is no previous connection is received.
+     *
+     * When an offer is received all OfferMessageListeners are notified before any WebRtcMessageListener is notified.
+     */
+    public interface OfferMessageListener {
+        void onOffer(String sessionId, String roomType, String sdp, String nick);
+    }
+
     /**
      * Listener for WebRTC messages.
      *
@@ -49,8 +70,25 @@ public abstract class SignalingMessageReceiver {
         void onEndOfCandidates();
     }
 
+    private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
+
     private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier();
 
+    /**
+     * Adds a listener for all offer messages.
+     *
+     * A listener is expected to be added only once. If the same listener is added again it will be notified just once.
+     *
+     * @param listener the OfferMessageListener
+     */
+    public void addListener(OfferMessageListener listener) {
+        offerMessageNotifier.addListener(listener);
+    }
+
+    public void removeListener(OfferMessageListener listener) {
+        offerMessageNotifier.removeListener(listener);
+    }
+
     /**
      * Adds a listener for WebRTC messages from the given session ID and room type.
      *
@@ -126,6 +164,11 @@ public abstract class SignalingMessageReceiver {
             String sdp = payload.getSdp();
             String nick = payload.getNick();
 
+            // If "processSignalingMessage" is called with two offers from two different threads it is possible,
+            // although extremely unlikely, that the WebRtcMessageListeners for the second offer are notified before the
+            // WebRtcMessageListeners for the first offer. This should not be a problem, though, so for simplicity
+            // the statements are not synchronized.
+            offerMessageNotifier.notifyOffer(sessionId, roomType, sdp, nick);
             webRtcMessageNotifier.notifyOffer(sessionId, roomType, sdp, nick);
 
             return;

+ 231 - 0
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java

@@ -0,0 +1,231 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.signaling;
+
+import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
+import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class SignalingMessageReceiverOfferTest {
+
+    private SignalingMessageReceiver signalingMessageReceiver;
+
+    @Before
+    public void setUp() {
+        // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate
+        // protected methods.
+        signalingMessageReceiver = new SignalingMessageReceiver() {
+        };
+    }
+
+    @Test
+    public void testAddOfferMessageListenerWithNullListener() {
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            signalingMessageReceiver.addListener(null);
+        });
+    }
+
+    @Test
+    public void testOfferMessage() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", null);
+    }
+
+    @Test
+    public void testOfferMessageWithNick() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+    }
+
+    @Test
+    public void testOfferMessageAfterRemovingListener() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+        signalingMessageReceiver.removeListener(mockedOfferMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verifyNoInteractions(mockedOfferMessageListener);
+    }
+
+    @Test
+    public void testOfferMessageAfterRemovingSingleListenerOfSeveral() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener3 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener1);
+        signalingMessageReceiver.addListener(mockedOfferMessageListener2);
+        signalingMessageReceiver.addListener(mockedOfferMessageListener3);
+        signalingMessageReceiver.removeListener(mockedOfferMessageListener2);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener1, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        verify(mockedOfferMessageListener3, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        verifyNoInteractions(mockedOfferMessageListener2);
+    }
+
+    @Test
+    public void testOfferMessageAfterAddingListenerAgain() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+    }
+
+    @Test
+    public void testAddOfferMessageListenerWhenHandlingOffer() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.addListener(mockedOfferMessageListener2);
+            return null;
+        }).when(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener1);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener1, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        verifyNoInteractions(mockedOfferMessageListener2);
+    }
+
+    @Test
+    public void testRemoveOfferMessageListenerWhenHandlingOffer() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.removeListener(mockedOfferMessageListener2);
+            return null;
+        }).when(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener1);
+        signalingMessageReceiver.addListener(mockedOfferMessageListener2);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        InOrder inOrder = inOrder(mockedOfferMessageListener1, mockedOfferMessageListener2);
+
+        inOrder.verify(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        inOrder.verify(mockedOfferMessageListener2).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+    }
+}

+ 135 - 0
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverTest.java

@@ -0,0 +1,135 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.signaling;
+
+import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
+import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class SignalingMessageReceiverTest {
+
+    private SignalingMessageReceiver signalingMessageReceiver;
+
+    @Before
+    public void setUp() {
+        // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate
+        // protected methods.
+        signalingMessageReceiver = new SignalingMessageReceiver() {
+        };
+    }
+
+    @Test
+    public void testOfferWithOfferAndWebRtcMessageListeners() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
+            mock(SignalingMessageReceiver.WebRtcMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+        signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        InOrder inOrder = inOrder(mockedOfferMessageListener, mockedWebRtcMessageListener);
+
+        inOrder.verify(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        inOrder.verify(mockedWebRtcMessageListener).onOffer("theSdp", "theNick");
+    }
+
+    @Test
+    public void testAddWebRtcMessageListenerWhenHandlingOffer() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
+            mock(SignalingMessageReceiver.WebRtcMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
+            return null;
+        }).when(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        InOrder inOrder = inOrder(mockedOfferMessageListener, mockedWebRtcMessageListener);
+
+        inOrder.verify(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        inOrder.verify(mockedWebRtcMessageListener).onOffer("theSdp", "theNick");
+    }
+
+    @Test
+    public void testRemoveWebRtcMessageListenerWhenHandlingOffer() {
+        SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener =
+            mock(SignalingMessageReceiver.OfferMessageListener.class);
+        SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
+            mock(SignalingMessageReceiver.WebRtcMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.removeListener(mockedWebRtcMessageListener);
+            return null;
+        }).when(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+
+        signalingMessageReceiver.addListener(mockedOfferMessageListener);
+        signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("offer");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("offer");
+        messagePayload.setSdp("theSdp");
+        messagePayload.setNick("theNick");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick");
+        verifyNoInteractions(mockedWebRtcMessageListener);
+    }
+}