Merge pull request #2600 from nextcloud/split-call-participants-and-peer-connections

Split call participants and peer connections
This commit is contained in:
Marcel Hibbe 2023-01-16 10:49:25 +01:00 committed by GitHub
commit a87f2fb102
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2407 additions and 414 deletions

View file

@ -60,6 +60,9 @@ import com.nextcloud.talk.adapters.ParticipantDisplayItem;
import com.nextcloud.talk.adapters.ParticipantsAdapter; import com.nextcloud.talk.adapters.ParticipantsAdapter;
import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.call.CallParticipant;
import com.nextcloud.talk.call.CallParticipantList;
import com.nextcloud.talk.call.CallParticipantModel;
import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.databinding.CallActivityBinding; import com.nextcloud.talk.databinding.CallActivityBinding;
import com.nextcloud.talk.events.ConfigurationChangeEvent; import com.nextcloud.talk.events.ConfigurationChangeEvent;
@ -123,8 +126,8 @@ import org.webrtc.VideoTrack;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -231,7 +234,6 @@ public class CallActivity extends CallBaseActivity {
private MediaStream localStream; private MediaStream localStream;
private String credentials; private String credentials;
private List<PeerConnectionWrapper> peerConnectionWrapperList = new ArrayList<>(); private List<PeerConnectionWrapper> peerConnectionWrapperList = new ArrayList<>();
private Map<String, String> userIdsBySessionId = new HashMap<>();
private boolean videoOn = false; private boolean videoOn = false;
private boolean microphoneOn = false; private boolean microphoneOn = false;
@ -263,31 +265,30 @@ public class CallActivity extends CallBaseActivity {
private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners = private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
new HashMap<>(); new HashMap<>();
private Map<String, PeerConnectionWrapper.DataChannelMessageListener> dataChannelMessageListeners = new HashMap<>(); private PeerConnectionWrapper.PeerConnectionObserver selfPeerConnectionObserver = new CallActivitySelfPeerConnectionObserver();
private Map<String, PeerConnectionWrapper.PeerConnectionObserver> peerConnectionObservers = new HashMap<>(); private Map<String, CallParticipant> callParticipants = new HashMap<>();
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = new SignalingMessageReceiver.ParticipantListMessageListener() { private Map<String, ScreenParticipantDisplayItemManager> screenParticipantDisplayItemManagers = new HashMap<>();
private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper());
private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() {
@Override @Override
public void onUsersInRoom(List<Participant> participants) { public void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
processUsersInRoom(participants); Collection<Participant> left, Collection<Participant> unchanged) {
handleCallParticipantsChanged(joined, updated, left, unchanged);
} }
@Override @Override
public void onParticipantsUpdate(List<Participant> participants) { public void onCallEndedForAll() {
processUsersInRoom(participants);
}
@Override
public void onAllParticipantsUpdate(long inCall) {
if (inCall == Participant.InCallFlags.DISCONNECTED) {
Log.d(TAG, "A moderator ended the call for all."); Log.d(TAG, "A moderator ended the call for all.");
hangup(true); hangup(true);
} }
}
}; };
private CallParticipantList callParticipantList;
private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() { private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() {
@Override @Override
public void onOffer(String sessionId, String roomType, String sdp, String nick) { public void onOffer(String sessionId, String roomType, String sdp, String nick) {
@ -382,6 +383,7 @@ public class CallActivity extends CallBaseActivity {
requestBluetoothPermission(); requestBluetoothPermission();
} }
basicInitialization(); basicInitialization();
callParticipants = new HashMap<>();
participantDisplayItems = new HashMap<>(); participantDisplayItems = new HashMap<>();
initViews(); initViews();
if (!isConnectionEstablished()) { if (!isConnectionEstablished()) {
@ -740,6 +742,10 @@ public class CallActivity extends CallBaseActivity {
} }
}); });
if (participantsAdapter != null) {
participantsAdapter.destroy();
}
participantsAdapter = new ParticipantsAdapter( participantsAdapter = new ParticipantsAdapter(
this, this,
participantDisplayItems, participantDisplayItems,
@ -1235,7 +1241,6 @@ public class CallActivity extends CallBaseActivity {
@Override @Override
public void onDestroy() { public void onDestroy() {
signalingMessageReceiver.removeListener(participantListMessageListener);
signalingMessageReceiver.removeListener(offerMessageListener); signalingMessageReceiver.removeListener(offerMessageListener);
if (localStream != null) { if (localStream != null) {
@ -1369,7 +1374,6 @@ public class CallActivity extends CallBaseActivity {
setupAndInitiateWebSocketsConnection(); setupAndInitiateWebSocketsConnection();
} else { } else {
signalingMessageReceiver = internalSignalingMessageReceiver; signalingMessageReceiver = internalSignalingMessageReceiver;
signalingMessageReceiver.addListener(participantListMessageListener);
signalingMessageReceiver.addListener(offerMessageListener); signalingMessageReceiver.addListener(offerMessageListener);
signalingMessageSender = internalSignalingMessageSender; signalingMessageSender = internalSignalingMessageSender;
joinRoomAndCall(); joinRoomAndCall();
@ -1459,6 +1463,9 @@ public class CallActivity extends CallBaseActivity {
inCallFlag += Participant.InCallFlags.WITH_VIDEO; inCallFlag += Participant.InCallFlags.WITH_VIDEO;
} }
callParticipantList = new CallParticipantList(signalingMessageReceiver);
callParticipantList.addObserver(callParticipantListObserver);
int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1}); int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
ncApi.joinCall( ncApi.joinCall(
@ -1573,7 +1580,6 @@ public class CallActivity extends CallBaseActivity {
// Although setupAndInitiateWebSocketsConnection could be called several times the web socket is // Although setupAndInitiateWebSocketsConnection could be called several times the web socket is
// initialized just once, so the message receiver is also initialized just once. // initialized just once, so the message receiver is also initialized just once.
signalingMessageReceiver = webSocketClient.getSignalingMessageReceiver(); signalingMessageReceiver = webSocketClient.getSignalingMessageReceiver();
signalingMessageReceiver.addListener(participantListMessageListener);
signalingMessageReceiver.addListener(offerMessageListener); signalingMessageReceiver.addListener(offerMessageListener);
signalingMessageSender = webSocketClient.getSignalingMessageSender(); signalingMessageSender = webSocketClient.getSignalingMessageSender();
} else { } else {
@ -1715,12 +1721,21 @@ public class CallActivity extends CallBaseActivity {
} }
} }
List<String> sessionIdsToEnd = new ArrayList<String>(peerConnectionWrapperList.size()); List<String> peerConnectionIdsToEnd = new ArrayList<String>(peerConnectionWrapperList.size());
for (PeerConnectionWrapper wrapper : peerConnectionWrapperList) { for (PeerConnectionWrapper wrapper : peerConnectionWrapperList) {
sessionIdsToEnd.add(wrapper.getSessionId()); peerConnectionIdsToEnd.add(wrapper.getSessionId());
} }
for (String sessionId : sessionIdsToEnd) { for (String sessionId : peerConnectionIdsToEnd) {
endPeerConnection(sessionId, false); endPeerConnection(sessionId, "video");
endPeerConnection(sessionId, "screen");
}
List<String> callParticipantIdsToEnd = new ArrayList<String>(peerConnectionWrapperList.size());
for (CallParticipant callParticipant : callParticipants.values()) {
callParticipantIdsToEnd.add(callParticipant.getCallParticipantModel().getSessionId());
}
for (String sessionId : callParticipantIdsToEnd) {
removeCallParticipant(sessionId);
} }
hangupNetworkCalls(shutDownView); hangupNetworkCalls(shutDownView);
@ -1731,6 +1746,9 @@ public class CallActivity extends CallBaseActivity {
Log.d(TAG, "hangupNetworkCalls. shutDownView=" + shutDownView); Log.d(TAG, "hangupNetworkCalls. shutDownView=" + shutDownView);
int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1}); int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
callParticipantList.removeObserver(callParticipantListObserver);
callParticipantList.destroy();
ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken)) ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -1768,11 +1786,9 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private void processUsersInRoom(List<Participant> participants) { private void handleCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
Log.d(TAG, "processUsersInRoom"); Collection<Participant> left, Collection<Participant> unchanged) {
List<String> newSessions = new ArrayList<>(); Log.d(TAG, "handleCallParticipantsChanged");
Set<String> oldSessions = new HashSet<>();
userIdsBySessionId = new HashMap<>();
hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient.hasMCU(); hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient.hasMCU();
Log.d(TAG, " hasMCU is " + hasMCU); Log.d(TAG, " hasMCU is " + hasMCU);
@ -1785,58 +1801,49 @@ public class CallActivity extends CallBaseActivity {
Log.d(TAG, " currentSessionId is " + currentSessionId); Log.d(TAG, " currentSessionId is " + currentSessionId);
boolean isSelfInCall = false; List<Participant> participantsInCall = new ArrayList<>();
participantsInCall.addAll(joined);
participantsInCall.addAll(updated);
participantsInCall.addAll(unchanged);
for (Participant participant : participants) { boolean isSelfInCall = false;
Participant selfParticipant = null;
for (Participant participant : participantsInCall) {
long inCallFlag = participant.getInCall(); long inCallFlag = participant.getInCall();
if (!participant.getSessionId().equals(currentSessionId)) { if (!participant.getSessionId().equals(currentSessionId)) {
Log.d(TAG, " inCallFlag of participant " Log.d(TAG, " inCallFlag of participant "
+ participant.getSessionId().substring(0, 4) + participant.getSessionId().substring(0, 4)
+ " : " + " : "
+ inCallFlag); + inCallFlag);
boolean isInCall = inCallFlag != 0;
if (isInCall) {
newSessions.add(participant.getSessionId());
}
userIdsBySessionId.put(participant.getSessionId(), participant.getUserId());
} else { } else {
Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag); Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag);
isSelfInCall = inCallFlag != 0; isSelfInCall = inCallFlag != 0;
if (inCallFlag == 0 && currentCallStatus != CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) { selfParticipant = participant;
}
}
if (!isSelfInCall && currentCallStatus != CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
Log.d(TAG, "Most probably a moderator ended the call for all."); Log.d(TAG, "Most probably a moderator ended the call for all.");
hangup(true); hangup(true);
return; return;
} }
}
}
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
if (!peerConnectionWrapper.isMCUPublisher()) {
oldSessions.add(peerConnectionWrapper.getSessionId());
}
}
if (!isSelfInCall) { if (!isSelfInCall) {
Log.d(TAG, "Self not in call, disconnecting from all other sessions"); Log.d(TAG, "Self not in call, disconnecting from all other sessions");
for (String sessionId : oldSessions) { for (Participant participant : participantsInCall) {
Log.d(TAG, " oldSession that will be removed is: " + sessionId); String sessionId = participant.getSessionId();
endPeerConnection(sessionId, false); Log.d(TAG, " session that will be removed is: " + sessionId);
endPeerConnection(sessionId, "video");
endPeerConnection(sessionId, "screen");
removeCallParticipant(sessionId);
} }
return; return;
} }
// Calculate sessions that left the call
List<String> disconnectedSessions = new ArrayList<>(oldSessions);
disconnectedSessions.removeAll(newSessions);
// Calculate sessions that join the call
newSessions.removeAll(oldSessions);
if (currentCallStatus == CallStatus.LEAVING) { if (currentCallStatus == CallStatus.LEAVING) {
return; return;
} }
@ -1846,42 +1853,72 @@ public class CallActivity extends CallBaseActivity {
getOrCreatePeerConnectionWrapperForSessionIdAndType(webSocketClient.getSessionId(), VIDEO_STREAM_TYPE_VIDEO, true); getOrCreatePeerConnectionWrapperForSessionIdAndType(webSocketClient.getSessionId(), VIDEO_STREAM_TYPE_VIDEO, true);
} }
for (String sessionId : newSessions) { boolean selfJoined = false;
boolean selfParticipantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(selfParticipant);
for (Participant participant : joined) {
String sessionId = participant.getSessionId();
if (sessionId == null) {
Log.w(TAG, "Null sessionId for call participant, this should not happen: " + participant);
continue;
}
if (sessionId.equals(currentSessionId)) {
selfJoined = true;
continue;
}
Log.d(TAG, " newSession joined: " + sessionId); Log.d(TAG, " newSession joined: " + sessionId);
getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false);
String userId = userIdsBySessionId.get(sessionId); CallParticipant callParticipant = addCallParticipant(sessionId);
String userId = participant.getUserId();
if (userId != null) { if (userId != null) {
runOnUiThread(() -> { callParticipants.get(sessionId).setUserId(userId);
boolean notifyDataSetChanged = false;
if (participantDisplayItems.get(sessionId + "-video") != null) {
participantDisplayItems.get(sessionId + "-video").setUserId(userId);
notifyDataSetChanged = true;
} }
if (participantDisplayItems.get(sessionId + "-screen") != null) {
participantDisplayItems.get(sessionId + "-screen").setUserId(userId); String nick;
notifyDataSetChanged = true; if (hasExternalSignalingServer) {
nick = webSocketClient.getDisplayNameForSession(sessionId);
} else {
nick = offerAnswerNickProviders.get(sessionId) != null ? offerAnswerNickProviders.get(sessionId).getNick() : "";
} }
if (notifyDataSetChanged) { callParticipants.get(sessionId).setNick(nick);
participantsAdapter.notifyDataSetChanged();
} boolean participantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(participant);
});
// FIXME Without MCU, PeerConnectionWrapper only sends an offer if the local session ID is higher than the
// remote session ID. However, if the other participant does not have audio nor video that participant
// will not send an offer, so no connection is actually established when the remote participant has a
// higher session ID but is not publishing media.
if ((hasMCU && participantHasAudioOrVideo) ||
(!hasMCU && selfParticipantHasAudioOrVideo && (!participantHasAudioOrVideo || sessionId.compareTo(currentSessionId) < 0))) {
getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false);
} }
} }
if (newSessions.size() > 0 && currentCallStatus != CallStatus.IN_CONVERSATION) { boolean othersInCall = selfJoined ? joined.size() > 1 : joined.size() > 0;
if (othersInCall && currentCallStatus != CallStatus.IN_CONVERSATION) {
setCallState(CallStatus.IN_CONVERSATION); setCallState(CallStatus.IN_CONVERSATION);
} }
for (String sessionId : disconnectedSessions) { for (Participant participant : left) {
String sessionId = participant.getSessionId();
Log.d(TAG, " oldSession that will be removed is: " + sessionId); Log.d(TAG, " oldSession that will be removed is: " + sessionId);
endPeerConnection(sessionId, false); endPeerConnection(sessionId, "video");
endPeerConnection(sessionId, "screen");
removeCallParticipant(sessionId);
} }
} }
private void deletePeerConnection(PeerConnectionWrapper peerConnectionWrapper) { private boolean participantInCallFlagsHaveAudioOrVideo(Participant participant) {
peerConnectionWrapper.removePeerConnection(); if (participant == null) {
peerConnectionWrapperList.remove(peerConnectionWrapper); return false;
}
return (participant.getInCall() & Participant.InCallFlags.WITH_AUDIO) > 0 ||
(!isVoiceOnlyCall && (participant.getInCall() & Participant.InCallFlags.WITH_VIDEO) > 0);
} }
private PeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId, String type) { private PeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId, String type) {
@ -1965,46 +2002,22 @@ public class CallActivity extends CallBaseActivity {
peerConnectionWrapperList.add(peerConnectionWrapper); 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);
// DataChannel messages are sent only in video peers; (sender) screen peers do not even open them.
PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener =
new CallActivityDataChannelMessageListener(sessionId);
dataChannelMessageListeners.put(sessionId, dataChannelMessageListener);
peerConnectionWrapper.addListener(dataChannelMessageListener);
}
if (!publisher && !hasExternalSignalingServer && offerAnswerNickProviders.get(sessionId) == null) {
OfferAnswerNickProvider offerAnswerNickProvider = new OfferAnswerNickProvider(sessionId);
offerAnswerNickProviders.put(sessionId, offerAnswerNickProvider);
signalingMessageReceiver.addListener(offerAnswerNickProvider.getVideoWebRtcMessageListener(), sessionId, "video");
signalingMessageReceiver.addListener(offerAnswerNickProvider.getScreenWebRtcMessageListener(), sessionId, "screen");
}
PeerConnectionWrapper.PeerConnectionObserver peerConnectionObserver =
new CallActivityPeerConnectionObserver(sessionId, type);
peerConnectionObservers.put(sessionId + "-" + type, peerConnectionObserver);
peerConnectionWrapper.addObserver(peerConnectionObserver);
if (!publisher) { if (!publisher) {
runOnUiThread(() -> { CallParticipant callParticipant = callParticipants.get(sessionId);
// userId is unknown here, but it will be got based on the session id, and the stream will be if (callParticipant == null) {
// updated once it is added to the connection. callParticipant = addCallParticipant(sessionId);
setupVideoStreamForLayout( }
null,
sessionId, if ("screen".equals(type)) {
false, callParticipant.setScreenPeerConnectionWrapper(peerConnectionWrapper);
type); } else {
}); callParticipant.setPeerConnectionWrapper(peerConnectionWrapper);
}
} }
if (publisher) { if (publisher) {
peerConnectionWrapper.addObserver(selfPeerConnectionObserver);
startSendingNick(); startSendingNick();
} }
@ -2012,39 +2025,71 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private List<PeerConnectionWrapper> getPeerConnectionWrapperListForSessionId(String sessionId) { private CallParticipant addCallParticipant(String sessionId) {
List<PeerConnectionWrapper> internalList = new ArrayList<>(); CallParticipant callParticipant = new CallParticipant(sessionId);
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) { callParticipants.put(sessionId, callParticipant);
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
internalList.add(peerConnectionWrapper); SignalingMessageReceiver.CallParticipantMessageListener callParticipantMessageListener =
new CallActivityCallParticipantMessageListener(sessionId);
callParticipantMessageListeners.put(sessionId, callParticipantMessageListener);
signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId);
if (!hasExternalSignalingServer) {
OfferAnswerNickProvider offerAnswerNickProvider = new OfferAnswerNickProvider(sessionId);
offerAnswerNickProviders.put(sessionId, offerAnswerNickProvider);
signalingMessageReceiver.addListener(offerAnswerNickProvider.getVideoWebRtcMessageListener(), sessionId, "video");
signalingMessageReceiver.addListener(offerAnswerNickProvider.getScreenWebRtcMessageListener(), sessionId, "screen");
}
final CallParticipantModel callParticipantModel = callParticipant.getCallParticipantModel();
ScreenParticipantDisplayItemManager screenParticipantDisplayItemManager =
new ScreenParticipantDisplayItemManager(callParticipantModel);
screenParticipantDisplayItemManagers.put(sessionId, screenParticipantDisplayItemManager);
callParticipantModel.addObserver(screenParticipantDisplayItemManager, screenParticipantDisplayItemManagersHandler);
runOnUiThread(() -> {
addParticipantDisplayItem(callParticipantModel, "video");
});
return callParticipant;
}
private void endPeerConnection(String sessionId, String type) {
PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type);
if (peerConnectionWrapper == null) {
return;
}
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
peerConnectionWrapper.removeObserver(selfPeerConnectionObserver);
}
CallParticipant callParticipant = callParticipants.get(sessionId);
if (callParticipant != null) {
if ("screen".equals(type)) {
callParticipant.setScreenPeerConnectionWrapper(null);
} else {
callParticipant.setPeerConnectionWrapper(null);
} }
} }
return internalList; peerConnectionWrapper.removePeerConnection();
peerConnectionWrapperList.remove(peerConnectionWrapper);
} }
private void endPeerConnection(String sessionId, boolean justScreen) { private void removeCallParticipant(String sessionId) {
List<PeerConnectionWrapper> peerConnectionWrappers; CallParticipant callParticipant = callParticipants.remove(sessionId);
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) { if (callParticipant == null) {
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) { return;
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
if (!justScreen && VIDEO_STREAM_TYPE_VIDEO.equals(peerConnectionWrapper.getVideoStreamType())) {
PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener = dataChannelMessageListeners.remove(sessionId);
peerConnectionWrapper.removeListener(dataChannelMessageListener);
}
String videoStreamType = peerConnectionWrapper.getVideoStreamType();
if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) {
PeerConnectionWrapper.PeerConnectionObserver peerConnectionObserver = peerConnectionObservers.remove(sessionId + "-" + videoStreamType);
peerConnectionWrapper.removeObserver(peerConnectionObserver);
runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
deletePeerConnection(peerConnectionWrapper);
}
}
}
} }
if (!justScreen) { ScreenParticipantDisplayItemManager screenParticipantDisplayItemManager =
screenParticipantDisplayItemManagers.remove(sessionId);
callParticipant.getCallParticipantModel().removeObserver(screenParticipantDisplayItemManager);
callParticipant.destroy();
SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId); SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
signalingMessageReceiver.removeListener(listener); signalingMessageReceiver.removeListener(listener);
@ -2053,12 +2098,18 @@ public class CallActivity extends CallBaseActivity {
signalingMessageReceiver.removeListener(offerAnswerNickProvider.getVideoWebRtcMessageListener()); signalingMessageReceiver.removeListener(offerAnswerNickProvider.getVideoWebRtcMessageListener());
signalingMessageReceiver.removeListener(offerAnswerNickProvider.getScreenWebRtcMessageListener()); signalingMessageReceiver.removeListener(offerAnswerNickProvider.getScreenWebRtcMessageListener());
} }
}
runOnUiThread(() -> removeParticipantDisplayItem(sessionId, "video"));
} }
private void removeMediaStream(String sessionId, String videoStreamType) { private void removeParticipantDisplayItem(String sessionId, String videoStreamType) {
Log.d(TAG, "removeMediaStream"); Log.d(TAG, "removeParticipantDisplayItem");
participantDisplayItems.remove(sessionId + "-" + videoStreamType); ParticipantDisplayItem participantDisplayItem = participantDisplayItems.remove(sessionId + "-" + videoStreamType);
if (participantDisplayItem == null) {
return;
}
participantDisplayItem.destroy();
if (!isDestroyed()) { if (!isDestroyed()) {
initGridAdapter(); initGridAdapter();
@ -2072,7 +2123,10 @@ public class CallActivity extends CallBaseActivity {
updateSelfVideoViewPosition(); updateSelfVideoViewPosition();
} }
private void updateSelfVideoViewConnected(boolean connected) { private void updateSelfVideoViewIceConnectionState(PeerConnection.IceConnectionState iceConnectionState) {
boolean connected = iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
// FIXME In voice only calls there is no video view, so the progress bar would appear floating in the middle of // FIXME In voice only calls there is no video view, so the progress bar would appear floating in the middle of
// nowhere. However, a way to signal that the local participant is not connected to the HPB is still need in // nowhere. However, a way to signal that the local participant is not connected to the HPB is still need in
// that case. // that case.
@ -2133,28 +2187,6 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private void handlePeerConnected(String sessionId, String videoStreamType) {
String participantDisplayItemId = sessionId + "-" + videoStreamType;
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
updateSelfVideoViewConnected(true);
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setConnected(true);
participantsAdapter.notifyDataSetChanged();
}
}
private void handlePeerDisconnected(String sessionId, String videoStreamType) {
String participantDisplayItemId = sessionId + "-" + videoStreamType;
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
updateSelfVideoViewConnected(false);
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setConnected(false);
participantsAdapter.notifyDataSetChanged();
}
}
private void startSendingNick() { private void startSendingNick() {
DataChannelMessage dataChannelMessage = new DataChannelMessage(); DataChannelMessage dataChannelMessage = new DataChannelMessage();
dataChannelMessage.setType("nickChanged"); dataChannelMessage.setType("nickChanged");
@ -2204,42 +2236,16 @@ public class CallActivity extends CallBaseActivity {
this); this);
} }
private void setupVideoStreamForLayout(@Nullable MediaStream mediaStream, private void addParticipantDisplayItem(CallParticipantModel callParticipantModel, String videoStreamType) {
String session,
boolean videoStreamEnabled,
String videoStreamType) {
PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(session,
videoStreamType);
boolean connected = false;
if (peerConnectionWrapper != null) {
PeerConnection.IceConnectionState iceConnectionState = peerConnectionWrapper.getPeerConnection().iceConnectionState();
connected = iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
}
String nick;
if (hasExternalSignalingServer) {
nick = webSocketClient.getDisplayNameForSession(session);
} else {
nick = offerAnswerNickProviders.get(session) != null ? offerAnswerNickProviders.get(session).getNick() : "";
}
String userId = userIdsBySessionId.get(session);
String defaultGuestNick = getResources().getString(R.string.nc_nick_guest); String defaultGuestNick = getResources().getString(R.string.nc_nick_guest);
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl, ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
userId,
session,
connected,
nick,
defaultGuestNick, defaultGuestNick,
mediaStream, rootEglBase,
videoStreamType, videoStreamType,
videoStreamEnabled, callParticipantModel);
rootEglBase); String sessionId = callParticipantModel.getSessionId();
participantDisplayItems.put(session + "-" + videoStreamType, participantDisplayItem); participantDisplayItems.put(sessionId + "-" + videoStreamType, participantDisplayItem);
initGridAdapter(); initGridAdapter();
} }
@ -2548,17 +2554,8 @@ public class CallActivity extends CallBaseActivity {
private void onOfferOrAnswer(String nick) { private void onOfferOrAnswer(String nick) {
this.nick = nick; this.nick = nick;
boolean notifyDataSetChanged = false; if (callParticipants.get(sessionId) != null) {
if (participantDisplayItems.get(sessionId + "-video") != null) { callParticipants.get(sessionId).setNick(nick);
participantDisplayItems.get(sessionId + "-video").setNick(nick);
notifyDataSetChanged = true;
}
if (participantDisplayItems.get(sessionId + "-screen") != null) {
participantDisplayItems.get(sessionId + "-screen").setNick(nick);
notifyDataSetChanged = true;
}
if (notifyDataSetChanged) {
participantsAdapter.notifyDataSetChanged();
} }
} }
@ -2585,146 +2582,55 @@ public class CallActivity extends CallBaseActivity {
@Override @Override
public void onUnshareScreen() { public void onUnshareScreen() {
endPeerConnection(sessionId, true); endPeerConnection(sessionId, "screen");
} }
} }
private class CallActivityDataChannelMessageListener implements PeerConnectionWrapper.DataChannelMessageListener { private class CallActivitySelfPeerConnectionObserver implements PeerConnectionWrapper.PeerConnectionObserver {
private final String participantDisplayItemId;
private CallActivityDataChannelMessageListener(String sessionId) {
// DataChannel messages are sent only in video peers, so the listener only acts on the "video" items.
this.participantDisplayItemId = sessionId + "-video";
}
@Override
public void onAudioOn() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(true);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onAudioOff() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(false);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onVideoOn() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(true);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onVideoOff() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(false);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onNickChanged(String nick) {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setNick(nick);
participantsAdapter.notifyDataSetChanged();
}
});
}
}
private class CallActivityPeerConnectionObserver implements PeerConnectionWrapper.PeerConnectionObserver {
private final String sessionId;
private final String videoStreamType;
private final String participantDisplayItemId;
private CallActivityPeerConnectionObserver(String sessionId, String videoStreamType) {
this.sessionId = sessionId;
this.videoStreamType = videoStreamType;
this.participantDisplayItemId = sessionId + "-" + videoStreamType;
}
@Override @Override
public void onStreamAdded(MediaStream mediaStream) { public void onStreamAdded(MediaStream mediaStream) {
handleStream(mediaStream);
} }
@Override @Override
public void onStreamRemoved(MediaStream mediaStream) { public void onStreamRemoved(MediaStream mediaStream) {
handleStream(null);
}
private void handleStream(MediaStream mediaStream) {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) == null) {
return;
}
boolean hasAtLeastOneVideoStream = false;
if (mediaStream != null) {
hasAtLeastOneVideoStream = mediaStream.videoTracks != null && mediaStream.videoTracks.size() > 0;
}
ParticipantDisplayItem participantDisplayItem = participantDisplayItems.get(participantDisplayItemId);
participantDisplayItem.setMediaStream(mediaStream);
participantDisplayItem.setStreamEnabled(hasAtLeastOneVideoStream);
participantsAdapter.notifyDataSetChanged();
});
} }
@Override @Override
public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) { public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) {
runOnUiThread(() -> { runOnUiThread(() -> {
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED || updateSelfVideoViewIceConnectionState(iceConnectionState);
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) {
handlePeerConnected(sessionId, videoStreamType);
return;
}
if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED ||
iceConnectionState == PeerConnection.IceConnectionState.NEW ||
iceConnectionState == PeerConnection.IceConnectionState.CHECKING) {
handlePeerDisconnected(sessionId, videoStreamType);
return;
}
if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
endPeerConnection(sessionId, VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType));
return;
}
if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) { if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
setCallState(CallStatus.PUBLISHER_FAILED); setCallState(CallStatus.PUBLISHER_FAILED);
webSocketClient.clearResumeId(); webSocketClient.clearResumeId();
hangup(false); hangup(false);
} else {
handlePeerDisconnected(sessionId, videoStreamType);
} }
});
}
}
private class ScreenParticipantDisplayItemManager implements CallParticipantModel.Observer {
private final CallParticipantModel callParticipantModel;
private ScreenParticipantDisplayItemManager(CallParticipantModel callParticipantModel) {
this.callParticipantModel = callParticipantModel;
}
@Override
public void onChange() {
String sessionId = callParticipantModel.getSessionId();
if (callParticipantModel.getScreenIceConnectionState() == null) {
removeParticipantDisplayItem(sessionId, "screen");
return; return;
} }
});
boolean hasScreenParticipantDisplayItem = participantDisplayItems.get(sessionId + "-screen") != null;
if (!hasScreenParticipantDisplayItem) {
addParticipantDisplayItem(callParticipantModel, "screen");
}
} }
} }

