Merge pull request #4581 from nextcloud/backport/4558/stable-20.1

[stable-20.1] Fix sending local state to other participants
This commit is contained in:
Marcel Hibbe 2025-01-08 14:09:16 +01:00 committed by GitHub
commit 41d2535d51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2765 additions and 91 deletions

View File

@ -63,6 +63,13 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.call.CallParticipant
import com.nextcloud.talk.call.CallParticipantList
import com.nextcloud.talk.call.CallParticipantModel
import com.nextcloud.talk.call.LocalStateBroadcaster
import com.nextcloud.talk.call.LocalStateBroadcasterMcu
import com.nextcloud.talk.call.LocalStateBroadcasterNoMcu
import com.nextcloud.talk.call.MessageSender
import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.data.user.model.User
@ -242,6 +249,9 @@ class CallActivity : CallBaseActivity() {
private var signalingMessageReceiver: SignalingMessageReceiver? = null
private val internalSignalingMessageSender = InternalSignalingMessageSender()
private var signalingMessageSender: SignalingMessageSender? = null
private var messageSender: MessageSender? = null
private val localCallParticipantModel: MutableLocalCallParticipantModel = MutableLocalCallParticipantModel()
private var localStateBroadcaster: LocalStateBroadcaster? = null
private val offerAnswerNickProviders: MutableMap<String?, OfferAnswerNickProvider?> = HashMap()
private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
@ -1119,6 +1129,7 @@ class CallActivity : CallBaseActivity() {
localStream!!.addTrack(localVideoTrack)
localVideoTrack!!.setEnabled(false)
localVideoTrack!!.addSink(binding!!.selfVideoRenderer)
localCallParticipantModel.isVideoEnabled = false
}
private fun microphoneInitialization() {
@ -1129,12 +1140,12 @@ class CallActivity : CallBaseActivity() {
localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource)
localAudioTrack!!.setEnabled(false)
localStream!!.addTrack(localAudioTrack)
localCallParticipantModel.isAudioEnabled = false
}
@SuppressLint("MissingPermission")
private fun startMicInputDetection() {
if (permissionUtil!!.isMicrophonePermissionGranted() && micInputAudioRecordThread == null) {
var isSpeakingLongTerm = false
micInputAudioRecorder = AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
@ -1151,13 +1162,8 @@ class CallActivity : CallBaseActivity() {
micInputAudioRecorder.read(byteArr, 0, byteArr.size)
val isCurrentlySpeaking = abs(byteArr[0].toDouble()) > MICROPHONE_VALUE_THRESHOLD
if (microphoneOn && isCurrentlySpeaking && !isSpeakingLongTerm) {
isSpeakingLongTerm = true
sendIsSpeakingMessage(true)
} else if (!isCurrentlySpeaking && isSpeakingLongTerm) {
isSpeakingLongTerm = false
sendIsSpeakingMessage(false)
}
localCallParticipantModel.isSpeaking = isCurrentlySpeaking
Thread.sleep(MICROPHONE_VALUE_SLEEP)
}
}
@ -1166,27 +1172,6 @@ class CallActivity : CallBaseActivity() {
}
}
@Suppress("Detekt.NestedBlockDepth")
private fun sendIsSpeakingMessage(isSpeaking: Boolean) {
val isSpeakingMessage: String =
if (isSpeaking) SIGNALING_MESSAGE_SPEAKING_STARTED else SIGNALING_MESSAGE_SPEAKING_STOPPED
if (isConnectionEstablished && othersInCall) {
if (!hasMCU) {
for (peerConnectionWrapper in peerConnectionWrapperList) {
peerConnectionWrapper.send(DataChannelMessage(isSpeakingMessage))
}
} else {
for (peerConnectionWrapper in peerConnectionWrapperList) {
if (peerConnectionWrapper.sessionId == webSocketClient!!.sessionId) {
peerConnectionWrapper.send(DataChannelMessage(isSpeakingMessage))
break
}
}
}
}
}
private fun createCameraCapturer(enumerator: CameraEnumerator?): VideoCapturer? {
val deviceNames = enumerator!!.deviceNames
@ -1330,12 +1315,9 @@ class CallActivity : CallBaseActivity() {
}
private fun toggleMedia(enable: Boolean, video: Boolean) {
var message: String
if (video) {
message = SIGNALING_MESSAGE_VIDEO_OFF
if (enable) {
binding!!.cameraButton.alpha = OPACITY_ENABLED
message = SIGNALING_MESSAGE_VIDEO_ON
startVideoCapture()
} else {
binding!!.cameraButton.alpha = OPACITY_DISABLED
@ -1349,6 +1331,7 @@ class CallActivity : CallBaseActivity() {
}
if (localStream != null && localStream!!.videoTracks.size > 0) {
localStream!!.videoTracks[0].setEnabled(enable)
localCallParticipantModel.isVideoEnabled = enable
}
if (enable) {
binding!!.selfVideoRenderer.visibility = View.VISIBLE
@ -1356,29 +1339,14 @@ class CallActivity : CallBaseActivity() {
binding!!.selfVideoRenderer.visibility = View.INVISIBLE
}
} else {
message = SIGNALING_MESSAGE_AUDIO_OFF
if (enable) {
message = SIGNALING_MESSAGE_AUDIO_ON
binding!!.microphoneButton.alpha = OPACITY_ENABLED
} else {
binding!!.microphoneButton.alpha = OPACITY_DISABLED
}
if (localStream != null && localStream!!.audioTracks.size > 0) {
localStream!!.audioTracks[0].setEnabled(enable)
}
}
if (isConnectionEstablished) {
if (!hasMCU) {
for (peerConnectionWrapper in peerConnectionWrapperList) {
peerConnectionWrapper.send(DataChannelMessage(message))
}
} else {
for (peerConnectionWrapper in peerConnectionWrapperList) {
if (peerConnectionWrapper.sessionId == webSocketClient!!.sessionId) {
peerConnectionWrapper.send(DataChannelMessage(message))
break
}
}
localCallParticipantModel.isAudioEnabled = enable
}
}
}
@ -1618,6 +1586,15 @@ class CallActivity : CallBaseActivity() {
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
signalingMessageReceiver!!.addListener(offerMessageListener)
signalingMessageSender = internalSignalingMessageSender
hasMCU = false
messageSender = MessageSenderNoMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrapperList
)
joinRoomAndCall()
}
}
@ -1755,6 +1732,15 @@ class CallActivity : CallBaseActivity() {
callParticipantList = CallParticipantList(signalingMessageReceiver)
callParticipantList!!.addObserver(callParticipantListObserver)
if (hasMCU) {
localStateBroadcaster = LocalStateBroadcasterMcu(localCallParticipantModel, messageSender)
} else {
localStateBroadcaster = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
messageSender as MessageSenderNoMcu
)
}
val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
ncApi!!.joinCall(
credentials,
@ -1903,6 +1889,26 @@ class CallActivity : CallBaseActivity() {
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
signalingMessageReceiver!!.addListener(offerMessageListener)
signalingMessageSender = webSocketClient!!.signalingMessageSender
// If the connection with the signaling server was not established yet the value will be false, but it will
// be overwritten with the right value once the response to the "hello" message is received.
hasMCU = webSocketClient!!.hasMCU()
Log.d(TAG, "hasMCU is $hasMCU")
if (hasMCU) {
messageSender = MessageSenderMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrapperList
)
}
} else {
if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
webSocketClient!!.restartWebSocket()
@ -1928,6 +1934,25 @@ class CallActivity : CallBaseActivity() {
when (webSocketCommunicationEvent.getType()) {
"hello" -> {
Log.d(TAG, "onMessageEvent 'hello'")
hasMCU = webSocketClient!!.hasMCU()
Log.d(TAG, "hasMCU is $hasMCU")
if (hasMCU) {
messageSender = MessageSenderMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrapperList
)
}
if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) {
if (currentCallStatus === CallStatus.RECONNECTING) {
hangup(false, false)
@ -2076,6 +2101,9 @@ class CallActivity : CallBaseActivity() {
private fun hangupNetworkCalls(shutDownView: Boolean, endCallForAll: Boolean) {
Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView")
val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
if (localStateBroadcaster != null) {
localStateBroadcaster!!.destroy()
}
if (callParticipantList != null) {
callParticipantList!!.removeObserver(callParticipantListObserver)
callParticipantList!!.destroy()
@ -2136,8 +2164,6 @@ class CallActivity : CallBaseActivity() {
unchanged: Collection<Participant>
) {
Log.d(TAG, "handleCallParticipantsChanged")
hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient!!.hasMCU()
Log.d(TAG, " hasMCU is $hasMCU")
// The signaling session is the same as the Nextcloud session only when the MCU is not used.
var currentSessionId = callSession
@ -2422,6 +2448,9 @@ class CallActivity : CallBaseActivity() {
callParticipantEventDisplayers[sessionId] = callParticipantEventDisplayer
callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler)
runOnUiThread { addParticipantDisplayItem(callParticipantModel, "video") }
localStateBroadcaster!!.handleCallParticipantAdded(callParticipant.callParticipantModel)
return callParticipant
}
@ -2447,6 +2476,9 @@ class CallActivity : CallBaseActivity() {
private fun removeCallParticipant(sessionId: String?) {
val callParticipant = callParticipants.remove(sessionId) ?: return
localStateBroadcaster!!.handleCallParticipantRemoved(callParticipant.callParticipantModel)
val screenParticipantDisplayItemManager = screenParticipantDisplayItemManagers.remove(sessionId)
callParticipant.callParticipantModel.removeObserver(screenParticipantDisplayItemManager)
val callParticipantEventDisplayer = callParticipantEventDisplayers.remove(sessionId)
@ -3264,12 +3296,5 @@ class CallActivity : CallBaseActivity() {
private const val Y_POS_NO_CALL_INFO: Float = 20f
private const val SESSION_ID_PREFFIX_END: Int = 4
private const val SIGNALING_MESSAGE_SPEAKING_STARTED = "speaking"
private const val SIGNALING_MESSAGE_SPEAKING_STOPPED = "stoppedSpeaking"
private const val SIGNALING_MESSAGE_VIDEO_ON = "videoOn"
private const val SIGNALING_MESSAGE_VIDEO_OFF = "videoOff"
private const val SIGNALING_MESSAGE_AUDIO_ON = "audioOn"
private const val SIGNALING_MESSAGE_AUDIO_OFF = "audioOff"
}
}

View File

@ -0,0 +1,114 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import android.os.Handler;
import java.util.Objects;
/**
* Read-only data model for local call participants.
* <p>
* Clients of the model can observe it with LocalCallParticipantModel.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 LocalCallParticipantModel {
protected final LocalCallParticipantModelNotifier localCallParticipantModelNotifier =
new LocalCallParticipantModelNotifier();
protected Data<Boolean> audioEnabled;
protected Data<Boolean> speaking;
protected Data<Boolean> speakingWhileMuted;
protected Data<Boolean> videoEnabled;
public interface Observer {
void onChange();
}
protected class Data<T> {
private T value;
public Data() {
}
public Data(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
if (Objects.equals(this.value, value)) {
return;
}
this.value = value;
localCallParticipantModelNotifier.notifyChange();
}
}
public LocalCallParticipantModel() {
this.audioEnabled = new Data<>(Boolean.FALSE);
this.speaking = new Data<>(Boolean.FALSE);
this.speakingWhileMuted = new Data<>(Boolean.FALSE);
this.videoEnabled = new Data<>(Boolean.FALSE);
}
public Boolean isAudioEnabled() {
return audioEnabled.getValue();
}
public Boolean isSpeaking() {
return speaking.getValue();
}
public Boolean isSpeakingWhileMuted() {
return speakingWhileMuted.getValue();
}
public Boolean isVideoEnabled() {
return videoEnabled.getValue();
}
/**
* Adds an Observer to be notified when any value changes.
*
* @param observer the Observer
* @see LocalCallParticipantModel#addObserver(Observer, Handler)
*/
public void addObserver(Observer observer) {
addObserver(observer, null);
}
/**
* Adds an observer to be notified when any value changes.
* <p>
* 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.
* <p>
* 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) {
localCallParticipantModelNotifier.addObserver(observer, handler);
}
public void removeObserver(Observer observer) {
localCallParticipantModelNotifier.removeObserver(observer);
}
}

View File

@ -0,0 +1,73 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
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 LocalCallParticipantModel.Observers.
* <p>
* This class is only meant for internal use by LocalCallParticipantModel; observers must register themselves against a
* LocalCallParticipantModel rather than against a LocalCallParticipantModelNotifier.
*/
class LocalCallParticipantModelNotifier {
private final List<LocalCallParticipantModelObserverOn> localCallParticipantModelObserversOn = new ArrayList<>();
/**
* Helper class to associate a LocalCallParticipantModel.Observer with a Handler.
*/
private static class LocalCallParticipantModelObserverOn {
public final LocalCallParticipantModel.Observer observer;
public final Handler handler;
private LocalCallParticipantModelObserverOn(LocalCallParticipantModel.Observer observer, Handler handler) {
this.observer = observer;
this.handler = handler;
}
}
public synchronized void addObserver(LocalCallParticipantModel.Observer observer, Handler handler) {
if (observer == null) {
throw new IllegalArgumentException("LocalCallParticipantModel.Observer can not be null");
}
removeObserver(observer);
localCallParticipantModelObserversOn.add(new LocalCallParticipantModelObserverOn(observer, handler));
}
public synchronized void removeObserver(LocalCallParticipantModel.Observer observer) {
Iterator<LocalCallParticipantModelObserverOn> it = localCallParticipantModelObserversOn.iterator();
while (it.hasNext()) {
LocalCallParticipantModelObserverOn observerOn = it.next();
if (observerOn.observer == observer) {
it.remove();
return;
}
}
}
public synchronized void notifyChange() {
for (LocalCallParticipantModelObserverOn observerOn : new ArrayList<>(localCallParticipantModelObserversOn)) {
if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) {
observerOn.observer.onChange();
} else {
observerOn.handler.post(() -> {
observerOn.observer.onChange();
});
}
}
}
}

View File

@ -0,0 +1,170 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
import java.util.Objects;
/**
* Helper class to send the local participant state to the other participants in the call.
* <p>
* Once created, and until destroyed, the LocalStateBroadcaster will send the changes in the local participant state to
* all the participants in the call. Note that the LocalStateBroadcaster does not check whether the local participant
* is actually in the call or not; it is expected that the LocalStateBroadcaster will be created and destroyed when the
* local participant joins and leaves the call.
* <p>
* The LocalStateBroadcaster also sends the current state to remote participants when they join (which implicitly
* sends it to all remote participants when the local participant joins the call) so they can set an initial state
* for the local participant.
*/
public abstract class LocalStateBroadcaster {
private final LocalCallParticipantModel localCallParticipantModel;
private final LocalCallParticipantModelObserver localCallParticipantModelObserver;
private final MessageSender messageSender;
private class LocalCallParticipantModelObserver implements LocalCallParticipantModel.Observer {
private Boolean audioEnabled;
private Boolean speaking;
private Boolean videoEnabled;
public LocalCallParticipantModelObserver(LocalCallParticipantModel localCallParticipantModel) {
audioEnabled = localCallParticipantModel.isAudioEnabled();
speaking = localCallParticipantModel.isSpeaking();
videoEnabled = localCallParticipantModel.isVideoEnabled();
}
@Override
public void onChange() {
if (!Objects.equals(audioEnabled, localCallParticipantModel.isAudioEnabled())) {
audioEnabled = localCallParticipantModel.isAudioEnabled();
messageSender.sendToAll(getDataChannelMessageForAudioState());
messageSender.sendToAll(getSignalingMessageForAudioState());
}
if (!Objects.equals(speaking, localCallParticipantModel.isSpeaking())) {
speaking = localCallParticipantModel.isSpeaking();
messageSender.sendToAll(getDataChannelMessageForSpeakingState());
}
if (!Objects.equals(videoEnabled, localCallParticipantModel.isVideoEnabled())) {
videoEnabled = localCallParticipantModel.isVideoEnabled();
messageSender.sendToAll(getDataChannelMessageForVideoState());
messageSender.sendToAll(getSignalingMessageForVideoState());
}
}
}
public LocalStateBroadcaster(LocalCallParticipantModel localCallParticipantModel,
MessageSender messageSender) {
this.localCallParticipantModel = localCallParticipantModel;
this.localCallParticipantModelObserver = new LocalCallParticipantModelObserver(localCallParticipantModel);
this.messageSender = messageSender;
this.localCallParticipantModel.addObserver(localCallParticipantModelObserver);
}
public void destroy() {
this.localCallParticipantModel.removeObserver(localCallParticipantModelObserver);
}
public abstract void handleCallParticipantAdded(CallParticipantModel callParticipantModel);
public abstract void handleCallParticipantRemoved(CallParticipantModel callParticipantModel);
protected DataChannelMessage getDataChannelMessageForAudioState() {
String type = "audioOff";
if (localCallParticipantModel.isAudioEnabled() != null && localCallParticipantModel.isAudioEnabled()) {
type = "audioOn";
}
return new DataChannelMessage(type);
}
protected DataChannelMessage getDataChannelMessageForSpeakingState() {
String type = "stoppedSpeaking";
if (localCallParticipantModel.isSpeaking() != null && localCallParticipantModel.isSpeaking()) {
type = "speaking";
}
return new DataChannelMessage(type);
}
protected DataChannelMessage getDataChannelMessageForVideoState() {
String type = "videoOff";
if (localCallParticipantModel.isVideoEnabled() != null && localCallParticipantModel.isVideoEnabled()) {
type = "videoOn";
}
return new DataChannelMessage(type);
}
/**
* Returns a signaling message with the common fields set (type and room type).
*
* @param type the type of the signaling message
* @return the signaling message
*/
private NCSignalingMessage createBaseSignalingMessage(String type) {
NCSignalingMessage ncSignalingMessage = new NCSignalingMessage();
// "roomType" is not really relevant without a peer or when referring to the whole participant, but it is
// nevertheless expected in the message. As most of the signaling messages currently sent to all participants
// are related to audio/video state "video" is used as the room type.
ncSignalingMessage.setRoomType("video");
ncSignalingMessage.setType(type);
return ncSignalingMessage;
}
/**
* Returns a signaling message to notify current audio state.
*
* @return the signaling message
*/
protected NCSignalingMessage getSignalingMessageForAudioState() {
String type = "mute";
if (localCallParticipantModel.isAudioEnabled() != null && localCallParticipantModel.isAudioEnabled()) {
type = "unmute";
}
NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage(type);
NCMessagePayload ncMessagePayload = new NCMessagePayload();
ncMessagePayload.setName("audio");
ncSignalingMessage.setPayload(ncMessagePayload);
return ncSignalingMessage;
}
/**
* Returns a signaling message to notify current video state.
*
* @return the signaling message
*/
protected NCSignalingMessage getSignalingMessageForVideoState() {
String type = "mute";
if (localCallParticipantModel.isVideoEnabled() != null && localCallParticipantModel.isVideoEnabled()) {
type = "unmute";
}
NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage(type);
NCMessagePayload ncMessagePayload = new NCMessagePayload();
ncMessagePayload.setName("video");
ncSignalingMessage.setPayload(ncMessagePayload);
return ncSignalingMessage;
}
}

View File

@ -0,0 +1,118 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Helper class to send the local participant state to the other participants in the call when an MCU is used.
* <p>
* Sending the state when it changes is handled by the base class; this subclass only handles sending the initial
* state when a remote participant is added.
* <p>
* When Janus is used data channel messages are sent to all remote participants (with a peer connection to receive from
* the local participant). Moreover, it is not possible to know when the remote participants open the data channel to
* receive the messages, or even when they establish the receiver connection; it is only possible to know when the
* data channel is open for the publisher connection of the local participant. Due to all that the state is sent
* several times with an increasing delay whenever a participant joins the call (which implicitly broadcasts the
* initial state when the local participant joins the call, as all the remote participants joined from the point of
* view of the local participant). If the state was already being sent the sending is restarted with each new
* participant that joins.
* <p>
* Similarly, in the case of signaling messages it is not possible either to know when the remote participants have
* "seen" the local participant and thus are ready to handle signaling messages about the state. However, in the case
* of signaling messages it is possible to send them to a specific participant, so the initial state is sent several
* times with an increasing delay directly to the participant that was added. Moreover, if the participant is removed
* the state is no longer directly sent.
* <p>
* In any case, note that the state is sent only when the remote participant joins the call. Even in case of
* temporary disconnections the normal state updates sent when the state changes are expected to be received by the
* other participant, as signaling messages are sent through a WebSocket and are therefore reliable. Moreover, even
* if the WebSocket is restarted and the connection resumed (rather than joining with a new session ID) the messages
* would be also received, as in that case they would be queued until the WebSocket is connected again.
* <p>
* Data channel messages, on the other hand, could be lost if the remote participant restarts the peer receiver
* connection (although they would be received in case of temporary disconnections, as data channels use a reliable
* transport by default). Therefore, as the speaking state is sent only through data channels, updates of the speaking
* state could be not received by remote participants.
*/
public class LocalStateBroadcasterMcu extends LocalStateBroadcaster {
private final MessageSender messageSender;
private final Map<String, Disposable> sendStateWithRepetitionByParticipant = new HashMap<>();
private Disposable sendStateWithRepetition;
public LocalStateBroadcasterMcu(LocalCallParticipantModel localCallParticipantModel,
MessageSender messageSender) {
super(localCallParticipantModel, messageSender);
this.messageSender = messageSender;
}
public void destroy() {
super.destroy();
if (sendStateWithRepetition != null) {
sendStateWithRepetition.dispose();
}
for (Disposable sendStateWithRepetitionForParticipant: sendStateWithRepetitionByParticipant.values()) {
sendStateWithRepetitionForParticipant.dispose();
}
}
@Override
public void handleCallParticipantAdded(CallParticipantModel callParticipantModel) {
if (sendStateWithRepetition != null) {
sendStateWithRepetition.dispose();
}
sendStateWithRepetition = Observable
.fromArray(new Integer[]{0, 1, 2, 4, 8, 16})
.concatMap(i -> Observable.just(i).delay(i, TimeUnit.SECONDS, Schedulers.io()))
.subscribe(value -> sendState());
String sessionId = callParticipantModel.getSessionId();
Disposable sendStateWithRepetitionForParticipant = sendStateWithRepetitionByParticipant.get(sessionId);
if (sendStateWithRepetitionForParticipant != null) {
sendStateWithRepetitionForParticipant.dispose();
}
sendStateWithRepetitionByParticipant.put(sessionId, Observable
.fromArray(new Integer[]{0, 1, 2, 4, 8, 16})
.concatMap(i -> Observable.just(i).delay(i, TimeUnit.SECONDS, Schedulers.io()))
.subscribe(value -> sendState(sessionId)));
}
@Override
public void handleCallParticipantRemoved(CallParticipantModel callParticipantModel) {
String sessionId = callParticipantModel.getSessionId();
Disposable sendStateWithRepetitionForParticipant = sendStateWithRepetitionByParticipant.get(sessionId);
if (sendStateWithRepetitionForParticipant != null) {
sendStateWithRepetitionForParticipant.dispose();
}
}
private void sendState() {
messageSender.sendToAll(getDataChannelMessageForAudioState());
messageSender.sendToAll(getDataChannelMessageForSpeakingState());
messageSender.sendToAll(getDataChannelMessageForVideoState());
}
private void sendState(String sessionId) {
messageSender.send(getSignalingMessageForAudioState(), sessionId);
messageSender.send(getSignalingMessageForVideoState(), sessionId);
}
}

View File

@ -0,0 +1,128 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import org.webrtc.PeerConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Helper class to send the local participant state to the other participants in the call when an MCU is not used.
* <p>
* Sending the state when it changes is handled by the base class; this subclass only handles sending the initial
* state when a remote participant is added.
* <p>
* The state is sent when a connection with another participant is first established (which implicitly broadcasts the
* initial state when the local participant joins the call, as a connection is established with all the remote
* participants). Note that, as long as that participant stays in the call, the initial state is not sent again, even
* after a temporary disconnection; data channels use a reliable transport by default, so even if the state changes
* while the connection is temporarily interrupted the normal state update messages should be received by the other
* participant once the connection is restored.
* <p>
* Nevertheless, in case of a failed connection and an ICE restart it is unclear whether the data channel messages
* would be received or not (as the data channel transport may be the one that failed and needs to be restarted).
* However, the state (except the speaking state) is also sent through signaling messages, which need to be
* explicitly fetched from the internal signaling server, so even in case of a failed connection they will be
* eventually received once the remote participant connects again.
*/
public class LocalStateBroadcasterNoMcu extends LocalStateBroadcaster {
private final MessageSenderNoMcu messageSender;
private final Map<String, IceConnectionStateObserver> iceConnectionStateObservers = new HashMap<>();
private class IceConnectionStateObserver implements CallParticipantModel.Observer {
private final CallParticipantModel callParticipantModel;
private PeerConnection.IceConnectionState iceConnectionState;
public IceConnectionStateObserver(CallParticipantModel callParticipantModel) {
this.callParticipantModel = callParticipantModel;
callParticipantModel.addObserver(this);
iceConnectionStateObservers.put(callParticipantModel.getSessionId(), this);
}
@Override
public void onChange() {
if (Objects.equals(iceConnectionState, callParticipantModel.getIceConnectionState())) {
return;
}
iceConnectionState = callParticipantModel.getIceConnectionState();
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) {
remove();
sendState(callParticipantModel.getSessionId());
}
}
@Override
public void onReaction(String reaction) {
}
public void remove() {
callParticipantModel.removeObserver(this);
iceConnectionStateObservers.remove(callParticipantModel.getSessionId());
}
}
public LocalStateBroadcasterNoMcu(LocalCallParticipantModel localCallParticipantModel,
MessageSenderNoMcu messageSender) {
super(localCallParticipantModel, messageSender);
this.messageSender = messageSender;
}
public void destroy() {
super.destroy();
// The observers remove themselves from the map, so a copy is needed to remove them while iterating.
List<IceConnectionStateObserver> iceConnectionStateObserversCopy =
new ArrayList<>(iceConnectionStateObservers.values());
for (IceConnectionStateObserver iceConnectionStateObserver : iceConnectionStateObserversCopy) {
iceConnectionStateObserver.remove();
}
}
@Override
public void handleCallParticipantAdded(CallParticipantModel callParticipantModel) {
IceConnectionStateObserver iceConnectionStateObserver =
iceConnectionStateObservers.get(callParticipantModel.getSessionId());
if (iceConnectionStateObserver != null) {
iceConnectionStateObserver.remove();
}
iceConnectionStateObserver = new IceConnectionStateObserver(callParticipantModel);
iceConnectionStateObservers.put(callParticipantModel.getSessionId(), iceConnectionStateObserver);
}
@Override
public void handleCallParticipantRemoved(CallParticipantModel callParticipantModel) {
IceConnectionStateObserver iceConnectionStateObserver =
iceConnectionStateObservers.get(callParticipantModel.getSessionId());
if (iceConnectionStateObserver != null) {
iceConnectionStateObserver.remove();
}
}
private void sendState(String sessionId) {
messageSender.send(getDataChannelMessageForAudioState(), sessionId);
messageSender.send(getDataChannelMessageForSpeakingState(), sessionId);
messageSender.send(getDataChannelMessageForVideoState(), sessionId);
messageSender.send(getSignalingMessageForAudioState(), sessionId);
messageSender.send(getSignalingMessageForVideoState(), sessionId);
}
}

View File

@ -0,0 +1,93 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
import com.nextcloud.talk.signaling.SignalingMessageSender;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
import java.util.List;
import java.util.Set;
/**
* Helper class to send messages to participants in a call.
* <p>
* A specific subclass has to be created depending on whether an MCU is being used or not.
* <p>
* Note that recipients of signaling messages are not validated, so no error will be triggered if trying to send a
* message to a participant with a session ID that does not exist or is not in the call.
* <p>
* Also note that, unlike signaling messages, data channel messages require a peer connection. Therefore data channel
* messages may not be received by a participant if there is no peer connection with that participant (for example, if
* neither the local and remote participants have publishing rights). Moreover, data channel messages are expected to
* be received only on peer connections with type "video", so data channel messages will not be sent on other peer
* connections.
*/
public abstract class MessageSender {
private final SignalingMessageSender signalingMessageSender;
private final Set<String> callParticipantSessionIds;
protected final List<PeerConnectionWrapper> peerConnectionWrappers;
public MessageSender(SignalingMessageSender signalingMessageSender,
Set<String> callParticipantSessionIds,
List<PeerConnectionWrapper> peerConnectionWrappers) {
this.signalingMessageSender = signalingMessageSender;
this.callParticipantSessionIds = callParticipantSessionIds;
this.peerConnectionWrappers = peerConnectionWrappers;
}
/**
* Sends the given data channel message to all the participants in the call.
*
* @param dataChannelMessage the message to send
*/
public abstract void sendToAll(DataChannelMessage dataChannelMessage);
/**
* Sends the given signaling message to the given session ID.
* <p>
* Note that the signaling message will be modified to set the recipient in the "to" field.
*
* @param ncSignalingMessage the message to send
* @param sessionId the signaling session ID of the participant to send the message to
*/
public void send(NCSignalingMessage ncSignalingMessage, String sessionId) {
ncSignalingMessage.setTo(sessionId);
signalingMessageSender.send(ncSignalingMessage);
}
/**
* Sends the given signaling message to all the participants in the call.
* <p>
* Note that the signaling message will be modified to set each of the recipients in the "to" field.
*
* @param ncSignalingMessage the message to send
*/
public void sendToAll(NCSignalingMessage ncSignalingMessage) {
for (String sessionId: callParticipantSessionIds) {
ncSignalingMessage.setTo(sessionId);
signalingMessageSender.send(ncSignalingMessage);
}
}
protected PeerConnectionWrapper getPeerConnectionWrapper(String sessionId) {
for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) {
if (peerConnectionWrapper.getSessionId().equals(sessionId)
&& "video".equals(peerConnectionWrapper.getVideoStreamType())) {
return peerConnectionWrapper;
}
}
return null;
}
}

View File

@ -0,0 +1,41 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.signaling.SignalingMessageSender;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
import java.util.List;
import java.util.Set;
/**
* Helper class to send messages to participants in a call when an MCU is used.
* <p>
* Note that when Janus is used it is not possible to send a data channel message to a specific participant. Any data
* channel message will be broadcast to all the subscribers of the publisher peer connection (the own peer connection).
*/
public class MessageSenderMcu extends MessageSender {
private final String ownSessionId;
public MessageSenderMcu(SignalingMessageSender signalingMessageSender,
Set<String> callParticipantSessionIds,
List<PeerConnectionWrapper> peerConnectionWrappers,
String ownSessionId) {
super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers);
this.ownSessionId = ownSessionId;
}
public void sendToAll(DataChannelMessage dataChannelMessage) {
PeerConnectionWrapper ownPeerConnectionWrapper = getPeerConnectionWrapper(ownSessionId);
if (ownPeerConnectionWrapper != null) {
ownPeerConnectionWrapper.send(dataChannelMessage);
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.signaling.SignalingMessageSender;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
import java.util.List;
import java.util.Set;
/**
* Helper class to send messages to participants in a call when an MCU is not used.
*/
public class MessageSenderNoMcu extends MessageSender {
public MessageSenderNoMcu(SignalingMessageSender signalingMessageSender,
Set<String> callParticipantSessionIds,
List<PeerConnectionWrapper> peerConnectionWrappers) {
super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers);
}
/**
* Sends the given data channel message to the given signaling session ID.
*
* @param dataChannelMessage the message to send
* @param sessionId the signaling session ID of the participant to send the message to
*/
public void send(DataChannelMessage dataChannelMessage, String sessionId) {
PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapper(sessionId);
if (peerConnectionWrapper != null) {
peerConnectionWrapper.send(dataChannelMessage);
}
}
public void sendToAll(DataChannelMessage dataChannelMessage) {
for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) {
if ("video".equals(peerConnectionWrapper.getVideoStreamType())){
peerConnectionWrapper.send(dataChannelMessage);
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import java.util.Objects;
/**
* Mutable data model for local call participants.
* <p>
* Setting "speaking" will automatically set "speaking" or "speakingWhileMuted" as needed, depending on whether audio is
* enabled or not. Similarly, setting whether the audio is enabled or disabled will automatically switch between
* "speaking" and "speakingWhileMuted" as needed.
* <p>
* There is no synchronization when setting the values; if needed, it should be handled by the clients of the model.
*/
public class MutableLocalCallParticipantModel extends LocalCallParticipantModel {
public void setAudioEnabled(Boolean audioEnabled) {
if (Objects.equals(this.audioEnabled.getValue(), audioEnabled)) {
return;
}
if (audioEnabled == null || !audioEnabled) {
this.speakingWhileMuted.setValue(this.speaking.getValue());
this.speaking.setValue(Boolean.FALSE);
}
this.audioEnabled.setValue(audioEnabled);
if (audioEnabled != null && audioEnabled) {
this.speaking.setValue(this.speakingWhileMuted.getValue());
this.speakingWhileMuted.setValue(Boolean.FALSE);
}
}
public void setSpeaking(Boolean speaking) {
if (this.audioEnabled.getValue() != null && this.audioEnabled.getValue()) {
this.speaking.setValue(speaking);
} else {
this.speakingWhileMuted.setValue(speaking);
}
}
public void setVideoEnabled(Boolean videoEnabled) {
this.videoEnabled.setValue(videoEnabled);
}
}

View File

@ -62,9 +62,6 @@ public class PeerConnectionWrapper {
private final List<DataChannelMessage> pendingDataChannelMessages = new ArrayList<>();
private final SdpObserver sdpObserver;
private final boolean hasInitiated;
private final MediaStream localStream;
private final boolean isMCUPublisher;
private final String videoStreamType;
@ -113,14 +110,13 @@ public class PeerConnectionWrapper {
boolean isMCUPublisher, boolean hasMCU, String videoStreamType,
SignalingMessageReceiver signalingMessageReceiver,
SignalingMessageSender signalingMessageSender) {
this.localStream = localStream;
this.videoStreamType = videoStreamType;
this.sessionId = sessionId;
this.mediaConstraints = mediaConstraints;
sdpObserver = new SdpObserver();
hasInitiated = sessionId.compareTo(localSession) < 0;
boolean hasInitiated = sessionId.compareTo(localSession) < 0;
this.isMCUPublisher = isMCUPublisher;
PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServerList);
@ -133,12 +129,12 @@ public class PeerConnectionWrapper {
this.signalingMessageSender = signalingMessageSender;
if (peerConnection != null) {
if (this.localStream != null) {
List<String> localStreamIds = Collections.singletonList(this.localStream.getId());
for(AudioTrack track : this.localStream.audioTracks) {
if (localStream != null) {
List<String> localStreamIds = Collections.singletonList(localStream.getId());
for(AudioTrack track : localStream.audioTracks) {
peerConnection.addTrack(track, localStreamIds);
}
for(VideoTrack track : this.localStream.videoTracks) {
for(VideoTrack track : localStream.videoTracks) {
peerConnection.addTrack(track, localStreamIds);
}
}
@ -329,22 +325,6 @@ public class PeerConnectionWrapper {
return sessionId;
}
private void sendInitialMediaStatus() {
if (localStream != null) {
if (localStream.videoTracks.size() == 1 && localStream.videoTracks.get(0).enabled()) {
send(new DataChannelMessage("videoOn"));
} else {
send(new DataChannelMessage("videoOff"));
}
if (localStream.audioTracks.size() == 1 && localStream.audioTracks.get(0).enabled()) {
send(new DataChannelMessage("audioOn"));
} else {
send(new DataChannelMessage("audioOff"));
}
}
}
public boolean isMCUPublisher() {
return isMCUPublisher;
}
@ -432,10 +412,6 @@ public class PeerConnectionWrapper {
}
pendingDataChannelMessages.clear();
}
if (dataChannel.state() == DataChannel.State.OPEN) {
sendInitialMediaStatus();
}
}
}
@ -523,11 +499,6 @@ public class PeerConnectionWrapper {
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
if (hasInitiated) {
sendInitialMediaStatus();
}
}
peerConnectionNotifier.notifyIceConnectionStateChanged(iceConnectionState);
}

View File

@ -0,0 +1,168 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
class LocalCallParticipantModelTest {
private var localCallParticipantModel: MutableLocalCallParticipantModel? = null
private var mockedLocalCallParticipantModelObserver: LocalCallParticipantModel.Observer? = null
@Before
fun setUp() {
localCallParticipantModel = MutableLocalCallParticipantModel()
mockedLocalCallParticipantModelObserver = Mockito.mock(LocalCallParticipantModel.Observer::class.java)
}
@Test
fun testSetAudioEnabled() {
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetAudioEnabledWhileSpeakingWhileMuted() {
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioEnabledTwiceWhileSpeakingWhileMuted() {
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetAudioDisabledWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioDisabledTwiceWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetSpeakingWhileAudioEnabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetNotSpeakingWhileAudioEnabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = false
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetSpeakingWhileAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = true
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetNotSpeakingWhileAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
}

View File

@ -0,0 +1,641 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.models.json.signaling.NCMessagePayload
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.times
import java.util.concurrent.TimeUnit
@Suppress("LongMethod")
class LocalStateBroadcasterMcuTest {
private var localCallParticipantModel: MutableLocalCallParticipantModel? = null
private var mockedMessageSender: MessageSender? = null
private var localStateBroadcasterMcu: LocalStateBroadcasterMcu? = null
@Before
fun setUp() {
localCallParticipantModel = MutableLocalCallParticipantModel()
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.isVideoEnabled = true
mockedMessageSender = Mockito.mock(MessageSender::class.java)
}
private fun getExpectedUnmuteAudio(): NCSignalingMessage {
val expectedUnmuteAudio = NCSignalingMessage()
expectedUnmuteAudio.roomType = "video"
expectedUnmuteAudio.type = "unmute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedUnmuteAudio.payload = payload
return expectedUnmuteAudio
}
private fun getExpectedMuteAudio(): NCSignalingMessage {
val expectedMuteAudio = NCSignalingMessage()
expectedMuteAudio.roomType = "video"
expectedMuteAudio.type = "mute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedMuteAudio.payload = payload
return expectedMuteAudio
}
private fun getExpectedUnmuteVideo(): NCSignalingMessage {
val expectedUnmuteVideo = NCSignalingMessage()
expectedUnmuteVideo.roomType = "video"
expectedUnmuteVideo.type = "unmute"
val payload = NCMessagePayload()
payload.name = "video"
expectedUnmuteVideo.payload = payload
return expectedUnmuteVideo
}
private fun getExpectedMuteVideo(): NCSignalingMessage {
val expectedMuteVideo = NCSignalingMessage()
expectedMuteVideo.roomType = "video"
expectedMuteVideo.type = "mute"
val payload = NCMessagePayload()
payload.name = "video"
expectedMuteVideo.payload = payload
return expectedMuteVideo
}
@Test
fun testStateSentWithExponentialBackoffWhenParticipantAdded() {
val testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
localStateBroadcasterMcu = LocalStateBroadcasterMcu(
localCallParticipantModel,
mockedMessageSender
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel)
// Sending will be done in another thread, so just adding the participant does not send anything until that
// other thread could run.
Mockito.verifyNoInteractions(mockedMessageSender)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
var messageCount = 1
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
messageCount = 2
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
messageCount = 3
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(4, TimeUnit.SECONDS)
messageCount = 4
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(8, TimeUnit.SECONDS)
messageCount = 5
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(16, TimeUnit.SECONDS)
messageCount = 6
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testStateSentWithExponentialBackoffIsTheCurrentState() {
// This test could have been included in "testStateSentWithExponentialBackoffWhenParticipantAdded", but was
// kept separate for clarity.
val testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
localStateBroadcasterMcu = LocalStateBroadcasterMcu(
localCallParticipantModel,
mockedMessageSender
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel)
// Sending will be done in another thread, so just adding the participant does not send anything until that
// other thread could run.
Mockito.verifyNoInteractions(mockedMessageSender)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localCallParticipantModel!!.isSpeaking = false
val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking")
// Changing the state causes the normal state update to be sent, independently of the initial state
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedStoppedSpeaking)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedStoppedSpeaking)
Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(2)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(2)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = false
val expectedAudioOff = DataChannelMessage("audioOff")
val expectedMuteAudio = getExpectedMuteAudio()
// Changing the state causes the normal state update to be sent, independently of the initial state
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedStoppedSpeaking)
Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio)
Mockito.verify(mockedMessageSender!!, times(1)).send(expectedMuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(3)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = false
val expectedVideoOff = DataChannelMessage("videoOff")
val expectedMuteVideo = getExpectedMuteVideo()
// Changing the state causes the normal state update to be sent, independently of the initial state
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedVideoOff)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo)
testScheduler.advanceTimeBy(4, TimeUnit.SECONDS)
Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedStoppedSpeaking)
Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedVideoOff)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo)
Mockito.verify(mockedMessageSender!!, times(2)).send(expectedMuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(1)).send(expectedMuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = true
// Changing the state causes the normal state update to be sent, independently of the initial state
Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedUnmuteVideo)
testScheduler.advanceTimeBy(8, TimeUnit.SECONDS)
Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!, times(5)).sendToAll(expectedStoppedSpeaking)
Mockito.verify(mockedMessageSender!!, times(5)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo)
Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedUnmuteVideo)
Mockito.verify(mockedMessageSender!!, times(3)).send(expectedMuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(4)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testStateSentWithExponentialBackoffWhenAnotherParticipantAdded() {
// The state sent through data channels should be restarted, although the state sent through signaling
// messages should be independent for each participant.
val testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
localStateBroadcasterMcu = LocalStateBroadcasterMcu(
localCallParticipantModel,
mockedMessageSender
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel)
// Sending will be done in another thread, so just adding the participant does not send anything until that
// other thread could run.
Mockito.verifyNoInteractions(mockedMessageSender)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
var dataChannelMessageCount = 1
var signalingMessageCount1 = 1
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
dataChannelMessageCount = 2
signalingMessageCount1 = 2
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
dataChannelMessageCount = 3
signalingMessageCount1 = 3
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(4, TimeUnit.SECONDS)
dataChannelMessageCount = 4
signalingMessageCount1 = 4
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
val callParticipantModel2 = MutableCallParticipantModel("theSessionId2")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel2)
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
dataChannelMessageCount = 5
var signalingMessageCount2 = 1
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
dataChannelMessageCount = 6
signalingMessageCount2 = 2
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
dataChannelMessageCount = 7
signalingMessageCount2 = 3
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(4, TimeUnit.SECONDS)
dataChannelMessageCount = 8
signalingMessageCount2 = 4
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
// 0+1+2+4+1=8 seconds since last signaling messages for participant 1
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
signalingMessageCount1 = 5
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
// 1+7=8 seconds since last data channel messages and signaling messages for participant 2
testScheduler.advanceTimeBy(7, TimeUnit.SECONDS)
dataChannelMessageCount = 9
signalingMessageCount2 = 5
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
// 7+9=16 seconds since last signaling messages for participant 1
testScheduler.advanceTimeBy(9, TimeUnit.SECONDS)
signalingMessageCount1 = 6
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
// 9+7=16 seconds since last data channel messages and signaling messages for participant 2
testScheduler.advanceTimeBy(7, TimeUnit.SECONDS)
dataChannelMessageCount = 10
signalingMessageCount2 = 6
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testStateSentWithExponentialBackoffWhenParticipantRemoved() {
// For simplicity the exponential backoff is not aborted when the participant that triggered it is removed.
// However, the signaling messages are stopped when the participant is removed.
val testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
localStateBroadcasterMcu = LocalStateBroadcasterMcu(
localCallParticipantModel,
mockedMessageSender
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel)
// Sending will be done in another thread, so just adding the participant does not send anything until that
// other thread could run.
Mockito.verifyNoInteractions(mockedMessageSender)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
var dataChannelMessageCount = 1
var signalingMessageCount = 1
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
dataChannelMessageCount = 2
signalingMessageCount = 2
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
dataChannelMessageCount = 3
signalingMessageCount = 3
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(4, TimeUnit.SECONDS)
dataChannelMessageCount = 4
signalingMessageCount = 4
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localStateBroadcasterMcu!!.handleCallParticipantRemoved(callParticipantModel)
testScheduler.advanceTimeBy(8, TimeUnit.SECONDS)
dataChannelMessageCount = 5
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(16, TimeUnit.SECONDS)
dataChannelMessageCount = 6
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testStateNoLongerSentOnceDestroyed() {
val testScheduler = TestScheduler()
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
localStateBroadcasterMcu = LocalStateBroadcasterMcu(
localCallParticipantModel,
mockedMessageSender
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
val callParticipantModel2 = MutableCallParticipantModel("theSessionId2")
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel)
localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel2)
// Sending will be done in another thread, so just adding the participant does not send anything until that
// other thread could run.
Mockito.verifyNoInteractions(mockedMessageSender)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
testScheduler.advanceTimeBy(0, TimeUnit.SECONDS)
var messageCount = 1
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(1, TimeUnit.SECONDS)
messageCount = 2
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
messageCount = 3
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSender)
localStateBroadcasterMcu!!.destroy()
testScheduler.advanceTimeBy(100, TimeUnit.SECONDS)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
}

View File

@ -0,0 +1,357 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.models.json.signaling.NCMessagePayload
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.webrtc.PeerConnection
class LocalStateBroadcasterNoMcuTest {
private var localCallParticipantModel: MutableLocalCallParticipantModel? = null
private var mockedMessageSenderNoMcu: MessageSenderNoMcu? = null
private var localStateBroadcasterNoMcu: LocalStateBroadcasterNoMcu? = null
@Before
fun setUp() {
localCallParticipantModel = MutableLocalCallParticipantModel()
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.isVideoEnabled = true
mockedMessageSenderNoMcu = Mockito.mock(MessageSenderNoMcu::class.java)
}
private fun getExpectedUnmuteAudio(): NCSignalingMessage {
val expectedUnmuteAudio = NCSignalingMessage()
expectedUnmuteAudio.roomType = "video"
expectedUnmuteAudio.type = "unmute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedUnmuteAudio.payload = payload
return expectedUnmuteAudio
}
private fun getExpectedUnmuteVideo(): NCSignalingMessage {
val expectedUnmuteVideo = NCSignalingMessage()
expectedUnmuteVideo.roomType = "video"
expectedUnmuteVideo.type = "unmute"
val payload = NCMessagePayload()
payload.name = "video"
expectedUnmuteVideo.payload = payload
return expectedUnmuteVideo
}
@Test
fun testStateSentWhenIceConnected() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateSentWhenIceCompleted() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceCompletedAfterConnected() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceConnectedAgain() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
// Completed -> Connected could happen with an ICE restart
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.DISCONNECTED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
// Failed -> Checking could happen with an ICE restart
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.FAILED)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentToOtherParticipantsWhenIceConnected() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
val callParticipantModel2 = MutableCallParticipantModel("theSessionId2")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel2)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteAudio = getExpectedUnmuteAudio()
val expectedUnmuteVideo = getExpectedUnmuteVideo()
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId2")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId2")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId2")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId2")
Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId2")
Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceConnectedAfterParticipantIsRemoved() {
// This should not happen, as peer connections are expected to be ended when a call participant is removed, but
// just in case.
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
localStateBroadcasterNoMcu!!.handleCallParticipantRemoved(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceCompletedAfterParticipantIsRemoved() {
// This should not happen, as peer connections are expected to be ended when a call participant is removed, but
// just in case.
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
localStateBroadcasterNoMcu!!.handleCallParticipantRemoved(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceConnectedAfterDestroyed() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
val callParticipantModel2 = MutableCallParticipantModel("theSessionId2")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel2)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
localStateBroadcasterNoMcu!!.destroy()
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
}
@Test
fun testStateNotSentWhenIceCompletedAfterDestroyed() {
localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu(
localCallParticipantModel,
mockedMessageSenderNoMcu
)
val callParticipantModel = MutableCallParticipantModel("theSessionId")
localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel)
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
localStateBroadcasterNoMcu!!.destroy()
callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED)
Mockito.verifyNoInteractions(mockedMessageSenderNoMcu)
}
}

View File

@ -0,0 +1,324 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.models.json.signaling.NCMessagePayload
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
@Suppress("TooManyFunctions")
class LocalStateBroadcasterTest {
private class LocalStateBroadcaster(
localCallParticipantModel: LocalCallParticipantModel?,
messageSender: MessageSender?
) : com.nextcloud.talk.call.LocalStateBroadcaster(localCallParticipantModel, messageSender) {
override fun handleCallParticipantAdded(callParticipantModel: CallParticipantModel) {
// Not used in base class tests
}
override fun handleCallParticipantRemoved(callParticipantModel: CallParticipantModel) {
// Not used in base class tests
}
}
private var localCallParticipantModel: MutableLocalCallParticipantModel? = null
private var mockedMessageSender: MessageSender? = null
private var localStateBroadcaster: LocalStateBroadcaster? = null
@Before
fun setUp() {
localCallParticipantModel = MutableLocalCallParticipantModel()
mockedMessageSender = Mockito.mock(MessageSender::class.java)
}
@Test
fun testEnableAudio() {
localCallParticipantModel!!.isAudioEnabled = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = true
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedUnmuteAudio = NCSignalingMessage()
expectedUnmuteAudio.roomType = "video"
expectedUnmuteAudio.type = "unmute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedUnmuteAudio.payload = payload
Mockito.verify(mockedMessageSender!!).sendToAll(expectedAudioOn)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteAudio)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testEnableAudioTwice() {
localCallParticipantModel!!.isAudioEnabled = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = true
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableAudio() {
localCallParticipantModel!!.isAudioEnabled = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = false
val expectedAudioOff = DataChannelMessage("audioOff")
val expectedMuteAudio = NCSignalingMessage()
expectedMuteAudio.roomType = "video"
expectedMuteAudio.type = "mute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedMuteAudio.payload = payload
Mockito.verify(mockedMessageSender!!).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteAudio)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableAudioTwice() {
localCallParticipantModel!!.isAudioEnabled = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = false
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testEnableSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = true
val expectedSpeaking = DataChannelMessage("speaking")
Mockito.verify(mockedMessageSender!!).sendToAll(expectedSpeaking)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testEnableSpeakingTwice() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = true
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testEnableSpeakingWithAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = true
Mockito.verifyNoInteractions(mockedMessageSender)
}
@Test
fun testEnableAudioWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.isAudioEnabled = true
val expectedAudioOn = DataChannelMessage("audioOn")
val expectedSpeaking = DataChannelMessage("speaking")
val expectedUnmuteAudio = NCSignalingMessage()
expectedUnmuteAudio.roomType = "video"
expectedUnmuteAudio.type = "unmute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedUnmuteAudio.payload = payload
val inOrder = Mockito.inOrder(mockedMessageSender)
inOrder.verify(mockedMessageSender!!).sendToAll(expectedAudioOn)
inOrder.verify(mockedMessageSender!!).sendToAll(expectedSpeaking)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteAudio)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = false
val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking")
Mockito.verify(mockedMessageSender!!).sendToAll(expectedStoppedSpeaking)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableSpeakingTwice() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = false
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableAudioWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isAudioEnabled = false
val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking")
val expectedAudioOff = DataChannelMessage("audioOff")
val expectedMuteAudio = NCSignalingMessage()
expectedMuteAudio.roomType = "video"
expectedMuteAudio.type = "mute"
val payload = NCMessagePayload()
payload.name = "audio"
expectedMuteAudio.payload = payload
val inOrder = Mockito.inOrder(mockedMessageSender)
inOrder.verify(mockedMessageSender!!).sendToAll(expectedStoppedSpeaking)
inOrder.verify(mockedMessageSender!!).sendToAll(expectedAudioOff)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteAudio)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableSpeakingWithAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isSpeaking = false
Mockito.verifyNoInteractions(mockedMessageSender)
}
@Test
fun testEnableVideo() {
localCallParticipantModel!!.isVideoEnabled = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = true
val expectedVideoOn = DataChannelMessage("videoOn")
val expectedUnmuteVideo = NCSignalingMessage()
expectedUnmuteVideo.roomType = "video"
expectedUnmuteVideo.type = "unmute"
val payload = NCMessagePayload()
payload.name = "video"
expectedUnmuteVideo.payload = payload
Mockito.verify(mockedMessageSender!!).sendToAll(expectedVideoOn)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteVideo)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testEnableVideoTwice() {
localCallParticipantModel!!.isVideoEnabled = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = true
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableVideo() {
localCallParticipantModel!!.isVideoEnabled = true
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = false
val expectedVideoOff = DataChannelMessage("videoOff")
val expectedMuteVideo = NCSignalingMessage()
expectedMuteVideo.roomType = "video"
expectedMuteVideo.type = "mute"
val payload = NCMessagePayload()
payload.name = "video"
expectedMuteVideo.payload = payload
Mockito.verify(mockedMessageSender!!).sendToAll(expectedVideoOff)
Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteVideo)
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testDisableVideoTwice() {
localCallParticipantModel!!.isVideoEnabled = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localCallParticipantModel!!.isVideoEnabled = false
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
@Test
fun testChangeStateAfterDestroying() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = false
localCallParticipantModel!!.isVideoEnabled = false
localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender)
localStateBroadcaster!!.destroy()
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.isVideoEnabled = true
Mockito.verifyNoMoreInteractions(mockedMessageSender)
}
}

View File

@ -0,0 +1,118 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.webrtc.PeerConnectionWrapper
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.never
class MessageSenderMcuTest {
private var peerConnectionWrappers: MutableList<PeerConnectionWrapper?>? = null
private var peerConnectionWrapper1: PeerConnectionWrapper? = null
private var peerConnectionWrapper2: PeerConnectionWrapper? = null
private var peerConnectionWrapper2Screen: PeerConnectionWrapper? = null
private var peerConnectionWrapper4Screen: PeerConnectionWrapper? = null
private var ownPeerConnectionWrapper: PeerConnectionWrapper? = null
private var ownPeerConnectionWrapperScreen: PeerConnectionWrapper? = null
private var messageSender: MessageSenderMcu? = null
@Before
fun setUp() {
val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java)
val callParticipants = HashMap<String, CallParticipant>()
peerConnectionWrappers = ArrayList()
peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1")
Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(peerConnectionWrapper1)
peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2")
Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(peerConnectionWrapper2)
peerConnectionWrapper2Screen = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper2Screen!!.sessionId).thenReturn("theSessionId2")
Mockito.`when`(peerConnectionWrapper2Screen!!.videoStreamType).thenReturn("screen")
peerConnectionWrappers!!.add(peerConnectionWrapper2Screen)
peerConnectionWrapper4Screen = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper4Screen!!.sessionId).thenReturn("theSessionId4")
Mockito.`when`(peerConnectionWrapper4Screen!!.videoStreamType).thenReturn("screen")
peerConnectionWrappers!!.add(peerConnectionWrapper4Screen)
ownPeerConnectionWrapper = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(ownPeerConnectionWrapper!!.sessionId).thenReturn("ownSessionId")
Mockito.`when`(ownPeerConnectionWrapper!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(ownPeerConnectionWrapper)
ownPeerConnectionWrapperScreen = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(ownPeerConnectionWrapperScreen!!.sessionId).thenReturn("ownSessionId")
Mockito.`when`(ownPeerConnectionWrapperScreen!!.videoStreamType).thenReturn("screen")
peerConnectionWrappers!!.add(ownPeerConnectionWrapperScreen)
messageSender = MessageSenderMcu(
signalingMessageSender,
callParticipants.keys,
peerConnectionWrappers,
"ownSessionId"
)
}
@Test
fun testSendDataChannelMessageToAll() {
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(ownPeerConnectionWrapper!!).send(message)
Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageToAllIfOwnScreenPeerConnection() {
peerConnectionWrappers!!.remove(ownPeerConnectionWrapper)
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message)
Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageToAllWithoutOwnPeerConnection() {
peerConnectionWrappers!!.remove(ownPeerConnectionWrapper)
peerConnectionWrappers!!.remove(ownPeerConnectionWrapperScreen)
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message)
Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
}

View File

@ -0,0 +1,101 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.webrtc.PeerConnectionWrapper
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.never
class MessageSenderNoMcuTest {
private var peerConnectionWrappers: MutableList<PeerConnectionWrapper?>? = null
private var peerConnectionWrapper1: PeerConnectionWrapper? = null
private var peerConnectionWrapper2: PeerConnectionWrapper? = null
private var peerConnectionWrapper2Screen: PeerConnectionWrapper? = null
private var peerConnectionWrapper4Screen: PeerConnectionWrapper? = null
private var messageSender: MessageSenderNoMcu? = null
@Before
fun setUp() {
val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java)
val callParticipants = HashMap<String, CallParticipant>()
peerConnectionWrappers = ArrayList()
peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1")
Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(peerConnectionWrapper1)
peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2")
Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(peerConnectionWrapper2)
peerConnectionWrapper2Screen = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper2Screen!!.sessionId).thenReturn("theSessionId2")
Mockito.`when`(peerConnectionWrapper2Screen!!.videoStreamType).thenReturn("screen")
peerConnectionWrappers!!.add(peerConnectionWrapper2Screen)
peerConnectionWrapper4Screen = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(peerConnectionWrapper4Screen!!.sessionId).thenReturn("theSessionId4")
Mockito.`when`(peerConnectionWrapper4Screen!!.videoStreamType).thenReturn("screen")
peerConnectionWrappers!!.add(peerConnectionWrapper4Screen)
messageSender = MessageSenderNoMcu(signalingMessageSender, callParticipants.keys, peerConnectionWrappers)
}
@Test
fun testSendDataChannelMessage() {
val message = DataChannelMessage()
messageSender!!.send(message, "theSessionId2")
Mockito.verify(peerConnectionWrapper2!!).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageIfScreenPeerConnection() {
val message = DataChannelMessage()
messageSender!!.send(message, "theSessionId4")
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageIfNoPeerConnection() {
val message = DataChannelMessage()
messageSender!!.send(message, "theSessionId3")
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageToAll() {
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(peerConnectionWrapper1!!).send(message)
Mockito.verify(peerConnectionWrapper2!!).send(message)
Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message)
Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message)
}
}

View File

@ -0,0 +1,134 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import com.nextcloud.talk.models.json.signaling.DataChannelMessage
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.webrtc.PeerConnectionWrapper
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.times
import org.mockito.invocation.InvocationOnMock
class MessageSenderTest {
private class MessageSender(
signalingMessageSender: SignalingMessageSender?,
callParticipantSessionIds: Set<String>?,
peerConnectionWrappers: List<PeerConnectionWrapper>?
) : com.nextcloud.talk.call.MessageSender(
signalingMessageSender,
callParticipantSessionIds,
peerConnectionWrappers
) {
override fun sendToAll(dataChannelMessage: DataChannelMessage?) {
// Not used in base class tests
}
}
private var signalingMessageSender: SignalingMessageSender? = null
private var callParticipants: MutableMap<String, CallParticipant>? = null
private var messageSender: MessageSender? = null
@Before
fun setUp() {
signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java)
callParticipants = HashMap()
val callParticipant1: CallParticipant = Mockito.mock(CallParticipant::class.java)
callParticipants!!["theSessionId1"] = callParticipant1
val callParticipant2: CallParticipant = Mockito.mock(CallParticipant::class.java)
callParticipants!!["theSessionId2"] = callParticipant2
val callParticipant3: CallParticipant = Mockito.mock(CallParticipant::class.java)
callParticipants!!["theSessionId3"] = callParticipant3
val callParticipant4: CallParticipant = Mockito.mock(CallParticipant::class.java)
callParticipants!!["theSessionId4"] = callParticipant4
val peerConnectionWrappers = ArrayList<PeerConnectionWrapper>()
messageSender = MessageSender(signalingMessageSender, callParticipants!!.keys, peerConnectionWrappers)
}
@Test
fun testSendSignalingMessage() {
val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java)
messageSender!!.send(message, "theSessionId2")
Mockito.verify(message).to = "theSessionId2"
Mockito.verify(signalingMessageSender!!).send(message)
}
@Test
fun testSendSignalingMessageIfUnknownSessionId() {
val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java)
messageSender!!.send(message, "unknownSessionId")
Mockito.verify(message).to = "unknownSessionId"
Mockito.verify(signalingMessageSender!!).send(message)
}
@Test
fun testSendSignalingMessageToAll() {
val sentTo: MutableList<String?> = ArrayList()
doAnswer { invocation: InvocationOnMock ->
val arguments = invocation.arguments
val message = (arguments[0] as NCSignalingMessage)
sentTo.add(message.to)
null
}.`when`(signalingMessageSender!!).send(any())
val message = NCSignalingMessage()
messageSender!!.sendToAll(message)
assertTrue(sentTo.contains("theSessionId1"))
assertTrue(sentTo.contains("theSessionId2"))
assertTrue(sentTo.contains("theSessionId3"))
assertTrue(sentTo.contains("theSessionId4"))
Mockito.verify(signalingMessageSender!!, times(4)).send(message)
Mockito.verifyNoMoreInteractions(signalingMessageSender)
}
@Test
fun testSendSignalingMessageToAllWhenParticipantsWereUpdated() {
val callParticipant5: CallParticipant = Mockito.mock(CallParticipant::class.java)
callParticipants!!["theSessionId5"] = callParticipant5
callParticipants!!.remove("theSessionId2")
callParticipants!!.remove("theSessionId3")
val sentTo: MutableList<String?> = ArrayList()
doAnswer { invocation: InvocationOnMock ->
val arguments = invocation.arguments
val message = (arguments[0] as NCSignalingMessage)
sentTo.add(message.to)
null
}.`when`(signalingMessageSender!!).send(any())
val message = NCSignalingMessage()
messageSender!!.sendToAll(message)
assertTrue(sentTo.contains("theSessionId1"))
assertTrue(sentTo.contains("theSessionId4"))
assertTrue(sentTo.contains("theSessionId5"))
Mockito.verify(signalingMessageSender!!, times(3)).send(message)
Mockito.verifyNoMoreInteractions(signalingMessageSender)
}
}