mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-12-18 14:42:16 +03:00
Merge pull request #2600 from nextcloud/split-call-participants-and-peer-connections
Split call participants and peer connections
This commit is contained in:
commit
a87f2fb102
14 changed files with 2407 additions and 414 deletions
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
198
app/src/main/java/com/nextcloud/talk/call/CallParticipant.java
Normal file
198
app/src/main/java/com/nextcloud/talk/call/CallParticipant.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue