Merge pull request #2536 from nextcloud/add-listeners-for-signaling-messages

Add listeners for signaling messages
This commit is contained in:
Marcel Hibbe 2022-12-23 21:48:24 +01:00 committed by GitHub
commit 93137e1eeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2551 additions and 248 deletions

View File

@ -78,7 +78,6 @@ import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick;
import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
import com.nextcloud.talk.models.json.signaling.NCMessageWrapper;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
@ -86,6 +85,7 @@ import com.nextcloud.talk.models.json.signaling.Signaling;
import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.IceServer;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import com.nextcloud.talk.ui.dialog.AudioOutputDialog;
import com.nextcloud.talk.users.UserManager;
import com.nextcloud.talk.utils.ApiUtils;
@ -116,14 +116,12 @@ import org.webrtc.CameraVideoCapturer;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.Logging;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
@ -175,11 +173,6 @@ import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISS
import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID;
import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN;
import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY;
import static com.nextcloud.talk.webrtc.Globals.JOB_ID;
import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE;
import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_ALL;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_IN_CALL;
@AutoInjector(NextcloudTalkApplication.class)
public class CallActivity extends CallBaseActivity {
@ -266,6 +259,40 @@ public class CallActivity extends CallBaseActivity {
private SpotlightView spotlightView;
private InternalSignalingMessageReceiver internalSignalingMessageReceiver = new InternalSignalingMessageReceiver();
private SignalingMessageReceiver signalingMessageReceiver;
private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
new HashMap<>();
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = new SignalingMessageReceiver.ParticipantListMessageListener() {
@Override
public void onUsersInRoom(List<Participant> participants) {
processUsersInRoom(participants);
}
@Override
public void onParticipantsUpdate(List<Participant> participants) {
processUsersInRoom(participants);
}
@Override
public void onAllParticipantsUpdate(long inCall) {
if (inCall == Participant.InCallFlags.DISCONNECTED) {
Log.d(TAG, "A moderator ended the call for all.");
hangup(true);
}
}
};
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;
@ -1206,6 +1233,9 @@ public class CallActivity extends CallBaseActivity {
@Override
public void onDestroy() {
signalingMessageReceiver.removeListener(participantListMessageListener);
signalingMessageReceiver.removeListener(offerMessageListener);
if (localStream != null) {
localStream.dispose();
localStream = null;
@ -1336,6 +1366,9 @@ public class CallActivity extends CallBaseActivity {
if (hasExternalSignalingServer) {
setupAndInitiateWebSocketsConnection();
} else {
signalingMessageReceiver = internalSignalingMessageReceiver;
signalingMessageReceiver.addListener(participantListMessageListener);
signalingMessageReceiver.addListener(offerMessageListener);
joinRoomAndCall();
}
}
@ -1534,6 +1567,11 @@ public class CallActivity extends CallBaseActivity {
externalSignalingServer.getExternalSignalingServer(),
conversationUser, externalSignalingServer.getExternalSignalingTicket(),
TextUtils.isEmpty(credentials));
// Although setupAndInitiateWebSocketsConnection could be called several times the web socket is
// initialized just once, so the message receiver is also initialized just once.
signalingMessageReceiver = webSocketClient.getSignalingMessageReceiver();
signalingMessageReceiver.addListener(participantListMessageListener);
signalingMessageReceiver.addListener(offerMessageListener);
} else {
if (webSocketClient.isConnected() && currentCallStatus == CallStatus.PUBLISHER_FAILED) {
webSocketClient.restartWebSocket();
@ -1577,42 +1615,6 @@ public class CallActivity extends CallBaseActivity {
performCall();
}
break;
case PARTICIPANTS_UPDATE:
Log.d(TAG, "onMessageEvent 'participantsUpdate'");
// See MagicWebSocketInstance#onMessage in case "participants" how the 'updateParameters' are created
Map<String, String> updateParameters = webSocketCommunicationEvent.getHashMap();
if (updateParameters == null) {
break;
}
String updateRoomToken = updateParameters.get(ROOM_TOKEN);
String updateAll = updateParameters.get(UPDATE_ALL);
String updateInCall = updateParameters.get(UPDATE_IN_CALL);
String jobId = updateParameters.get(JOB_ID);
if (roomToken.equals(updateRoomToken)) {
if (updateAll != null && Boolean.parseBoolean(updateAll)) {
if ("0".equals(updateInCall)) {
Log.d(TAG, "Most probably a moderator ended the call for all.");
hangup(true);
}
} else if (jobId != null) {
// In that case a list of users for the room is passed.
processUsersInRoom(
(List<HashMap<String, Object>>) webSocketClient
.getJobWithId(
Integer.valueOf(jobId)));
}
}
break;
case "signalingMessage":
Log.d(TAG, "onMessageEvent 'signalingMessage'");
processMessage((NCSignalingMessage) webSocketClient.getJobWithId(
Integer.valueOf(webSocketCommunicationEvent.getHashMap().get("jobId"))));
break;
case "peerReadyForRequestingOffer":
Log.d(TAG, "onMessageEvent 'peerReadyForRequestingOffer'");
webSocketClient.requestOfferForSessionIdWithType(
@ -1652,85 +1654,16 @@ public class CallActivity extends CallBaseActivity {
}
if ("usersInRoom".equals(messageType)) {
processUsersInRoom((List<HashMap<String, Object>>) signaling.getMessageWrapper());
internalSignalingMessageReceiver.process((List<Map<String, Object>>) signaling.getMessageWrapper());
} else if ("message".equals(messageType)) {
NCSignalingMessage ncSignalingMessage = LoganSquare.parse(signaling.getMessageWrapper().toString(),
NCSignalingMessage.class);
processMessage(ncSignalingMessage);
internalSignalingMessageReceiver.process(ncSignalingMessage);
} else {
Log.e(TAG, "unexpected message type when receiving signaling message");
}
}
private void processMessage(NCSignalingMessage ncSignalingMessage) {
if ("video".equals(ncSignalingMessage.getRoomType()) || "screen".equals(ncSignalingMessage.getRoomType())) {
String type = null;
if (ncSignalingMessage.getPayload() != null && ncSignalingMessage.getPayload().getType() != null) {
type = ncSignalingMessage.getPayload().getType();
} else if (ncSignalingMessage.getType() != null) {
type = ncSignalingMessage.getType();
}
PeerConnectionWrapper peerConnectionWrapper = null;
if ("offer".equals(type)) {
peerConnectionWrapper =
getOrCreatePeerConnectionWrapperForSessionIdAndType(ncSignalingMessage.getFrom(),
ncSignalingMessage.getRoomType(), false);
} else {
peerConnectionWrapper =
getPeerConnectionWrapperForSessionIdAndType(ncSignalingMessage.getFrom(),
ncSignalingMessage.getRoomType());
}
if ("unshareScreen".equals(type) ||
(("offer".equals(type) ||
"answer".equals(type) ||
"candidate".equals(type) ||
"endOfCandidates".equals(type)) &&
peerConnectionWrapper != null)) {
switch (type) {
case "unshareScreen":
endPeerConnection(ncSignalingMessage.getFrom(), true);
break;
case "offer":
case "answer":
peerConnectionWrapper.setNick(ncSignalingMessage.getPayload().getNick());
SessionDescription sessionDescriptionWithPreferredCodec;
String sessionDescriptionStringWithPreferredCodec = MagicWebRTCUtils.preferCodec
(ncSignalingMessage.getPayload().getSdp(),
"H264", false);
sessionDescriptionWithPreferredCodec = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
sessionDescriptionStringWithPreferredCodec);
if (peerConnectionWrapper.getPeerConnection() != null) {
peerConnectionWrapper.getPeerConnection().setRemoteDescription(
peerConnectionWrapper.getMagicSdpObserver(),
sessionDescriptionWithPreferredCodec);
}
break;
case "candidate":
NCIceCandidate ncIceCandidate = ncSignalingMessage.getPayload().getIceCandidate();
IceCandidate iceCandidate = new IceCandidate(ncIceCandidate.getSdpMid(),
ncIceCandidate.getSdpMLineIndex(),
ncIceCandidate.getCandidate());
peerConnectionWrapper.addCandidate(iceCandidate);
break;
case "endOfCandidates":
peerConnectionWrapper.drainIceCandidates();
break;
default:
break;
}
}
} else {
Log.e(TAG, "unexpected RoomType while processing NCSignalingMessage");
}
}
private void hangup(boolean shutDownView) {
Log.d(TAG, "hangup! shutDownView=" + shutDownView);
if (shutDownView) {
@ -1836,7 +1769,7 @@ public class CallActivity extends CallBaseActivity {
}
}
private void processUsersInRoom(List<HashMap<String, Object>> users) {
private void processUsersInRoom(List<Participant> participants) {
Log.d(TAG, "processUsersInRoom");
List<String> newSessions = new ArrayList<>();
Set<String> oldSessions = new HashSet<>();
@ -1855,27 +1788,20 @@ public class CallActivity extends CallBaseActivity {
boolean isSelfInCall = false;
for (HashMap<String, Object> participant : users) {
long inCallFlag = (long) participant.get("inCall");
if (!participant.get("sessionId").equals(currentSessionId)) {
for (Participant participant : participants) {
long inCallFlag = participant.getInCall();
if (!participant.getSessionId().equals(currentSessionId)) {
Log.d(TAG, " inCallFlag of participant "
+ participant.get("sessionId").toString().substring(0, 4)
+ participant.getSessionId().substring(0, 4)
+ " : "
+ inCallFlag);
boolean isInCall = inCallFlag != 0;
if (isInCall) {
newSessions.add(participant.get("sessionId").toString());
newSessions.add(participant.getSessionId());
}
// The property is "userId" when not using the external signaling server and "userid" when using it.
String userId = null;
if (participant.get("userId") != null) {
userId = participant.get("userId").toString();
} else if (participant.get("userid") != null) {
userId = participant.get("userid").toString();
}
userIdsBySessionId.put(participant.get("sessionId").toString(), userId);
userIdsBySessionId.put(participant.getSessionId(), participant.getUserId());
} else {
Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag);
isSelfInCall = inCallFlag != 0;
@ -2028,7 +1954,8 @@ public class CallActivity extends CallBaseActivity {
localStream,
true,
true,
type);
type,
signalingMessageReceiver);
} else if (hasMCU) {
peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
@ -2039,7 +1966,8 @@ public class CallActivity extends CallBaseActivity {
null,
false,
true,
type);
type,
signalingMessageReceiver);
} else {
if (!"screen".equals(type)) {
peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
@ -2050,7 +1978,8 @@ public class CallActivity extends CallBaseActivity {
localStream,
false,
false,
type);
type,
signalingMessageReceiver);
} else {
peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
iceServers,
@ -2060,12 +1989,22 @@ public class CallActivity extends CallBaseActivity {
null,
false,
false,
type);
type,
signalingMessageReceiver);
}
}
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();
}
@ -2098,6 +2037,11 @@ public class CallActivity extends CallBaseActivity {
}
}
}
if (!justScreen) {
SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
signalingMessageReceiver.removeListener(listener);
}
}
private void removeMediaStream(String sessionId, String videoStreamType) {
@ -2671,6 +2615,35 @@ public class CallActivity extends CallBaseActivity {
}
}
/**
* Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from CallActivity.
*
* All listeners are called in the main thread.
*/
private static class InternalSignalingMessageReceiver extends SignalingMessageReceiver {
public void process(List<Map<String, Object>> users) {
processUsersInRoom(users);
}
public void process(NCSignalingMessage message) {
processSignalingMessage(message);
}
}
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

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

@ -0,0 +1,68 @@
/*
* 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.participants.Participant;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Helper class to register and notify ParticipantListMessageListeners.
*
* This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
* a SignalingMessageReceiver rather than against a ParticipantListMessageNotifier.
*/
class ParticipantListMessageNotifier {
private final Set<SignalingMessageReceiver.ParticipantListMessageListener> participantListMessageListeners = new LinkedHashSet<>();
public synchronized void addListener(SignalingMessageReceiver.ParticipantListMessageListener listener) {
if (listener == null) {
throw new IllegalArgumentException("participantListMessageListeners can not be null");
}
participantListMessageListeners.add(listener);
}
public synchronized void removeListener(SignalingMessageReceiver.ParticipantListMessageListener listener) {
participantListMessageListeners.remove(listener);
}
public synchronized void notifyUsersInRoom(List<Participant> participants) {
for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) {
listener.onUsersInRoom(participants);
}
}
public synchronized void notifyParticipantsUpdate(List<Participant> participants) {
for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) {
listener.onParticipantsUpdate(participants);
}
}
public synchronized void notifyAllParticipantsUpdate(long inCall) {
for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) {
listener.onAllParticipantsUpdate(inCall);
}
}
}