View file

@ -1,83 +1,88 @@
package com.nextcloud.talk.adapters; package com.nextcloud.talk.adapters;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import com.nextcloud.talk.call.CallParticipantModel;
import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.ApiUtils;
import org.webrtc.EglBase; import org.webrtc.EglBase;
import org.webrtc.MediaStream; import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
public class ParticipantDisplayItem { public class ParticipantDisplayItem {
private String baseUrl;
private String userId; public interface Observer {
private String session; void onChange();
private boolean connected; }
private String nick;
/**
* Shared handler to receive change notifications from the model on the main thread.
*/
private static final Handler handler = new Handler(Looper.getMainLooper());
private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier();
private final String baseUrl;
private final String defaultGuestNick; private final String defaultGuestNick;
private final EglBase rootEglBase;
private final String session;
private final String streamType;
private final CallParticipantModel callParticipantModel;
private final CallParticipantModel.Observer callParticipantModelObserver = this::updateFromModel;
private String userId;
private PeerConnection.IceConnectionState iceConnectionState;
private String nick;
private String urlForAvatar; private String urlForAvatar;
private MediaStream mediaStream; private MediaStream mediaStream;
private String streamType;
private boolean streamEnabled; private boolean streamEnabled;
private EglBase rootEglBase;
private boolean isAudioEnabled; private boolean isAudioEnabled;
public ParticipantDisplayItem(String baseUrl, String userId, String session, boolean connected, String nick, String defaultGuestNick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) { public ParticipantDisplayItem(String baseUrl, String defaultGuestNick, EglBase rootEglBase, String streamType,
CallParticipantModel callParticipantModel) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.userId = userId;
this.session = session;
this.connected = connected;
this.nick = nick;
this.defaultGuestNick = defaultGuestNick; this.defaultGuestNick = defaultGuestNick;
this.mediaStream = mediaStream;
this.streamType = streamType;
this.streamEnabled = streamEnabled;
this.rootEglBase = rootEglBase; this.rootEglBase = rootEglBase;
this.updateUrlForAvatar(); this.session = callParticipantModel.getSessionId();
this.streamType = streamType;
this.callParticipantModel = callParticipantModel;
this.callParticipantModel.addObserver(callParticipantModelObserver, handler);
updateFromModel();
} }
public String getUserId() { public void destroy() {
return userId; this.callParticipantModel.removeObserver(callParticipantModelObserver);
} }
public void setUserId(String userId) { private void updateFromModel() {
this.userId = userId; userId = callParticipantModel.getUserId();
nick = callParticipantModel.getNick();
this.updateUrlForAvatar(); this.updateUrlForAvatar();
if ("screen".equals(streamType)) {
iceConnectionState = callParticipantModel.getScreenIceConnectionState();
mediaStream = callParticipantModel.getScreenMediaStream();
isAudioEnabled = true;
streamEnabled = true;
} else {
iceConnectionState = callParticipantModel.getIceConnectionState();
mediaStream = callParticipantModel.getMediaStream();
isAudioEnabled = callParticipantModel.isAudioAvailable() != null ?
callParticipantModel.isAudioAvailable() : false;
streamEnabled = callParticipantModel.isVideoAvailable() != null ?
callParticipantModel.isVideoAvailable() : false;
} }
public String getSession() { participantDisplayItemNotifier.notifyChange();
return session;
}
public void setSession(String session) {
this.session = session;
}
public boolean isConnected() {
return connected;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
public String getNick() {
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
return defaultGuestNick;
}
return nick;
}
public void setNick(String nick) {
this.nick = nick;
this.updateUrlForAvatar();
}
public String getUrlForAvatar() {
return urlForAvatar;
} }
private void updateUrlForAvatar() { private void updateUrlForAvatar() {
@ -88,44 +93,48 @@ public class ParticipantDisplayItem {
} }
} }
public boolean isConnected() {
return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED ||
// If there is no connection state that means that no connection is needed, so it is a special case that is
// also seen as "connected".
iceConnectionState == null;
}
public String getNick() {
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
return defaultGuestNick;
}
return nick;
}
public String getUrlForAvatar() {
return urlForAvatar;
}
public MediaStream getMediaStream() { public MediaStream getMediaStream() {
return mediaStream; return mediaStream;
} }
public void setMediaStream(MediaStream mediaStream) {
this.mediaStream = mediaStream;
}
public String getStreamType() {
return streamType;
}
public void setStreamType(String streamType) {
this.streamType = streamType;
}
public boolean isStreamEnabled() { public boolean isStreamEnabled() {
return streamEnabled; return streamEnabled;
} }
public void setStreamEnabled(boolean streamEnabled) {
this.streamEnabled = streamEnabled;
}
public EglBase getRootEglBase() { public EglBase getRootEglBase() {
return rootEglBase; return rootEglBase;
} }
public void setRootEglBase(EglBase rootEglBase) {
this.rootEglBase = rootEglBase;
}
public boolean isAudioEnabled() { public boolean isAudioEnabled() {
return isAudioEnabled; return isAudioEnabled;
} }
public void setAudioEnabled(boolean audioEnabled) { public void addObserver(Observer observer) {
isAudioEnabled = audioEnabled; participantDisplayItemNotifier.addObserver(observer);
}
public void removeObserver(Observer observer) {
participantDisplayItemNotifier.removeObserver(observer);
} }
@Override @Override

