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>
This commit is contained in:
Daniel Calviño Sánchez 2022-10-18 01:16:09 +02:00
parent 476fb59a08
commit d42fe61e89
5 changed files with 473 additions and 5 deletions

View file

@ -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");

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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");
}
}

View file

@ -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);
}
}