Add listener for call participant messages

Although "unshareScreen" is technically bound to a specific peer
connection it is instead treated as a general message on the call
participant.

Nevertheless, call participant messages will make possible (at a later
point) to listen to events like "raise hand" or "mute" (which, again,
could be technically bound to a specific peer connection, but at least
for now are treated as a general message on the call participant).

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2022-10-19 17:27:05 +02:00
parent d42fe61e89
commit bda7d2719b
4 changed files with 428 additions and 8 deletions

View File

@ -266,6 +266,9 @@ public class CallActivity extends CallBaseActivity {
private CallActivitySignalingMessageReceiver signalingMessageReceiver = new CallActivitySignalingMessageReceiver();
private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
new HashMap<>();
private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() {
@Override
public void onOffer(String sessionId, String roomType, String sdp, String nick) {
@ -1675,14 +1678,6 @@ public class CallActivity extends CallBaseActivity {
private void processMessage(NCSignalingMessage ncSignalingMessage) {
if ("video".equals(ncSignalingMessage.getRoomType()) || "screen".equals(ncSignalingMessage.getRoomType())) {
String type = ncSignalingMessage.getType();
if ("unshareScreen".equals(type)) {
endPeerConnection(ncSignalingMessage.getFrom(), true);
return;
}
signalingMessageReceiver.process(ncSignalingMessage);
} else {
Log.e(TAG, "unexpected RoomType while processing NCSignalingMessage");
@ -2028,6 +2023,15 @@ public class CallActivity extends CallBaseActivity {
peerConnectionWrapperList.add(peerConnectionWrapper);
// Currently there is no separation between call participants and peer connections, so any video peer
// connection (except the own publisher connection) is treated as a call participant.
if (!publisher && "video".equals(type)) {
SignalingMessageReceiver.CallParticipantMessageListener callParticipantMessageListener =
new CallActivityCallParticipantMessageListener(sessionId);
callParticipantMessageListeners.put(sessionId, callParticipantMessageListener);
signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId);
}
if (publisher) {
startSendingNick();
}
@ -2060,6 +2064,11 @@ public class CallActivity extends CallBaseActivity {
}
}
}
if (!justScreen) {
SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
signalingMessageReceiver.removeListener(listener);
}
}
private void removeMediaStream(String sessionId, String videoStreamType) {
@ -2642,6 +2651,20 @@ public class CallActivity extends CallBaseActivity {
}
}
private class CallActivityCallParticipantMessageListener implements SignalingMessageReceiver.CallParticipantMessageListener {
private final String sessionId;
public CallActivityCallParticipantMessageListener(String sessionId) {
this.sessionId = sessionId;
}
@Override
public void onUnshareScreen() {
endPeerConnection(sessionId, true);
}
}
private class MicrophoneButtonTouchListener implements View.OnTouchListener {
@SuppressLint("ClickableViewAccessibility")

View File

@ -0,0 +1,95 @@
/*
* 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.Iterator;
import java.util.List;
/**
* Helper class to register and notify CallParticipantMessageListeners.
*
* This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
* a SignalingMessageReceiver rather than against a CallParticipantMessageNotifier.
*/
class CallParticipantMessageNotifier {
/**
* Helper class to associate a CallParticipantMessageListener with a session ID.
*/
private static class CallParticipantMessageListenerFrom {
public final SignalingMessageReceiver.CallParticipantMessageListener listener;
public final String sessionId;
private CallParticipantMessageListenerFrom(SignalingMessageReceiver.CallParticipantMessageListener listener,
String sessionId) {
this.listener = listener;
this.sessionId = sessionId;
}
}
private final List<CallParticipantMessageListenerFrom> callParticipantMessageListenersFrom = new ArrayList<>();
public synchronized void addListener(SignalingMessageReceiver.CallParticipantMessageListener listener, String sessionId) {
if (listener == null) {
throw new IllegalArgumentException("CallParticipantMessageListener can not be null");
}
if (sessionId == null) {
throw new IllegalArgumentException("sessionId can not be null");
}
removeListener(listener);
callParticipantMessageListenersFrom.add(new CallParticipantMessageListenerFrom(listener, sessionId));
}
public synchronized void removeListener(SignalingMessageReceiver.CallParticipantMessageListener listener) {
Iterator<CallParticipantMessageListenerFrom> it = callParticipantMessageListenersFrom.iterator();
while (it.hasNext()) {
CallParticipantMessageListenerFrom listenerFrom = it.next();
if (listenerFrom.listener == listener) {
it.remove();
return;
}
}
}
private List<SignalingMessageReceiver.CallParticipantMessageListener> getListenersFor(String sessionId) {
List<SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
new ArrayList<>(callParticipantMessageListenersFrom.size());
for (CallParticipantMessageListenerFrom listenerFrom : callParticipantMessageListenersFrom) {
if (listenerFrom.sessionId.equals(sessionId)) {
callParticipantMessageListeners.add(listenerFrom.listener);
}
}
return callParticipantMessageListeners;
}
public synchronized void notifyUnshareScreen(String sessionId) {
for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
listener.onUnshareScreen();
}
}
}

View File

@ -44,6 +44,19 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
*/
public abstract class SignalingMessageReceiver {
/**
* Listener for call participant messages.
*
* The messages are bound to a specific call participant (or, rather, session), so each listener is expected to
* handle messages only for a single call participant.
*
* Although "unshareScreen" is technically bound to a specific peer connection it is instead treated as a general
* message on the call participant.
*/
public interface CallParticipantMessageListener {
void onUnshareScreen();
}
/**
* Listener for WebRTC offers.
*
@ -70,10 +83,29 @@ public abstract class SignalingMessageReceiver {
void onEndOfCandidates();
}
private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier();
/**
* Adds a listener for call participant messages.
*
* A listener is expected to be added only once. If the same listener is added again it will no longer be notified
* for the messages from the previous session ID.
*
* @param listener the CallParticipantMessageListener
* @param sessionId the ID of the session that messages come from
*/
public void addListener(CallParticipantMessageListener listener, String sessionId) {
callParticipantMessageNotifier.addListener(listener, sessionId);
}
public void removeListener(CallParticipantMessageListener listener) {
callParticipantMessageNotifier.removeListener(listener);
}
/**
* Adds a listener for all offer messages.
*
@ -116,6 +148,43 @@ public abstract class SignalingMessageReceiver {
String sessionId = signalingMessage.getFrom();
String roomType = signalingMessage.getRoomType();
// "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling
// server is used, and to the room when the external signaling server is used. However, the (relevant) data
// of the received message ("from" and "type") is the same in both cases.
if ("unshareScreen".equals(type)) {
// Message schema (external signaling server):
// {
// "type": "message",
// "message": {
// "sender": {
// ...
// },
// "data": {
// "roomType": "screen",
// "type": "unshareScreen",
// "from": #STRING#,
// },
// },
// }
//
// Message schema (internal signaling server):
// {
// "type": "message",
// "data": {
// "to": #STRING#,
// "sid": #STRING#,
// "broadcaster": #STRING#,
// "roomType": "screen",
// "type": "unshareScreen",
// "from": #STRING#,
// },
// }
callParticipantMessageNotifier.notifyUnshareScreen(sessionId);
return;
}
if ("offer".equals(type)) {
// Message schema (external signaling server):
// {

View File

@ -0,0 +1,233 @@
/*
* 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.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 SignalingMessageReceiverCallParticipantTest {
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 testAddCallParticipantMessageListenerWithNullListener() {
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener(null, "theSessionId");
});
}
@Test
public void testAddCallParticipantMessageListenerWithNullSessionId() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, null);
});
}
@Test
public void testCallParticipantMessageUnshareScreen() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener, only()).onUnshareScreen();
}
@Test
public void testCallParticipantMessageSeveralListenersSameFrom() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
verify(mockedCallParticipantMessageListener2, only()).onUnshareScreen();
}
@Test
public void testCallParticipantMessageNotMatchingSessionId() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("notMatchingSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedCallParticipantMessageListener);
}
@Test
public void testCallParticipantMessageAfterRemovingListener() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener);
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedCallParticipantMessageListener);
}
@Test
public void testCallParticipantMessageAfterRemovingSingleListenerOfSeveral() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener3 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener3, "theSessionId");
signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2);
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
verify(mockedCallParticipantMessageListener3, only()).onUnshareScreen();
verifyNoInteractions(mockedCallParticipantMessageListener2);
}
@Test
public void testCallParticipantMessageAfterAddingListenerAgainForDifferentFrom() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId2");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedCallParticipantMessageListener);
signalingMessage.setFrom("theSessionId2");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener, only()).onUnshareScreen();
}
@Test
public void testAddCallParticipantMessageListenerWhenHandlingCallParticipantMessage() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
return null;
}).when(mockedCallParticipantMessageListener1).onUnshareScreen();
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
verifyNoInteractions(mockedCallParticipantMessageListener2);
}
@Test
public void testRemoveCallParticipantMessageListenerWhenHandlingCallParticipantMessage() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2);
return null;
}).when(mockedCallParticipantMessageListener1).onUnshareScreen();
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("unshareScreen");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
InOrder inOrder = inOrder(mockedCallParticipantMessageListener1, mockedCallParticipantMessageListener2);
inOrder.verify(mockedCallParticipantMessageListener1).onUnshareScreen();
inOrder.verify(mockedCallParticipantMessageListener2).onUnshareScreen();
}
}