View file

@ -0,0 +1,55 @@
/*
* 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.adapters;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Helper class to register and notify ParticipantDisplayItem.Observers.
*
* This class is only meant for internal use by ParticipantDisplayItem; observers must register themselves against a
* ParticipantDisplayItem rather than against a ParticipantDisplayItemNotifier.
*/
class ParticipantDisplayItemNotifier {
private final Set<ParticipantDisplayItem.Observer> participantDisplayItemObservers = new LinkedHashSet<>();
public synchronized void addObserver(ParticipantDisplayItem.Observer observer) {
if (observer == null) {
throw new IllegalArgumentException("ParticipantDisplayItem.Observer can not be null");
}
participantDisplayItemObservers.add(observer);
}
public synchronized void removeObserver(ParticipantDisplayItem.Observer observer) {
participantDisplayItemObservers.remove(observer);
}
public synchronized void notifyChange() {
for (ParticipantDisplayItem.Observer observer : new ArrayList<>(participantDisplayItemObservers)) {
observer.onChange();
}
}
}

View file

@ -29,6 +29,8 @@ public class ParticipantsAdapter extends BaseAdapter {
private static final String TAG = "ParticipantsAdapter"; private static final String TAG = "ParticipantsAdapter";
private final ParticipantDisplayItem.Observer participantDisplayItemObserver = this::notifyDataSetChanged;
private final Context mContext; private final Context mContext;
private final ArrayList<ParticipantDisplayItem> participantDisplayItems; private final ArrayList<ParticipantDisplayItem> participantDisplayItems;
private final RelativeLayout gridViewWrapper; private final RelativeLayout gridViewWrapper;
@ -50,8 +52,17 @@ public class ParticipantsAdapter extends BaseAdapter {
this.participantDisplayItems = new ArrayList<>(); this.participantDisplayItems = new ArrayList<>();
this.participantDisplayItems.addAll(participantDisplayItems.values()); this.participantDisplayItems.addAll(participantDisplayItems.values());
for (ParticipantDisplayItem participantDisplayItem : this.participantDisplayItems) {
participantDisplayItem.addObserver(participantDisplayItemObserver);
}
} }
public void destroy() {
for (ParticipantDisplayItem participantDisplayItem : participantDisplayItems) {
participantDisplayItem.removeObserver(participantDisplayItemObserver);
}
}
@Override @Override
public int getCount() { public int getCount() {

View file

@ -0,0 +1,198 @@
/*
* 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.call;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
/**
* Model for (remote) call participants.
*
* This class keeps track of the state changes in a call participant and updates its data model as needed. View classes
* are expected to directly use the read-only data model.
*/
public class CallParticipant {
private final PeerConnectionWrapper.PeerConnectionObserver peerConnectionObserver =
new PeerConnectionWrapper.PeerConnectionObserver() {
@Override
public void onStreamAdded(MediaStream mediaStream) {
handleStreamChange(mediaStream);
}
@Override
public void onStreamRemoved(MediaStream mediaStream) {
handleStreamChange(mediaStream);
}
@Override
public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) {
handleIceConnectionStateChange(iceConnectionState);
}
};
private final PeerConnectionWrapper.PeerConnectionObserver screenPeerConnectionObserver =
new PeerConnectionWrapper.PeerConnectionObserver() {
@Override
public void onStreamAdded(MediaStream mediaStream) {
callParticipantModel.setScreenMediaStream(mediaStream);
}
@Override
public void onStreamRemoved(MediaStream mediaStream) {
callParticipantModel.setScreenMediaStream(null);
}
@Override
public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) {
callParticipantModel.setScreenIceConnectionState(iceConnectionState);
}
};
// DataChannel messages are sent only in video peers; (sender) screen peers do not even open them.
private final PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener =
new PeerConnectionWrapper.DataChannelMessageListener() {
@Override
public void onAudioOn() {
callParticipantModel.setAudioAvailable(Boolean.TRUE);
}
@Override
public void onAudioOff() {
callParticipantModel.setAudioAvailable(Boolean.FALSE);
}
@Override
public void onVideoOn() {
callParticipantModel.setVideoAvailable(Boolean.TRUE);
}
@Override
public void onVideoOff() {
callParticipantModel.setVideoAvailable(Boolean.FALSE);
}
@Override
public void onNickChanged(String nick) {
callParticipantModel.setNick(nick);
}
};
private final MutableCallParticipantModel callParticipantModel;
private PeerConnectionWrapper peerConnectionWrapper;
private PeerConnectionWrapper screenPeerConnectionWrapper;
public CallParticipant(String sessionId) {
callParticipantModel = new MutableCallParticipantModel(sessionId);
}
public void destroy() {
if (peerConnectionWrapper != null) {
peerConnectionWrapper.removeObserver(peerConnectionObserver);
peerConnectionWrapper.removeListener(dataChannelMessageListener);
}
if (screenPeerConnectionWrapper != null) {
screenPeerConnectionWrapper.removeObserver(screenPeerConnectionObserver);
}
}
public CallParticipantModel getCallParticipantModel() {
return callParticipantModel;
}
public void setUserId(String userId) {
callParticipantModel.setUserId(userId);
}
public void setNick(String nick) {
callParticipantModel.setNick(nick);
}
public void setPeerConnectionWrapper(PeerConnectionWrapper peerConnectionWrapper) {
if (this.peerConnectionWrapper != null) {
this.peerConnectionWrapper.removeObserver(peerConnectionObserver);
this.peerConnectionWrapper.removeListener(dataChannelMessageListener);
}
this.peerConnectionWrapper = peerConnectionWrapper;
if (this.peerConnectionWrapper == null) {
callParticipantModel.setIceConnectionState(null);
callParticipantModel.setMediaStream(null);
callParticipantModel.setAudioAvailable(null);
callParticipantModel.setVideoAvailable(null);
return;
}
handleIceConnectionStateChange(this.peerConnectionWrapper.getPeerConnection().iceConnectionState());
handleStreamChange(this.peerConnectionWrapper.getStream());
this.peerConnectionWrapper.addObserver(peerConnectionObserver);
this.peerConnectionWrapper.addListener(dataChannelMessageListener);
}
private void handleIceConnectionStateChange(PeerConnection.IceConnectionState iceConnectionState) {
callParticipantModel.setIceConnectionState(iceConnectionState);
if (iceConnectionState == PeerConnection.IceConnectionState.NEW ||
iceConnectionState == PeerConnection.IceConnectionState.CHECKING) {
callParticipantModel.setAudioAvailable(null);
callParticipantModel.setVideoAvailable(null);
}
}
private void handleStreamChange(MediaStream mediaStream) {
if (mediaStream == null) {
callParticipantModel.setMediaStream(null);
callParticipantModel.setVideoAvailable(Boolean.FALSE);
return;
}
boolean hasAtLeastOneVideoStream = mediaStream.videoTracks != null && !mediaStream.videoTracks.isEmpty();
callParticipantModel.setMediaStream(mediaStream);
callParticipantModel.setVideoAvailable(hasAtLeastOneVideoStream);
}
public void setScreenPeerConnectionWrapper(PeerConnectionWrapper screenPeerConnectionWrapper) {
if (this.screenPeerConnectionWrapper != null) {
this.screenPeerConnectionWrapper.removeObserver(screenPeerConnectionObserver);
}
this.screenPeerConnectionWrapper = screenPeerConnectionWrapper;
if (this.screenPeerConnectionWrapper == null) {
callParticipantModel.setScreenIceConnectionState(null);
callParticipantModel.setScreenMediaStream(null);
return;
}
callParticipantModel.setScreenIceConnectionState(this.screenPeerConnectionWrapper.getPeerConnection().iceConnectionState());
callParticipantModel.setScreenMediaStream(this.screenPeerConnectionWrapper.getStream());
this.screenPeerConnectionWrapper.addObserver(screenPeerConnectionObserver);
}
}

View file

@ -0,0 +1,164 @@
/*
* 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.call;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class to keep track of the participants in a call based on the signaling messages.
*
* The CallParticipantList adds a listener for participant list messages as soon as it is created and starts tracking
* the call participants until destroyed. Notifications about the changes can be received by adding an observer to the
* CallParticipantList; note that no sorting is guaranteed on the participants.
*/
public class CallParticipantList {
public interface Observer {
void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
Collection<Participant> left, Collection<Participant> unchanged);
void onCallEndedForAll();
}
private final SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener =
new SignalingMessageReceiver.ParticipantListMessageListener() {
private final Map<String, Participant> callParticipants = new HashMap<>();
@Override
public void onUsersInRoom(List<Participant> participants) {
processParticipantList(participants);
}
@Override
public void onParticipantsUpdate(List<Participant> participants) {
processParticipantList(participants);
}
private void processParticipantList(List<Participant> participants) {
Collection<Participant> joined = new ArrayList<>();
Collection<Participant> updated = new ArrayList<>();
Collection<Participant> left = new ArrayList<>();
Collection<Participant> unchanged = new ArrayList<>();
Collection<Participant> knownCallParticipantsNotFound = new ArrayList<>(callParticipants.values());
for (Participant participant : participants) {
String sessionId = participant.getSessionId();
Participant callParticipant = callParticipants.get(sessionId);
boolean knownCallParticipant = callParticipant != null;
if (!knownCallParticipant && participant.getInCall() != Participant.InCallFlags.DISCONNECTED) {
callParticipants.put(sessionId, copyParticipant(participant));
joined.add(copyParticipant(participant));
} else if (knownCallParticipant && participant.getInCall() == Participant.InCallFlags.DISCONNECTED) {
callParticipants.remove(sessionId);
// No need to copy it, as it will be no longer used.
callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
left.add(callParticipant);
} else if (knownCallParticipant && callParticipant.getInCall() != participant.getInCall()) {
callParticipant.setInCall(participant.getInCall());
updated.add(copyParticipant(participant));
} else if (knownCallParticipant) {
unchanged.add(copyParticipant(participant));
}
if (knownCallParticipant) {
knownCallParticipantsNotFound.remove(callParticipant);
}
}
for (Participant callParticipant : knownCallParticipantsNotFound) {
callParticipants.remove(callParticipant.getSessionId());
// No need to copy it, as it will be no longer used.
callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
left.add(callParticipant);
}
if (!joined.isEmpty() || !updated.isEmpty() || !left.isEmpty()) {
callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged);
}
}
@Override
public void onAllParticipantsUpdate(long inCall) {
if (inCall != Participant.InCallFlags.DISCONNECTED) {
// Updating all participants is expected to happen only to disconnect them.
return;
}
callParticipantListNotifier.notifyCallEndedForAll();
Collection<Participant> joined = new ArrayList<>();
Collection<Participant> updated = new ArrayList<>();
Collection<Participant> left = new ArrayList<>(callParticipants.size());
Collection<Participant> unchanged = new ArrayList<>();
for (Participant callParticipant : callParticipants.values()) {
// No need to copy it, as it will be no longer used.
callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED);
left.add(callParticipant);
}
callParticipants.clear();
if (!left.isEmpty()) {
callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged);
}
}
private Participant copyParticipant(Participant participant) {
Participant copiedParticipant = new Participant();
copiedParticipant.setInCall(participant.getInCall());
copiedParticipant.setLastPing(participant.getLastPing());
copiedParticipant.setSessionId(participant.getSessionId());
copiedParticipant.setType(participant.getType());
copiedParticipant.setUserId(participant.getUserId());
return copiedParticipant;
}
};
private final CallParticipantListNotifier callParticipantListNotifier = new CallParticipantListNotifier();
private final SignalingMessageReceiver signalingMessageReceiver;
public CallParticipantList(SignalingMessageReceiver signalingMessageReceiver) {
this.signalingMessageReceiver = signalingMessageReceiver;
this.signalingMessageReceiver.addListener(participantListMessageListener);
}
public void destroy() {
signalingMessageReceiver.removeListener(participantListMessageListener);
}
public void addObserver(Observer observer) {
callParticipantListNotifier.addObserver(observer);
}
public void removeObserver(Observer observer) {
callParticipantListNotifier.removeObserver(observer);
}
}

