mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 12:09:45 +01:00
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:
commit
41d2535d51
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
93
app/src/main/java/com/nextcloud/talk/call/MessageSender.java
Normal file
93
app/src/main/java/com/nextcloud/talk/call/MessageSender.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
134
app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt
Normal file
134
app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user