View File

@ -0,0 +1,601 @@
/*
* 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.converters.EnumParticipantTypeConverter;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 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
* in a different thread, as long as the notifier thread is not forced to wait until the listener is added or removed).
*
* SignalingMessageReceiver does not fetch the signaling messages itself; subclasses must fetch them and then call
* the appropriate protected methods to process the messages and notify the listeners.
*/
public abstract class SignalingMessageReceiver {
/**
* Listener for participant list messages.
*
* The messages are implicitly bound to the room currently joined in the signaling server; listeners are expected
* to know the current room.
*/
public interface ParticipantListMessageListener {
/**
* List of all the participants in the room.
*
* This message is received only when the internal signaling server is used.
*
* The message is received periodically, and the participants may not have been modified since the last message.
*
* Only the following participant properties are set:
* - inCall
* - lastPing
* - sessionId
* - userId (if the participant is not a guest)
*
* "participantPermissions" is provided in the message (since Talk 13), but not currently set in the
* participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is
* ignored.
*
* @param participants all the participants (users and guests) in the room
*/
void onUsersInRoom(List<Participant> participants);
/**
* List of all the participants in the call or the room (depending on what triggered the event).
*
* This message is received only when the external signaling server is used.
*
* The message is received when any participant changed, although what changed is not provided and should be
* derived from the difference with previous messages. The list of participants may include only the
* participants in the call (including those that just left it and thus triggered the event) or all the
* participants currently in the room (participants in the room but not currently active, that is, without a
* session, are not included).
*
* Only the following participant properties are set:
* - inCall
* - lastPing
* - sessionId
* - type
* - userId (if the participant is not a guest)
*
* "nextcloudSessionId" is provided in the message (when the "inCall" property of any participant changed), but
* not currently set in the participant.
*
* "participantPermissions" is provided in the message (since Talk 13), but not currently set in the
* participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is
* ignored.
*
* @param participants all the participants (users and guests) in the room
*/
void onParticipantsUpdate(List<Participant> participants);
/**
* Update of the properties of all the participants in the room.
*
* This message is received only when the external signaling server is used.
*
* @param inCall the new value of the inCall property
*/
void onAllParticipantsUpdate(long inCall);
}
/**
* 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.
*
* 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.
*
* The messages are bound to a specific peer connection, so each listener is expected to handle messages only for
* a single peer connection.
*/
public interface WebRtcMessageListener {
void onOffer(String sdp, String nick);
void onAnswer(String sdp, String nick);
void onCandidate(String sdpMid, int sdpMLineIndex, String sdp);
void onEndOfCandidates();
}
private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier();
private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier();
/**
* Adds a listener for participant list 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 ParticipantListMessageListener
*/
public void addListener(ParticipantListMessageListener listener) {
participantListMessageNotifier.addListener(listener);
}
public void removeListener(ParticipantListMessageListener listener) {
participantListMessageNotifier.removeListener(listener);
}
/**
* 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.
*
* 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.
*
* 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 or room type.
*
* @param listener the WebRtcMessageListener
* @param sessionId the ID of the session that messages come from
* @param roomType the room type that messages come from
*/
public void addListener(WebRtcMessageListener listener, String sessionId, String roomType) {
webRtcMessageNotifier.addListener(listener, sessionId, roomType);
}
public void removeListener(WebRtcMessageListener listener) {
webRtcMessageNotifier.removeListener(listener);
}
protected void processEvent(Map<String, Object> eventMap) {
if (!"update".equals(eventMap.get("type")) || !"participants".equals(eventMap.get("target"))) {
return;
}
Map<String, Object> updateMap;
try {
updateMap = (Map<String, Object>) eventMap.get("update");
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
if (updateMap == null) {
// Broken message, this should not happen.
return;
}
if (updateMap.get("all") != null && Boolean.parseBoolean(updateMap.get("all").toString())) {
processAllParticipantsUpdate(updateMap);
return;
}
if (updateMap.get("users") != null) {
processParticipantsUpdate(updateMap);
return;
}
}
private void processAllParticipantsUpdate(Map<String, Object> updateMap) {
// Message schema:
// {
// "type": "event",
// "event": {
// "target": "participants",
// "type": "update",
// "update": {
// "roomid": #STRING#,
// "incall": 0,
// "all": true,
// },
// },
// }
long inCall;
try {
inCall = Long.parseLong(updateMap.get("inCall").toString());
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
participantListMessageNotifier.notifyAllParticipantsUpdate(inCall);
}
private void processParticipantsUpdate(Map<String, Object> updateMap) {
// Message schema:
// {
// "type": "event",
// "event": {
// "target": "participants",
// "type": "update",
// "update": {
// "roomid": #INTEGER#,
// "users": [
// {
// "inCall": #INTEGER#,
// "lastPing": #INTEGER#,
// "sessionId": #STRING#,
// "participantType": #INTEGER#,
// "userId": #STRING#, // Optional
// "nextcloudSessionId": #STRING#, // Optional
// "participantPermissions": #INTEGER#, // Talk >= 13
// },
// ...
// ],
// },
// },
// }
//
// Note that "userId" in participants->update comes from the Nextcloud server, so it is "userId"; in other
// messages, like room->join, it comes directly from the external signaling server, so it is "userid" instead.
List<Map<String, Object>> users;
try {
users = (List<Map<String, Object>>) updateMap.get("users");
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
if (users == null) {
// Broken message, this should not happen.
return;
}
List<Participant> participants = new ArrayList<>(users.size());
for (Map<String, Object> user: users) {
try {
participants.add(getParticipantFromMessageMap(user));
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
}
participantListMessageNotifier.notifyParticipantsUpdate(participants);
}
protected void processUsersInRoom(List<Map<String, Object>> users) {
// Message schema:
// {
// "type": "usersInRoom",
// "data": [
// {
// "inCall": #INTEGER#,
// "lastPing": #INTEGER#,
// "roomId": #INTEGER#,
// "sessionId": #STRING#,
// "userId": #STRING#, // Always included, although it can be empty
// "participantPermissions": #INTEGER#, // Talk >= 13
// },
// ...
// ],
// }
List<Participant> participants = new ArrayList<>(users.size());
for (Map<String, Object> user: users) {
try {
participants.add(getParticipantFromMessageMap(user));
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
}
participantListMessageNotifier.notifyUsersInRoom(participants);
}
/**
* Creates and initializes a Participant from the data in the given map.
*
* Maps from internal and external signaling server messages can be used. Nevertheless, besides the differences
* between the messages and the optional properties, it is expected that the message is correct and the given data
* is parseable. Broken messages (for example, a string instead of an integer for "inCall" or a missing
* "sessionId") may cause a RuntimeException to be thrown.
*
* @param participantMap the map with the participant data
* @return the Participant
*/
private Participant getParticipantFromMessageMap(Map<String, Object> participantMap) {
Participant participant = new Participant();
participant.setInCall(Long.parseLong(participantMap.get("inCall").toString()));
participant.setLastPing(Long.parseLong(participantMap.get("lastPing").toString()));
participant.setSessionId(participantMap.get("sessionId").toString());
if (participantMap.get("userId") != null && !participantMap.get("userId").toString().isEmpty()) {
participant.setUserId(participantMap.get("userId").toString());
}
// Only in external signaling messages
if (participantMap.get("participantType") != null) {
int participantTypeInt = Integer.parseInt(participantMap.get("participantType").toString());
EnumParticipantTypeConverter converter = new EnumParticipantTypeConverter();
participant.setType(converter.getFromInt(participantTypeInt));
}
return participant;
}
protected void processSignalingMessage(NCSignalingMessage signalingMessage) {
// Note that in the internal signaling server message "data" is the String representation of a JSON
// object, although it is already decoded when used here.
String type = signalingMessage.getType();
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):
// {
// "type": "message",
// "message": {
// "sender": {
// ...
// },
// "data": {
// "to": #STRING#,
// "from": #STRING#,
// "type": "offer",
// "roomType": #STRING#, // "video" or "screen"
// "payload": {
// "type": "offer",
// "sdp": #STRING#,
// },
// "sid": #STRING#, // external signaling server >= 0.5.0
// },
// },
// }
//
// Message schema (internal signaling server):
// {
// "type": "message",
// "data": {
// "to": #STRING#,
// "sid": #STRING#,
// "roomType": #STRING#, // "video" or "screen"
// "type": "offer",
// "payload": {
// "type": "offer",
// "sdp": #STRING#,
// "nick": #STRING#, // Optional
// },
// "from": #STRING#,
// },
// }
NCMessagePayload payload = signalingMessage.getPayload();
if (payload == null) {
// Broken message, this should not happen.
return;
}
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;
}
if ("answer".equals(type)) {
// Message schema: same as offers, but with type "answer".
NCMessagePayload payload = signalingMessage.getPayload();
if (payload == null) {
// Broken message, this should not happen.
return;
}
String sdp = payload.getSdp();
String nick = payload.getNick();
webRtcMessageNotifier.notifyAnswer(sessionId, roomType, sdp, nick);
return;
}
if ("candidate".equals(type)) {
// Message schema (external signaling server):
// {
// "type": "message",
// "message": {
// "sender": {
// ...
// },
// "data": {
// "to": #STRING#,
// "from": #STRING#,
// "type": "candidate",
// "roomType": #STRING#, // "video" or "screen"
// "payload": {
// "candidate": {
// "candidate": #STRING#,
// "sdpMid": #STRING#,
// "sdpMLineIndex": #INTEGER#,
// },
// },
// "sid": #STRING#, // external signaling server >= 0.5.0
// },
// },
// }
//
// Message schema (internal signaling server):
// {
// "type": "message",
// "data": {
// "to": #STRING#,
// "sid": #STRING#,
// "roomType": #STRING#, // "video" or "screen"
// "type": "candidate",
// "payload": {
// "candidate": {
// "candidate": #STRING#,
// "sdpMid": #STRING#,
// "sdpMLineIndex": #INTEGER#,
// },
// },
// "from": #STRING#,
// },
// }
NCMessagePayload payload = signalingMessage.getPayload();
if (payload == null) {
// Broken message, this should not happen.
return;
}
NCIceCandidate ncIceCandidate = payload.getIceCandidate();
if (ncIceCandidate == null) {
// Broken message, this should not happen.
return;
}
webRtcMessageNotifier.notifyCandidate(sessionId,
roomType,
ncIceCandidate.getSdpMid(),
ncIceCandidate.getSdpMLineIndex(),
ncIceCandidate.getCandidate());
return;
}
if ("endOfCandidates".equals(type)) {
webRtcMessageNotifier.notifyEndOfCandidates(sessionId, roomType);
return;
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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 WebRtcMessageListeners.
*
* This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
* a SignalingMessageReceiver rather than against a WebRtcMessageNotifier.
*/
class WebRtcMessageNotifier {
/**
* Helper class to associate a WebRtcMessageListener with a session ID and room type.
*/
private static class WebRtcMessageListenerFrom {
public final SignalingMessageReceiver.WebRtcMessageListener listener;
public final String sessionId;
public final String roomType;
private WebRtcMessageListenerFrom(SignalingMessageReceiver.WebRtcMessageListener listener,
String sessionId,
String roomType) {
this.listener = listener;
this.sessionId = sessionId;
this.roomType = roomType;
}
}
private final List<WebRtcMessageListenerFrom> webRtcMessageListenersFrom = new ArrayList<>();
public synchronized void addListener(SignalingMessageReceiver.WebRtcMessageListener listener, String sessionId, String roomType) {
if (listener == null) {
throw new IllegalArgumentException("WebRtcMessageListener can not be null");
}
if (sessionId == null) {
throw new IllegalArgumentException("sessionId can not be null");
}
if (roomType == null) {
throw new IllegalArgumentException("roomType can not be null");
}
removeListener(listener);
webRtcMessageListenersFrom.add(new WebRtcMessageListenerFrom(listener, sessionId, roomType));
}
public synchronized void removeListener(SignalingMessageReceiver.WebRtcMessageListener listener) {
Iterator<WebRtcMessageListenerFrom> it = webRtcMessageListenersFrom.iterator();
while (it.hasNext()) {
WebRtcMessageListenerFrom listenerFrom = it.next();
if (listenerFrom.listener == listener) {
it.remove();
return;
}
}
}
private List<SignalingMessageReceiver.WebRtcMessageListener> getListenersFor(String sessionId, String roomType) {
List<SignalingMessageReceiver.WebRtcMessageListener> webRtcMessageListeners =
new ArrayList<>(webRtcMessageListenersFrom.size());
for (WebRtcMessageListenerFrom listenerFrom : webRtcMessageListenersFrom) {
if (listenerFrom.sessionId.equals(sessionId) && listenerFrom.roomType.equals(roomType)) {
webRtcMessageListeners.add(listenerFrom.listener);
}
}
return webRtcMessageListeners;
}
public synchronized void notifyOffer(String sessionId, String roomType, String sdp, String nick) {
for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) {
listener.onOffer(sdp, nick);
}
}
public synchronized void notifyAnswer(String sessionId, String roomType, String sdp, String nick) {
for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) {
listener.onAnswer(sdp, nick);
}
}
public synchronized void notifyCandidate(String sessionId, String roomType, String sdpMid, int sdpMLineIndex, String sdp) {
for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) {
listener.onCandidate(sdpMid, sdpMLineIndex, sdp);
}
}
public synchronized void notifyEndOfCandidates(String sessionId, String roomType) {
for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) {
listener.onEndOfCandidates();
}
}
}

View File

@ -1,31 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.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.utils
import java.util.concurrent.ConcurrentHashMap
class MagicMap : ConcurrentHashMap<Int, Any>() {
fun add(element: Any): Int {
val key = System.identityHashCode(element)
super.put(key, element)
return key
}
}

View File

@ -2,17 +2,6 @@ package com.nextcloud.talk.webrtc;
public class Globals {
public static final String ROOM_TOKEN = "roomToken";
public static final String JOB_ID = "jobId";
public static final String PARTICIPANTS_UPDATE = "participantsUpdate";
public static final String TARGET_PARTICIPANTS = "participants";
public static final String EVENT_TYPE = "type";
public static final String EVENT_TYPE_UPDATE = "update";
public static final String UPDATE_ALL = "all";
public static final String UPDATE_IN_CALL = "incall";
public static final String UPDATE_ROOM_ID = "roomid";
public static final String UPDATE_USERS = "users";
}

View File

@ -40,7 +40,7 @@ import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage;
import com.nextcloud.talk.utils.MagicMap;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import org.greenrobot.eventbus.EventBus;
@ -65,16 +65,8 @@ import okio.ByteString;
import static com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS;
import static com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS;
import static com.nextcloud.talk.webrtc.Globals.EVENT_TYPE;
import static com.nextcloud.talk.webrtc.Globals.EVENT_TYPE_UPDATE;
import static com.nextcloud.talk.webrtc.Globals.JOB_ID;
import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE;
import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
import static com.nextcloud.talk.webrtc.Globals.TARGET_PARTICIPANTS;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_ALL;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_IN_CALL;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_ROOM_ID;
import static com.nextcloud.talk.webrtc.Globals.UPDATE_USERS;
@AutoInjector(NextcloudTalkApplication.class)
public class MagicWebSocketInstance extends WebSocketListener {
@ -98,7 +90,6 @@ public class MagicWebSocketInstance extends WebSocketListener {
private boolean connected;
private WebSocketConnectionHelper webSocketConnectionHelper;
private WebSocket internalWebSocket;
private MagicMap magicMap;
private String connectionUrl;
private String currentRoomToken;
@ -109,6 +100,8 @@ public class MagicWebSocketInstance extends WebSocketListener {
private List<String> messagesQueue = new ArrayList<>();
private final ExternalSignalingMessageReceiver signalingMessageReceiver = new ExternalSignalingMessageReceiver();
MagicWebSocketInstance(User conversationUser, String connectionUrl, String webSocketTicket) {
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
@ -117,7 +110,6 @@ public class MagicWebSocketInstance extends WebSocketListener {
this.webSocketTicket = webSocketTicket;
this.webSocketConnectionHelper = new WebSocketConnectionHelper();
this.usersHashMap = new HashMap<>();
magicMap = new MagicMap();
connected = false;
eventBus.register(this);
@ -276,45 +268,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
}
break;
case TARGET_PARTICIPANTS:
if (EVENT_TYPE_UPDATE.equals(eventOverallWebSocketMessage.getEventMap().get(EVENT_TYPE))) {
HashMap<String, String> refreshChatHashMap = new HashMap<>();
HashMap<String, Object> updateEventMap = (HashMap<String, Object>) eventOverallWebSocketMessage.getEventMap().get(EVENT_TYPE_UPDATE);
if (updateEventMap == null) {
break;
}
if (updateEventMap.containsKey(UPDATE_ROOM_ID)) {
Object updateRoomId = updateEventMap.get(UPDATE_ROOM_ID);
if (updateRoomId != null) {
refreshChatHashMap.put(ROOM_TOKEN,
(String) updateEventMap.get(UPDATE_ROOM_ID));
}
}
if (updateEventMap.containsKey(UPDATE_USERS)) {
Object updateUsers = updateEventMap.get(UPDATE_USERS);
if (updateUsers != null) {
refreshChatHashMap.put(JOB_ID, Integer.toString(magicMap.add(updateUsers)));
}
}
if (updateEventMap.containsKey(UPDATE_IN_CALL)) {
Object inCall = updateEventMap.get(UPDATE_IN_CALL);
if (inCall != null) {
refreshChatHashMap.put(UPDATE_IN_CALL, Long.toString((Long) inCall));
}
}
if (updateEventMap.containsKey(UPDATE_ALL)) {
Object updateAll = updateEventMap.get(UPDATE_ALL);
if (updateAll != null) {
refreshChatHashMap.put(UPDATE_ALL, Boolean.toString((Boolean) updateAll));
}
}
eventBus.post(new WebSocketCommunicationEvent(PARTICIPANTS_UPDATE, refreshChatHashMap));
}
signalingMessageReceiver.process(eventOverallWebSocketMessage.getEventMap());
break;
}
}
@ -326,11 +280,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
}
if (!TextUtils.isEmpty(ncSignalingMessage.getFrom())) {
HashMap<String, String> messageHashMap = new HashMap<>();
messageHashMap.put(JOB_ID, Integer.toString(magicMap.add(ncSignalingMessage)));
eventBus.post(new WebSocketCommunicationEvent("signalingMessage", messageHashMap));
}
signalingMessageReceiver.process(ncSignalingMessage);
break;
case "bye":
connected = false;
@ -407,12 +357,6 @@ public class MagicWebSocketInstance extends WebSocketListener {
}
}
public Object getJobWithId(Integer id) {
Object copyJob = magicMap.get(id);
magicMap.remove(id);
return copyJob;
}
public void requestOfferForSessionIdWithType(String sessionIdParam, String roomType) {
try {
String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType));
@ -471,4 +415,25 @@ public class MagicWebSocketInstance extends WebSocketListener {
restartWebSocket();
}
}
public SignalingMessageReceiver getSignalingMessageReceiver() {
return signalingMessageReceiver;
}
/**
* Temporary implementation of SignalingMessageReceiver until signaling related code is extracted to a Signaling
* class.
*
* All listeners are called in the WebSocket reader thread. This thread should be the same as long as the
* WebSocket stays connected, but it may change whenever it is connected again.
*/
private static class ExternalSignalingMessageReceiver extends SignalingMessageReceiver {
public void process(Map<String, Object> eventMap) {
processEvent(eventMap);
}
public void process(NCSignalingMessage message) {
processSignalingMessage(message);
}
}
}