View file

@ -0,0 +1,63 @@
/*
* 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.call;
import com.nextcloud.talk.models.json.participants.Participant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Helper class to register and notify CallParticipantList.Observers.
*
* This class is only meant for internal use by CallParticipantList; listeners must register themselves against
* a CallParticipantList rather than against a CallParticipantListNotifier.
*/
class CallParticipantListNotifier {
private final Set<CallParticipantList.Observer> callParticipantListObservers = new LinkedHashSet<>();
public synchronized void addObserver(CallParticipantList.Observer observer) {
if (observer == null) {
throw new IllegalArgumentException("CallParticipantList.Observer can not be null");
}
callParticipantListObservers.add(observer);
}
public synchronized void removeObserver(CallParticipantList.Observer observer) {
callParticipantListObservers.remove(observer);
}
public synchronized void notifyChanged(Collection<Participant> joined, Collection<Participant> updated,
Collection<Participant> left, Collection<Participant> unchanged) {
for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) {
observer.onCallParticipantsChanged(joined, updated, left, unchanged);
}
}
public synchronized void notifyCallEndedForAll() {
for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) {
observer.onCallEndedForAll();
}
}
}

View file

@ -0,0 +1,164 @@
/*
* 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.call;
import android.os.Handler;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import java.util.Objects;
/**
* Read-only data model for (remote) call participants.
*
* The received audio and video are available only if the participant is sending them and also has them enabled.
* Before a connection is established it is not known whether audio and video are available or not, so null is returned
* in that case (therefore it should not be autoboxed to a plain boolean without checking that).
*
* Audio and video in screen shares, on the other hand, are always seen as available.
*
* Clients of the model can observe it with CallParticipantModel.Observer to be notified when any value changes.
* Getters called after receiving a notification are guaranteed to provide at least the value that triggered the
* notification, but it may return even a more up to date one (so getting the value again on the following
* notification may return the same value as before).
*/
public class CallParticipantModel {
public interface Observer {
void onChange();
}
protected class Data<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
if (Objects.equals(this.value, value)) {
return;
}
this.value = value;
callParticipantModelNotifier.notifyChange();
}
}
private final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier();
protected final String sessionId;
protected Data<String> userId;
protected Data<String> nick;
protected Data<PeerConnection.IceConnectionState> iceConnectionState;
protected Data<MediaStream> mediaStream;
protected Data<Boolean> audioAvailable;
protected Data<Boolean> videoAvailable;
protected Data<PeerConnection.IceConnectionState> screenIceConnectionState;
protected Data<MediaStream> screenMediaStream;
public CallParticipantModel(String sessionId) {
this.sessionId = sessionId;
this.userId = new Data<>();
this.nick = new Data<>();
this.iceConnectionState = new Data<>();
this.mediaStream = new Data<>();
this.audioAvailable = new Data<>();
this.videoAvailable = new Data<>();
this.screenIceConnectionState = new Data<>();
this.screenMediaStream = new Data<>();
}
public String getSessionId() {
return sessionId;
}
public String getUserId() {
return userId.getValue();
}
public String getNick() {
return nick.getValue();
}
public PeerConnection.IceConnectionState getIceConnectionState() {
return iceConnectionState.getValue();
}
public MediaStream getMediaStream() {
return mediaStream.getValue();
}
public Boolean isAudioAvailable() {
return audioAvailable.getValue();
}
public Boolean isVideoAvailable() {
return videoAvailable.getValue();
}
public PeerConnection.IceConnectionState getScreenIceConnectionState() {
return screenIceConnectionState.getValue();
}
public MediaStream getScreenMediaStream() {
return screenMediaStream.getValue();
}
/**
* Adds an Observer to be notified when any value changes.
*
* @param observer the Observer
* @see CallParticipantModel#addObserver(Observer, Handler)
*/
public void addObserver(Observer observer) {
addObserver(observer, null);
}
/**
* Adds an observer to be notified when any value changes.
*
* The observer will be notified on the thread associated to the given handler. If no handler is given the
* observer will be immediately notified on the same thread that changed the value; the observer will be
* immediately notified too if the thread of the handler is the same thread that changed the value.
*
* An observer is expected to be added only once. If the same observer is added again it will be notified just
* once on the thread of the last handler.
*
* @param observer the Observer
* @param handler a Handler for the thread to be notified on
*/
public void addObserver(Observer observer, Handler handler) {
callParticipantModelNotifier.addObserver(observer, handler);
}
public void removeObserver(Observer observer) {
callParticipantModelNotifier.removeObserver(observer);
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.call;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Helper class to register and notify CallParticipantModel.Observers.
*
* This class is only meant for internal use by CallParticipantModel; observers must register themselves against a
* CallParticipantModel rather than against a CallParticipantModelNotifier.
*/
class CallParticipantModelNotifier {
/**
* Helper class to associate a CallParticipantModel.Observer with a Handler.
*/
private static class CallParticipantModelObserverOn {
public final CallParticipantModel.Observer observer;
public final Handler handler;
private CallParticipantModelObserverOn(CallParticipantModel.Observer observer, Handler handler) {
this.observer = observer;
this.handler = handler;
}
}
private final List<CallParticipantModelObserverOn> callParticipantModelObserversOn = new ArrayList<>();
public synchronized void addObserver(CallParticipantModel.Observer observer, Handler handler) {
if (observer == null) {
throw new IllegalArgumentException("CallParticipantModel.Observer can not be null");
}
removeObserver(observer);
callParticipantModelObserversOn.add(new CallParticipantModelObserverOn(observer, handler));
}
public synchronized void removeObserver(CallParticipantModel.Observer observer) {
Iterator<CallParticipantModelObserverOn> it = callParticipantModelObserversOn.iterator();
while (it.hasNext()) {
CallParticipantModelObserverOn observerOn = it.next();
if (observerOn.observer == observer) {
it.remove();
return;
}
}
}
public synchronized void notifyChange() {
for (CallParticipantModelObserverOn observerOn : new ArrayList<>(callParticipantModelObserversOn)) {
if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) {
observerOn.observer.onChange();
} else {
observerOn.handler.post(() -> {
observerOn.observer.onChange();
});
}
}
}
}

