mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-21 12:39:58 +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.CallParticipant
|
||||||
import com.nextcloud.talk.call.CallParticipantList
|
import com.nextcloud.talk.call.CallParticipantList
|
||||||
import com.nextcloud.talk.call.CallParticipantModel
|
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.call.ReactionAnimator
|
||||||
import com.nextcloud.talk.chat.ChatActivity
|
import com.nextcloud.talk.chat.ChatActivity
|
||||||
import com.nextcloud.talk.data.user.model.User
|
import com.nextcloud.talk.data.user.model.User
|
||||||
@ -242,6 +249,9 @@ class CallActivity : CallBaseActivity() {
|
|||||||
private var signalingMessageReceiver: SignalingMessageReceiver? = null
|
private var signalingMessageReceiver: SignalingMessageReceiver? = null
|
||||||
private val internalSignalingMessageSender = InternalSignalingMessageSender()
|
private val internalSignalingMessageSender = InternalSignalingMessageSender()
|
||||||
private var signalingMessageSender: SignalingMessageSender? = null
|
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 offerAnswerNickProviders: MutableMap<String?, OfferAnswerNickProvider?> = HashMap()
|
||||||
private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
|
private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
|
||||||
private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
|
private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
|
||||||
@ -1119,6 +1129,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
localStream!!.addTrack(localVideoTrack)
|
localStream!!.addTrack(localVideoTrack)
|
||||||
localVideoTrack!!.setEnabled(false)
|
localVideoTrack!!.setEnabled(false)
|
||||||
localVideoTrack!!.addSink(binding!!.selfVideoRenderer)
|
localVideoTrack!!.addSink(binding!!.selfVideoRenderer)
|
||||||
|
localCallParticipantModel.isVideoEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun microphoneInitialization() {
|
private fun microphoneInitialization() {
|
||||||
@ -1129,12 +1140,12 @@ class CallActivity : CallBaseActivity() {
|
|||||||
localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource)
|
localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource)
|
||||||
localAudioTrack!!.setEnabled(false)
|
localAudioTrack!!.setEnabled(false)
|
||||||
localStream!!.addTrack(localAudioTrack)
|
localStream!!.addTrack(localAudioTrack)
|
||||||
|
localCallParticipantModel.isAudioEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun startMicInputDetection() {
|
private fun startMicInputDetection() {
|
||||||
if (permissionUtil!!.isMicrophonePermissionGranted() && micInputAudioRecordThread == null) {
|
if (permissionUtil!!.isMicrophonePermissionGranted() && micInputAudioRecordThread == null) {
|
||||||
var isSpeakingLongTerm = false
|
|
||||||
micInputAudioRecorder = AudioRecord(
|
micInputAudioRecorder = AudioRecord(
|
||||||
MediaRecorder.AudioSource.MIC,
|
MediaRecorder.AudioSource.MIC,
|
||||||
SAMPLE_RATE,
|
SAMPLE_RATE,
|
||||||
@ -1151,13 +1162,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
micInputAudioRecorder.read(byteArr, 0, byteArr.size)
|
micInputAudioRecorder.read(byteArr, 0, byteArr.size)
|
||||||
val isCurrentlySpeaking = abs(byteArr[0].toDouble()) > MICROPHONE_VALUE_THRESHOLD
|
val isCurrentlySpeaking = abs(byteArr[0].toDouble()) > MICROPHONE_VALUE_THRESHOLD
|
||||||
|
|
||||||
if (microphoneOn && isCurrentlySpeaking && !isSpeakingLongTerm) {
|
localCallParticipantModel.isSpeaking = isCurrentlySpeaking
|
||||||
isSpeakingLongTerm = true
|
|
||||||
sendIsSpeakingMessage(true)
|
|
||||||
} else if (!isCurrentlySpeaking && isSpeakingLongTerm) {
|
|
||||||
isSpeakingLongTerm = false
|
|
||||||
sendIsSpeakingMessage(false)
|
|
||||||
}
|
|
||||||
Thread.sleep(MICROPHONE_VALUE_SLEEP)
|
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? {
|
private fun createCameraCapturer(enumerator: CameraEnumerator?): VideoCapturer? {
|
||||||
val deviceNames = enumerator!!.deviceNames
|
val deviceNames = enumerator!!.deviceNames
|
||||||
|
|
||||||
@ -1330,12 +1315,9 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleMedia(enable: Boolean, video: Boolean) {
|
private fun toggleMedia(enable: Boolean, video: Boolean) {
|
||||||
var message: String
|
|
||||||
if (video) {
|
if (video) {
|
||||||
message = SIGNALING_MESSAGE_VIDEO_OFF
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
binding!!.cameraButton.alpha = OPACITY_ENABLED
|
binding!!.cameraButton.alpha = OPACITY_ENABLED
|
||||||
message = SIGNALING_MESSAGE_VIDEO_ON
|
|
||||||
startVideoCapture()
|
startVideoCapture()
|
||||||
} else {
|
} else {
|
||||||
binding!!.cameraButton.alpha = OPACITY_DISABLED
|
binding!!.cameraButton.alpha = OPACITY_DISABLED
|
||||||
@ -1349,6 +1331,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
if (localStream != null && localStream!!.videoTracks.size > 0) {
|
if (localStream != null && localStream!!.videoTracks.size > 0) {
|
||||||
localStream!!.videoTracks[0].setEnabled(enable)
|
localStream!!.videoTracks[0].setEnabled(enable)
|
||||||
|
localCallParticipantModel.isVideoEnabled = enable
|
||||||
}
|
}
|
||||||
if (enable) {
|
if (enable) {
|
||||||
binding!!.selfVideoRenderer.visibility = View.VISIBLE
|
binding!!.selfVideoRenderer.visibility = View.VISIBLE
|
||||||
@ -1356,29 +1339,14 @@ class CallActivity : CallBaseActivity() {
|
|||||||
binding!!.selfVideoRenderer.visibility = View.INVISIBLE
|
binding!!.selfVideoRenderer.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message = SIGNALING_MESSAGE_AUDIO_OFF
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
message = SIGNALING_MESSAGE_AUDIO_ON
|
|
||||||
binding!!.microphoneButton.alpha = OPACITY_ENABLED
|
binding!!.microphoneButton.alpha = OPACITY_ENABLED
|
||||||
} else {
|
} else {
|
||||||
binding!!.microphoneButton.alpha = OPACITY_DISABLED
|
binding!!.microphoneButton.alpha = OPACITY_DISABLED
|
||||||
}
|
}
|
||||||
if (localStream != null && localStream!!.audioTracks.size > 0) {
|
if (localStream != null && localStream!!.audioTracks.size > 0) {
|
||||||
localStream!!.audioTracks[0].setEnabled(enable)
|
localStream!!.audioTracks[0].setEnabled(enable)
|
||||||
}
|
localCallParticipantModel.isAudioEnabled = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1618,6 +1586,15 @@ class CallActivity : CallBaseActivity() {
|
|||||||
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
|
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
|
||||||
signalingMessageReceiver!!.addListener(offerMessageListener)
|
signalingMessageReceiver!!.addListener(offerMessageListener)
|
||||||
signalingMessageSender = internalSignalingMessageSender
|
signalingMessageSender = internalSignalingMessageSender
|
||||||
|
|
||||||
|
hasMCU = false
|
||||||
|
|
||||||
|
messageSender = MessageSenderNoMcu(
|
||||||
|
signalingMessageSender,
|
||||||
|
callParticipants.keys,
|
||||||
|
peerConnectionWrapperList
|
||||||
|
)
|
||||||
|
|
||||||
joinRoomAndCall()
|
joinRoomAndCall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1755,6 +1732,15 @@ class CallActivity : CallBaseActivity() {
|
|||||||
callParticipantList = CallParticipantList(signalingMessageReceiver)
|
callParticipantList = CallParticipantList(signalingMessageReceiver)
|
||||||
callParticipantList!!.addObserver(callParticipantListObserver)
|
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))
|
val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
|
||||||
ncApi!!.joinCall(
|
ncApi!!.joinCall(
|
||||||
credentials,
|
credentials,
|
||||||
@ -1903,6 +1889,26 @@ class CallActivity : CallBaseActivity() {
|
|||||||
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
|
signalingMessageReceiver!!.addListener(localParticipantMessageListener)
|
||||||
signalingMessageReceiver!!.addListener(offerMessageListener)
|
signalingMessageReceiver!!.addListener(offerMessageListener)
|
||||||
signalingMessageSender = webSocketClient!!.signalingMessageSender
|
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 {
|
} else {
|
||||||
if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
|
if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
|
||||||
webSocketClient!!.restartWebSocket()
|
webSocketClient!!.restartWebSocket()
|
||||||
@ -1928,6 +1934,25 @@ class CallActivity : CallBaseActivity() {
|
|||||||
when (webSocketCommunicationEvent.getType()) {
|
when (webSocketCommunicationEvent.getType()) {
|
||||||
"hello" -> {
|
"hello" -> {
|
||||||
Log.d(TAG, "onMessageEvent '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 (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) {
|
||||||
if (currentCallStatus === CallStatus.RECONNECTING) {
|
if (currentCallStatus === CallStatus.RECONNECTING) {
|
||||||
hangup(false, false)
|
hangup(false, false)
|
||||||
@ -2076,6 +2101,9 @@ class CallActivity : CallBaseActivity() {
|
|||||||
private fun hangupNetworkCalls(shutDownView: Boolean, endCallForAll: Boolean) {
|
private fun hangupNetworkCalls(shutDownView: Boolean, endCallForAll: Boolean) {
|
||||||
Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView")
|
Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView")
|
||||||
val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
|
val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
|
||||||
|
if (localStateBroadcaster != null) {
|
||||||
|
localStateBroadcaster!!.destroy()
|
||||||
|
}
|
||||||
if (callParticipantList != null) {
|
if (callParticipantList != null) {
|
||||||
callParticipantList!!.removeObserver(callParticipantListObserver)
|
callParticipantList!!.removeObserver(callParticipantListObserver)
|
||||||
callParticipantList!!.destroy()
|
callParticipantList!!.destroy()
|
||||||
@ -2136,8 +2164,6 @@ class CallActivity : CallBaseActivity() {
|
|||||||
unchanged: Collection<Participant>
|
unchanged: Collection<Participant>
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "handleCallParticipantsChanged")
|
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.
|
// The signaling session is the same as the Nextcloud session only when the MCU is not used.
|
||||||
var currentSessionId = callSession
|
var currentSessionId = callSession
|
||||||
@ -2422,6 +2448,9 @@ class CallActivity : CallBaseActivity() {
|
|||||||
callParticipantEventDisplayers[sessionId] = callParticipantEventDisplayer
|
callParticipantEventDisplayers[sessionId] = callParticipantEventDisplayer
|
||||||
callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler)
|
callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler)
|
||||||
runOnUiThread { addParticipantDisplayItem(callParticipantModel, "video") }
|
runOnUiThread { addParticipantDisplayItem(callParticipantModel, "video") }
|
||||||
|
|
||||||
|
localStateBroadcaster!!.handleCallParticipantAdded(callParticipant.callParticipantModel)
|
||||||
|
|
||||||
return callParticipant
|
return callParticipant
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2447,6 +2476,9 @@ class CallActivity : CallBaseActivity() {
|
|||||||
|
|
||||||
private fun removeCallParticipant(sessionId: String?) {
|
private fun removeCallParticipant(sessionId: String?) {
|
||||||
val callParticipant = callParticipants.remove(sessionId) ?: return
|
val callParticipant = callParticipants.remove(sessionId) ?: return
|
||||||
|
|
||||||
|
localStateBroadcaster!!.handleCallParticipantRemoved(callParticipant.callParticipantModel)
|
||||||
|
|
||||||
val screenParticipantDisplayItemManager = screenParticipantDisplayItemManagers.remove(sessionId)
|
val screenParticipantDisplayItemManager = screenParticipantDisplayItemManagers.remove(sessionId)
|
||||||
callParticipant.callParticipantModel.removeObserver(screenParticipantDisplayItemManager)
|
callParticipant.callParticipantModel.removeObserver(screenParticipantDisplayItemManager)
|
||||||
val callParticipantEventDisplayer = callParticipantEventDisplayers.remove(sessionId)
|
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 Y_POS_NO_CALL_INFO: Float = 20f
|
||||||
|
|
||||||
private const val SESSION_ID_PREFFIX_END: Int = 4
|
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 List<DataChannelMessage> pendingDataChannelMessages = new ArrayList<>();
|
||||||
private final SdpObserver sdpObserver;
|
private final SdpObserver sdpObserver;
|
||||||
|
|
||||||
private final boolean hasInitiated;
|
|
||||||
|
|
||||||
private final MediaStream localStream;
|
|
||||||
private final boolean isMCUPublisher;
|
private final boolean isMCUPublisher;
|
||||||
private final String videoStreamType;
|
private final String videoStreamType;
|
||||||
|
|
||||||
@ -113,14 +110,13 @@ public class PeerConnectionWrapper {
|
|||||||
boolean isMCUPublisher, boolean hasMCU, String videoStreamType,
|
boolean isMCUPublisher, boolean hasMCU, String videoStreamType,
|
||||||
SignalingMessageReceiver signalingMessageReceiver,
|
SignalingMessageReceiver signalingMessageReceiver,
|
||||||
SignalingMessageSender signalingMessageSender) {
|
SignalingMessageSender signalingMessageSender) {
|
||||||
this.localStream = localStream;
|
|
||||||
this.videoStreamType = videoStreamType;
|
this.videoStreamType = videoStreamType;
|
||||||
|
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.mediaConstraints = mediaConstraints;
|
this.mediaConstraints = mediaConstraints;
|
||||||
|
|
||||||
sdpObserver = new SdpObserver();
|
sdpObserver = new SdpObserver();
|
||||||
hasInitiated = sessionId.compareTo(localSession) < 0;
|
boolean hasInitiated = sessionId.compareTo(localSession) < 0;
|
||||||
this.isMCUPublisher = isMCUPublisher;
|
this.isMCUPublisher = isMCUPublisher;
|
||||||
|
|
||||||
PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServerList);
|
PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServerList);
|
||||||
@ -133,12 +129,12 @@ public class PeerConnectionWrapper {
|
|||||||
this.signalingMessageSender = signalingMessageSender;
|
this.signalingMessageSender = signalingMessageSender;
|
||||||
|
|
||||||
if (peerConnection != null) {
|
if (peerConnection != null) {
|
||||||
if (this.localStream != null) {
|
if (localStream != null) {
|
||||||
List<String> localStreamIds = Collections.singletonList(this.localStream.getId());
|
List<String> localStreamIds = Collections.singletonList(localStream.getId());
|
||||||
for(AudioTrack track : this.localStream.audioTracks) {
|
for(AudioTrack track : localStream.audioTracks) {
|
||||||
peerConnection.addTrack(track, localStreamIds);
|
peerConnection.addTrack(track, localStreamIds);
|
||||||
}
|
}
|
||||||
for(VideoTrack track : this.localStream.videoTracks) {
|
for(VideoTrack track : localStream.videoTracks) {
|
||||||
peerConnection.addTrack(track, localStreamIds);
|
peerConnection.addTrack(track, localStreamIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,22 +325,6 @@ public class PeerConnectionWrapper {
|
|||||||
return sessionId;
|
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() {
|
public boolean isMCUPublisher() {
|
||||||
return isMCUPublisher;
|
return isMCUPublisher;
|
||||||
}
|
}
|
||||||
@ -432,10 +412,6 @@ public class PeerConnectionWrapper {
|
|||||||
}
|
}
|
||||||
pendingDataChannelMessages.clear();
|
pendingDataChannelMessages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataChannel.state() == DataChannel.State.OPEN) {
|
|
||||||
sendInitialMediaStatus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,11 +499,6 @@ public class PeerConnectionWrapper {
|
|||||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||||
|
|
||||||
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
|
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
|
||||||
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
|
|
||||||
if (hasInitiated) {
|
|
||||||
sendInitialMediaStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConnectionNotifier.notifyIceConnectionStateChanged(iceConnectionState);
|
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