View File

@ -37,6 +37,7 @@ import com.nextcloud.talk.events.WebSocketCommunicationEvent;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick;
import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import org.greenrobot.eventbus.EventBus;
import org.webrtc.AudioTrack;
@ -74,6 +75,9 @@ public class PeerConnectionWrapper {
private static final String TAG = PeerConnectionWrapper.class.getCanonicalName();
private final SignalingMessageReceiver signalingMessageReceiver;
private final WebRtcMessageListener webRtcMessageListener = new WebRtcMessageListener();
private List<IceCandidate> iceCandidates = new ArrayList<>();
private PeerConnection peerConnection;
private String sessionId;
@ -96,7 +100,8 @@ public class PeerConnectionWrapper {
List<PeerConnection.IceServer> iceServerList,
MediaConstraints mediaConstraints,
String sessionId, String localSession, @Nullable MediaStream localStream,
boolean isMCUPublisher, boolean hasMCU, String videoStreamType) {
boolean isMCUPublisher, boolean hasMCU, String videoStreamType,
SignalingMessageReceiver signalingMessageReceiver) {
Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication()).getComponentApplication().inject(this);
@ -114,6 +119,9 @@ public class PeerConnectionWrapper {
configuration.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
peerConnection = peerConnectionFactory.createPeerConnection(configuration, new MagicPeerConnectionObserver());
this.signalingMessageReceiver = signalingMessageReceiver;
this.signalingMessageReceiver.addListener(webRtcMessageListener, sessionId, videoStreamType);
if (peerConnection != null) {
if (this.localStream != null) {
List<String> localStreamIds = Collections.singletonList(this.localStream.getId());
@ -150,6 +158,8 @@ public class PeerConnectionWrapper {
}
public void removePeerConnection() {
signalingMessageReceiver.removeListener(webRtcMessageListener);
if (dataChannel != null) {
dataChannel.dispose();
dataChannel = null;
@ -167,7 +177,7 @@ public class PeerConnectionWrapper {
}
}
public void drainIceCandidates() {
private void drainIceCandidates() {
if (peerConnection != null) {
for (IceCandidate iceCandidate : iceCandidates) {
@ -178,11 +188,7 @@ public class PeerConnectionWrapper {
}
}
public MagicSdpObserver getMagicSdpObserver() {
return magicSdpObserver;
}
public void addCandidate(IceCandidate iceCandidate) {
private void addCandidate(IceCandidate iceCandidate) {
if (peerConnection != null && peerConnection.getRemoteDescription() != null) {
peerConnection.addIceCandidate(iceCandidate);
} else {
@ -222,10 +228,6 @@ public class PeerConnectionWrapper {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getNick() {
if (!TextUtils.isEmpty(nick)) {
return nick;
@ -234,7 +236,7 @@ public class PeerConnectionWrapper {
}
}
public void setNick(String nick) {
private void setNick(String nick) {
this.nick = nick;
}
@ -267,6 +269,43 @@ public class PeerConnectionWrapper {
return false;
}
private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener {
public void onOffer(String sdp, String nick) {
onOfferOrAnswer("offer", sdp, nick);
}
public void onAnswer(String sdp, String nick) {
onOfferOrAnswer("answer", sdp, nick);
}
private void onOfferOrAnswer(String type, String sdp, String nick) {
setNick(nick);
SessionDescription sessionDescriptionWithPreferredCodec;
boolean isAudio = false;
String sessionDescriptionStringWithPreferredCodec = MagicWebRTCUtils.preferCodec(sdp, "H264", isAudio);
sessionDescriptionWithPreferredCodec = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
sessionDescriptionStringWithPreferredCodec);
if (getPeerConnection() != null) {
getPeerConnection().setRemoteDescription(magicSdpObserver, sessionDescriptionWithPreferredCodec);
}
}
public void onCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
addCandidate(iceCandidate);
}
public void onEndOfCandidates() {
drainIceCandidates();
}
}
private class MagicDataChannelObserver implements DataChannel.Observer {
@Override

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

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((SignalingMessageReceiver.OfferMessageListener) 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,466 @@
/*
* 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.participants.Participant;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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 SignalingMessageReceiverParticipantListTest {
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 testAddParticipantListMessageListenerWithNullListener() {
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener((SignalingMessageReceiver.ParticipantListMessageListener) null);
});
}
@Test
public void testInternalSignalingParticipantListMessageUsersInRoom() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
List<Map<String, Object>> users = new ArrayList<>(2);
Map<String, Object> user1 = new HashMap<>();
user1.put("inCall", 7);
user1.put("lastPing", 4815);
user1.put("roomId", 108);
user1.put("sessionId", "theSessionId1");
user1.put("userId", "theUserId");
// If "participantPermissions" is set in any of the participants all the other participants in the message
// would have it too. But for test simplicity, and as it is not relevant for the processing, in this test it
// is included only in one of the participants.
user1.put("participantPermissions", 42);
users.add(user1);
Map<String, Object> user2 = new HashMap<>();
user2.put("inCall", 0);
user2.put("lastPing", 162342);
user2.put("roomId", 108);
user2.put("sessionId", "theSessionId2");
user2.put("userId", "");
users.add(user2);
signalingMessageReceiver.processUsersInRoom(users);
List<Participant> expectedParticipantList = new ArrayList<>();
Participant expectedParticipant1 = new Participant();
expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO);
expectedParticipant1.setLastPing(4815);
expectedParticipant1.setSessionId("theSessionId1");
expectedParticipant1.setUserId("theUserId");
expectedParticipantList.add(expectedParticipant1);
Participant expectedParticipant2 = new Participant();
expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant2.setLastPing(162342);
expectedParticipant2.setSessionId("theSessionId2");
expectedParticipantList.add(expectedParticipant2);
verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList);
}
@Test
public void testInternalSignalingParticipantListMessageAfterRemovingListener() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener);
List<Map<String, Object>> users = new ArrayList<>(1);
Map<String, Object> user = new HashMap<>();
user.put("inCall", 0);
user.put("lastPing", 4815);
user.put("roomId", 108);
user.put("sessionId", "theSessionId");
user.put("userId", "");
users.add(user);
signalingMessageReceiver.processUsersInRoom(users);
verifyNoInteractions(mockedParticipantListMessageListener);
}
@Test
public void testInternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener3);
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2);
List<Map<String, Object>> users = new ArrayList<>(1);
Map<String, Object> user = new HashMap<>();
user.put("inCall", 0);
user.put("lastPing", 4815);
user.put("roomId", 108);
user.put("sessionId", "theSessionId");
user.put("userId", "");
users.add(user);
signalingMessageReceiver.processUsersInRoom(users);
List<Participant> expectedParticipantList = new ArrayList<>();
Participant expectedParticipant = new Participant();
expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant.setLastPing(4815);
expectedParticipant.setSessionId("theSessionId");
expectedParticipantList.add(expectedParticipant);
verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList);
verify(mockedParticipantListMessageListener3, only()).onUsersInRoom(expectedParticipantList);
verifyNoInteractions(mockedParticipantListMessageListener2);
}
@Test
public void testInternalSignalingParticipantListMessageAfterAddingListenerAgain() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
List<Map<String, Object>> users = new ArrayList<>(1);
Map<String, Object> user = new HashMap<>();
user.put("inCall", 0);
user.put("lastPing", 4815);
user.put("roomId", 108);
user.put("sessionId", "theSessionId");
user.put("userId", "");
users.add(user);
signalingMessageReceiver.processUsersInRoom(users);
List<Participant> expectedParticipantList = new ArrayList<>();
Participant expectedParticipant = new Participant();
expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant.setLastPing(4815);
expectedParticipant.setSessionId("theSessionId");
expectedParticipantList.add(expectedParticipant);
verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList);
}
@Test
public void testAddParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
List<Participant> expectedParticipantList = new ArrayList<>();
Participant expectedParticipant = new Participant();
expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant.setLastPing(4815);
expectedParticipant.setSessionId("theSessionId");
expectedParticipantList.add(expectedParticipant);
doAnswer((invocation) -> {
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
return null;
}).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
List<Map<String, Object>> users = new ArrayList<>(1);
Map<String, Object> user = new HashMap<>();
user.put("inCall", 0);
user.put("lastPing", 4815);
user.put("roomId", 108);
user.put("sessionId", "theSessionId");
user.put("userId", "");
users.add(user);
signalingMessageReceiver.processUsersInRoom(users);
verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList);
verifyNoInteractions(mockedParticipantListMessageListener2);
}
@Test
public void testRemoveParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
List<Participant> expectedParticipantList = new ArrayList<>();
Participant expectedParticipant = new Participant();
expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant.setLastPing(4815);
expectedParticipant.setSessionId("theSessionId");
expectedParticipantList.add(expectedParticipant);
doAnswer((invocation) -> {
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2);
return null;
}).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
List<Map<String, Object>> users = new ArrayList<>(1);
Map<String, Object> user = new HashMap<>();
user.put("inCall", 0);
user.put("lastPing", 4815);
user.put("roomId", 108);
user.put("sessionId", "theSessionId");
user.put("userId", "");
users.add(user);
signalingMessageReceiver.processUsersInRoom(users);
InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2);
inOrder.verify(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList);
inOrder.verify(mockedParticipantListMessageListener2).onUsersInRoom(expectedParticipantList);
}
@Test
public void testExternalSignalingParticipantListMessageParticipantsUpdate() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
Map<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
List<Map<String, Object>> users = new ArrayList<>(2);
Map<String, Object> user1 = new HashMap<>();
user1.put("inCall", 7);
user1.put("lastPing", 4815);
user1.put("sessionId", "theSessionId1");
user1.put("participantType", 3);
user1.put("userId", "theUserId");
// If "nextcloudSessionId" or "participantPermissions" is set in any of the participants all the other
// participants in the message would have them too. But for test simplicity, and as it is not relevant for
// the processing, in this test they are included only in one of the participants.
user1.put("nextcloudSessionId", "theNextcloudSessionId");
user1.put("participantPermissions", 42);
users.add(user1);
Map<String, Object> user2 = new HashMap<>();
user2.put("inCall", 0);
user2.put("lastPing", 162342);
user2.put("sessionId", "theSessionId2");
user2.put("participantType", 4);
users.add(user2);
updateMap.put("users", users);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
List<Participant> expectedParticipantList = new ArrayList<>(2);
Participant expectedParticipant1 = new Participant();
expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO);
expectedParticipant1.setLastPing(4815);
expectedParticipant1.setSessionId("theSessionId1");
expectedParticipant1.setType(Participant.ParticipantType.USER);
expectedParticipant1.setUserId("theUserId");
expectedParticipantList.add(expectedParticipant1);
Participant expectedParticipant2 = new Participant();
expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED);
expectedParticipant2.setLastPing(162342);
expectedParticipant2.setSessionId("theSessionId2");
expectedParticipant2.setType(Participant.ParticipantType.GUEST);
expectedParticipantList.add(expectedParticipant2);
verify(mockedParticipantListMessageListener, only()).onParticipantsUpdate(expectedParticipantList);
}
@Test
public void testExternalSignalingParticipantListMessageAllParticipantsUpdate() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
Map<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
}
@Test
public void testExternalSignalingParticipantListMessageAfterRemovingListener() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
verifyNoInteractions(mockedParticipantListMessageListener);
}
@Test
public void testExternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener3);
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
verify(mockedParticipantListMessageListener3, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
verifyNoInteractions(mockedParticipantListMessageListener2);
}
@Test
public void testExternalSignalingParticipantListMessageAfterAddingListenerAgain() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
}
@Test
public void testAddParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
return null;
}).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
verifyNoInteractions(mockedParticipantListMessageListener2);
}
@Test
public void testRemoveParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() {
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 =
mock(SignalingMessageReceiver.ParticipantListMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2);
return null;
}).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener1);
signalingMessageReceiver.addListener(mockedParticipantListMessageListener2);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "update");
eventMap.put("target", "participants");
HashMap<String, Object> updateMap = new HashMap<>();
updateMap.put("roomId", 108);
updateMap.put("all", true);
updateMap.put("inCall", 0);
eventMap.put("update", updateMap);
signalingMessageReceiver.processEvent(eventMap);
InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2);
inOrder.verify(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
inOrder.verify(mockedParticipantListMessageListener2).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED);
}
}

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

View File

@ -0,0 +1,366 @@
/*
* 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.NCIceCandidate;
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 SignalingMessageReceiverWebRtcTest {
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 testAddWebRtcMessageListenerWithNullListener() {
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener(null, "theSessionId", "theRoomType");
});
}
@Test
public void testAddWebRtcMessageListenerWithNullSessionId() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, null, "theRoomType");
});
}
@Test
public void testAddWebRtcMessageListenerWithNullRoomType() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", null);
});
}
@Test
public void testWebRtcMessageOffer() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
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");
signalingMessage.setPayload(messagePayload);
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onOffer("theSdp", null);
}
@Test
public void testWebRtcMessageOfferWithNick() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
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(mockedWebRtcMessageListener, only()).onOffer("theSdp", "theNick");
}
@Test
public void testWebRtcMessageAnswer() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("answer");
signalingMessage.setRoomType("theRoomType");
NCMessagePayload messagePayload = new NCMessagePayload();
messagePayload.setType("answer");
messagePayload.setSdp("theSdp");
signalingMessage.setPayload(messagePayload);
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", null);
}
@Test
public void testWebRtcMessageAnswerWithNick() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("answer");
signalingMessage.setRoomType("theRoomType");
NCMessagePayload messagePayload = new NCMessagePayload();
messagePayload.setType("answer");
messagePayload.setSdp("theSdp");
messagePayload.setNick("theNick");
signalingMessage.setPayload(messagePayload);
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", "theNick");
}
@Test
public void testWebRtcMessageCandidate() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("candidate");
signalingMessage.setRoomType("theRoomType");
NCMessagePayload messagePayload = new NCMessagePayload();
NCIceCandidate iceCandidate = new NCIceCandidate();
iceCandidate.setSdpMid("theSdpMid");
iceCandidate.setSdpMLineIndex(42);
iceCandidate.setCandidate("theSdp");
messagePayload.setIceCandidate(iceCandidate);
signalingMessage.setPayload(messagePayload);
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onCandidate("theSdpMid", 42, "theSdp");
}
@Test
public void testWebRtcMessageEndOfCandidates() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onEndOfCandidates();
}
@Test
public void testWebRtcMessageSeveralListenersSameFrom() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType");
signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates();
verify(mockedWebRtcMessageListener2, only()).onEndOfCandidates();
}
@Test
public void testWebRtcMessageNotMatchingSessionId() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("notMatchingSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedWebRtcMessageListener);
}
@Test
public void testWebRtcMessageNotMatchingRoomType() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("notMatchingRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedWebRtcMessageListener);
}
@Test
public void testWebRtcMessageAfterRemovingListener() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
signalingMessageReceiver.removeListener(mockedWebRtcMessageListener);
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedWebRtcMessageListener);
}
@Test
public void testWebRtcMessageAfterRemovingSingleListenerOfSeveral() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener3 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType");
signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType");
signalingMessageReceiver.addListener(mockedWebRtcMessageListener3, "theSessionId", "theRoomType");
signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2);
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates();
verify(mockedWebRtcMessageListener3, only()).onEndOfCandidates();
verifyNoInteractions(mockedWebRtcMessageListener2);
}
@Test
public void testWebRtcMessageAfterAddingListenerAgainForDifferentFrom() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType");
signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId2", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verifyNoInteractions(mockedWebRtcMessageListener);
signalingMessage.setFrom("theSessionId2");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener, only()).onEndOfCandidates();
}
@Test
public void testAddWebRtcMessageListenerWhenHandlingWebRtcMessage() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType");
return null;
}).when(mockedWebRtcMessageListener1).onEndOfCandidates();
signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates();
verifyNoInteractions(mockedWebRtcMessageListener2);
}
@Test
public void testRemoveWebRtcMessageListenerWhenHandlingWebRtcMessage() {
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 =
mock(SignalingMessageReceiver.WebRtcMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2);
return null;
}).when(mockedWebRtcMessageListener1).onEndOfCandidates();
signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType");
signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("endOfCandidates");
signalingMessage.setRoomType("theRoomType");
signalingMessageReceiver.processSignalingMessage(signalingMessage);
InOrder inOrder = inOrder(mockedWebRtcMessageListener1, mockedWebRtcMessageListener2);
inOrder.verify(mockedWebRtcMessageListener1).onEndOfCandidates();
inOrder.verify(mockedWebRtcMessageListener2).onEndOfCandidates();
}
}