View file

@ -0,0 +1,67 @@
/*
* 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.call;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
/**
* Mutable data model for (remote) call participants.
*
* There is no synchronization when setting the values; if needed, it should be handled by the clients of the model.
*/
public class MutableCallParticipantModel extends CallParticipantModel {
public MutableCallParticipantModel(String sessionId) {
super(sessionId);
}
public void setUserId(String userId) {
this.userId.setValue(userId);
}
public void setNick(String nick) {
this.nick.setValue(nick);
}
public void setIceConnectionState(PeerConnection.IceConnectionState iceConnectionState) {
this.iceConnectionState.setValue(iceConnectionState);
}
public void setMediaStream(MediaStream mediaStream) {
this.mediaStream.setValue(mediaStream);
}
public void setAudioAvailable(Boolean audioAvailable) {
this.audioAvailable.setValue(audioAvailable);
}
public void setVideoAvailable(Boolean videoAvailable) {
this.videoAvailable.setValue(videoAvailable);
}
public void setScreenIceConnectionState(PeerConnection.IceConnectionState screenIceConnectionState) {
this.screenIceConnectionState.setValue(screenIceConnectionState);
}
public void setScreenMediaStream(MediaStream screenMediaStream) {
this.screenMediaStream.setValue(screenMediaStream);
}
}

View file

@ -121,6 +121,9 @@ public class PeerConnectionWrapper {
private final boolean isMCUPublisher; private final boolean isMCUPublisher;
private final String videoStreamType; private final String videoStreamType;
// It is assumed that there will be at most one remote stream at each time.
private MediaStream stream;
@Inject @Inject
Context context; Context context;
@ -219,6 +222,10 @@ public class PeerConnectionWrapper {
return videoStreamType; return videoStreamType;
} }
public MediaStream getStream() {
return stream;
}
public void removePeerConnection() { public void removePeerConnection() {
signalingMessageReceiver.removeListener(webRtcMessageListener); signalingMessageReceiver.removeListener(webRtcMessageListener);
@ -484,11 +491,15 @@ public class PeerConnectionWrapper {
@Override @Override
public void onAddStream(MediaStream mediaStream) { public void onAddStream(MediaStream mediaStream) {
stream = mediaStream;
peerConnectionNotifier.notifyStreamAdded(mediaStream); peerConnectionNotifier.notifyStreamAdded(mediaStream);
} }
@Override @Override
public void onRemoveStream(MediaStream mediaStream) { public void onRemoveStream(MediaStream mediaStream) {
stream = null;
peerConnectionNotifier.notifyStreamRemoved(mediaStream); peerConnectionNotifier.notifyStreamRemoved(mediaStream);
} }

View file

@ -0,0 +1,663 @@
/*
* 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.call;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO;
import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST;
import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST_MODERATOR;
import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.MODERATOR;
import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.OWNER;
import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.USER;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
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;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class CallParticipantListExternalSignalingTest {
private static class ParticipantsUpdateParticipantBuilder {
private Participant newUser(long inCall, long lastPing, String sessionId, Participant.ParticipantType type,
String userId) {
Participant participant = new Participant();
participant.setInCall(inCall);
participant.setLastPing(lastPing);
participant.setSessionId(sessionId);
participant.setType(type);
participant.setUserId(userId);
return participant;
}
private Participant newGuest(long inCall, long lastPing, String sessionId, Participant.ParticipantType type) {
Participant participant = new Participant();
participant.setInCall(inCall);
participant.setLastPing(lastPing);
participant.setSessionId(sessionId);
participant.setType(type);
return participant;
}
}
private final ParticipantsUpdateParticipantBuilder builder = new ParticipantsUpdateParticipantBuilder();
private CallParticipantList callParticipantList;
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener;
private CallParticipantList.Observer mockedCallParticipantListObserver;
private Collection<Participant> expectedJoined;
private Collection<Participant> expectedUpdated;
private Collection<Participant> expectedLeft;
private Collection<Participant> expectedUnchanged;
// The order of the left participants in some tests depends on how they are internally sorted by the map, so the
// list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in
// expectedLeft).
// Other tests can just relay on the not guaranteed, but known internal sorting of the elements.
private final ArgumentMatcher<List<Participant>> matchesExpectedLeftIgnoringOrder = left -> {
Collections.sort(left, Comparator.comparing(Participant::getSessionId));
return expectedLeft.equals(left);
};
@Before
public void setUp() {
SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class);
callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver);
mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class);
// Get internal ParticipantListMessageListener from callParticipantList set in the
// mockedSignalingMessageReceiver.
ArgumentCaptor<SignalingMessageReceiver.ParticipantListMessageListener> participantListMessageListenerArgumentCaptor =
ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class);
verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture());
participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue();
expectedJoined = new ArrayList<>();
expectedUpdated = new ArrayList<>();
expectedLeft = new ArrayList<>();
expectedUnchanged = new ArrayList<>();
}
@Test
public void testParticipantsUpdateJoinRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateJoinRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4"));
participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", OWNER, "theUserId5"));
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateJoinRoomThenJoinCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateJoinRoomThenJoinCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateJoinRoomAndCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateJoinRoomAndCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateJoinRoomAndCallRepeated() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
participantListMessageListener.onParticipantsUpdate(participants);
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateChangeCallFlags() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateChangeCallFlagsSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4"));
expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateChangeLastPing() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateChangeLastPingSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2", GUEST));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", USER, "theUserId3"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateChangeParticipantType() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateChangeParticipantTypeeSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST_MODERATOR));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", MODERATOR, "theUserId3"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateLeaveCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateLeaveCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateLeaveCallThenLeaveRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateLeaveCallThenLeaveRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testParticipantsUpdateLeaveCallAndRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participantListMessageListener.onParticipantsUpdate(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testParticipantsUpdateLeaveCallAndRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated),
argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged));
}
@Test
public void testParticipantsUpdateSeveralEventsSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", OWNER, "theUserId5"));
// theSessionId6 has not joined yet.
participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7", GUEST));
participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", USER, "theUserId8"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", MODERATOR, "theUserId9"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
// theSessionId1 is gone.
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST));
participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST));
participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST));
expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8"));
expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5"));
expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST));
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4"));
// Last ping and participant type are not seen as changed, even if they did.
expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9"));
verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated),
argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged));
}
@Test
public void testAllParticipantsUpdateDisconnected() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
InOrder inOrder = inOrder(mockedCallParticipantListObserver);
inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll();
inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testAllParticipantsUpdateDisconnectedWithSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
participants.add(builder.newUser(DISCONNECTED, 2, "theSessionId2", USER, "theUserId2"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 4, "theSessionId4", GUEST));
participantListMessageListener.onParticipantsUpdate(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1"));
expectedLeft.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 4, "theSessionId4", GUEST));
InOrder inOrder = inOrder(mockedCallParticipantListObserver);
inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll();
inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated),
argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged));
}
@Test
public void testAllParticipantsUpdateDisconnectedNoOneInCall() {
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED);
InOrder inOrder = inOrder(mockedCallParticipantListObserver);
inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll();
verifyNoMoreInteractions(mockedCallParticipantListObserver);
}
@Test
public void testAllParticipantsUpdateDisconnectedThenJoinCallAgain() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
participantListMessageListener.onParticipantsUpdate(participants);
expectedJoined.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
}

View file

@ -0,0 +1,535 @@
/*
* 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.call;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO;
import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
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 CallParticipantListInternalSignalingTest {
private static class UsersInRoomParticipantBuilder {
private Participant newUser(long inCall, long lastPing, String sessionId, String userId) {
Participant participant = new Participant();
participant.setInCall(inCall);
participant.setLastPing(lastPing);
participant.setSessionId(sessionId);
participant.setUserId(userId);
return participant;
}
private Participant newGuest(long inCall, long lastPing, String sessionId) {
Participant participant = new Participant();
participant.setInCall(inCall);
participant.setLastPing(lastPing);
participant.setSessionId(sessionId);
return participant;
}
}
private final UsersInRoomParticipantBuilder builder = new UsersInRoomParticipantBuilder();
private CallParticipantList callParticipantList;
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener;
private CallParticipantList.Observer mockedCallParticipantListObserver;
private Collection<Participant> expectedJoined;
private Collection<Participant> expectedUpdated;
private Collection<Participant> expectedLeft;
private Collection<Participant> expectedUnchanged;
// The order of the left participants in some tests depends on how they are internally sorted by the map, so the
// list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in
// expectedLeft).
// Other tests can just relay on the not guaranteed, but known internal sorting of the elements.
private final ArgumentMatcher<List<Participant>> matchesExpectedLeftIgnoringOrder = left -> {
Collections.sort(left, Comparator.comparing(Participant::getSessionId));
return expectedLeft.equals(left);
};
@Before
public void setUp() {
SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class);
callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver);
mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class);
// Get internal ParticipantListMessageListener from callParticipantList set in the
// mockedSignalingMessageReceiver.
ArgumentCaptor<SignalingMessageReceiver.ParticipantListMessageListener> participantListMessageListenerArgumentCaptor =
ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class);
verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture());
participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue();
expectedJoined = new ArrayList<>();
expectedUpdated = new ArrayList<>();
expectedLeft = new ArrayList<>();
expectedUnchanged = new ArrayList<>();
}
@Test
public void testUsersInRoomJoinRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomJoinRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4"));
participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", "theUserId5"));
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomJoinRoomThenJoinCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomJoinRoomThenJoinCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomJoinRoomAndCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomJoinRoomAndCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomJoinRoomAndCallRepeated() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
participantListMessageListener.onUsersInRoom(participants);
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomChangeCallFlags() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomChangeCallFlagsSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1"));
expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4"));
expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomChangeLastPing() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomChangeLastPingSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", "theUserId3"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", "theUserId3"));
callParticipantList.addObserver(mockedCallParticipantListObserver);
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomLeaveCall() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomLeaveCallSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomLeaveCallThenLeaveRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomLeaveCallThenLeaveRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
verifyNoInteractions(mockedCallParticipantListObserver);
}
@Test
public void testUsersInRoomLeaveCallAndRoom() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participantListMessageListener.onUsersInRoom(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated,
expectedLeft, expectedUnchanged);
}
@Test
public void testUsersInRoomLeaveCallAndRoomSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participantListMessageListener.onUsersInRoom(participants);
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated),
argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged));
}
@Test
public void testUsersInRoomSeveralEventsSeveralParticipants() {
List<Participant> participants = new ArrayList<>();
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1"));
participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", "theUserId5"));
// theSessionId6 has not joined yet.
participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7"));
participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", "theUserId8"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", "theUserId9"));
participantListMessageListener.onUsersInRoom(participants);
callParticipantList.addObserver(mockedCallParticipantListObserver);
participants = new ArrayList<>();
// theSessionId1 is gone.
participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3"));
participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5"));
participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6"));
participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7"));
participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8"));
participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9"));
participantListMessageListener.onUsersInRoom(participants);
expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6"));
expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8"));
expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5"));
expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7"));
expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1"));
expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2"));
expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4"));
// Last ping is not seen as changed, even if it did.
expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9"));
verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated),
argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged));
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.call;
import com.nextcloud.talk.signaling.SignalingMessageReceiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class CallParticipantListTest {
private SignalingMessageReceiver mockedSignalingMessageReceiver;
private CallParticipantList callParticipantList;
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener;
@Before
public void setUp() {
mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class);
callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver);
// Get internal ParticipantListMessageListener from callParticipantList set in the
// mockedSignalingMessageReceiver.
ArgumentCaptor<SignalingMessageReceiver.ParticipantListMessageListener> participantListMessageListenerArgumentCaptor =
ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class);
verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture());
participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue();
}
@Test
public void testDestroy() {
callParticipantList.destroy();
verify(mockedSignalingMessageReceiver).removeListener(participantListMessageListener);
verifyNoMoreInteractions(mockedSignalingMessageReceiver);
}
}