Add helper class to send messages to call participants

For now it just provides support for sending a data channel message to
all participants, so notifying all participants when the media is
toggled or the speaking status change can be directly refactored to use
it.

While it would have been fine to use a single class for both MCU and no
MCU they were split for easier and cleaner unit testing in future
stages.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2024-10-25 14:37:37 +02:00 committed by Marcel Hibbe
parent 8fa3224879
commit 3e36c85015
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
6 changed files with 257 additions and 24 deletions

View File

@ -63,6 +63,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.call.CallParticipant
import com.nextcloud.talk.call.CallParticipantList
import com.nextcloud.talk.call.CallParticipantModel
import com.nextcloud.talk.call.MessageSender
import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.data.user.model.User
@ -242,6 +245,7 @@ class CallActivity : CallBaseActivity() {
private var signalingMessageReceiver: SignalingMessageReceiver? = null
private val internalSignalingMessageSender = InternalSignalingMessageSender()
private var signalingMessageSender: SignalingMessageSender? = null
private var messageSender: MessageSender? = null
private val offerAnswerNickProviders: MutableMap<String?, OfferAnswerNickProvider?> = HashMap()
private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
@ -1172,18 +1176,7 @@ class CallActivity : CallBaseActivity() {
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
}
}
}
messageSender!!.sendToAll(DataChannelMessage(isSpeakingMessage))
}
}
@ -1368,18 +1361,7 @@ class CallActivity : CallBaseActivity() {
}
}
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
}
}
}
messageSender!!.sendToAll(DataChannelMessage(message))
}
}
@ -1621,6 +1603,10 @@ class CallActivity : CallBaseActivity() {
hasMCU = false
messageSender = MessageSenderNoMcu(
peerConnectionWrapperList
)
joinRoomAndCall()
}
}
@ -1911,6 +1897,17 @@ class CallActivity : CallBaseActivity() {
// 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(
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
peerConnectionWrapperList
)
}
} else {
if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
webSocketClient!!.restartWebSocket()
@ -1940,6 +1937,17 @@ class CallActivity : CallBaseActivity() {
hasMCU = webSocketClient!!.hasMCU()
Log.d(TAG, "hasMCU is $hasMCU")
if (hasMCU) {
messageSender = MessageSenderMcu(
peerConnectionWrapperList,
webSocketClient!!.sessionId
)
} else {
messageSender = MessageSenderNoMcu(
peerConnectionWrapperList
)
}
if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) {
if (currentCallStatus === CallStatus.RECONNECTING) {
hangup(false, false)

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
import java.util.List;
/**
* 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, 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).
*/
public abstract class MessageSender {
protected final List<PeerConnectionWrapper> peerConnectionWrappers;
public MessageSender(List<PeerConnectionWrapper> peerConnectionWrappers) {
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);
protected PeerConnectionWrapper getPeerConnectionWrapper(String sessionId) {
for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) {
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
return peerConnectionWrapper;
}
}
return null;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.webrtc.PeerConnectionWrapper;
import java.util.List;
/**
* Helper class to send messages to participants in a call when an MCU is used.
*/
public class MessageSenderMcu extends MessageSender {
private final String ownSessionId;
public MessageSenderMcu(List<PeerConnectionWrapper> peerConnectionWrappers,
String ownSessionId) {
super(peerConnectionWrappers);
this.ownSessionId = ownSessionId;
}
public void sendToAll(DataChannelMessage dataChannelMessage) {
PeerConnectionWrapper ownPeerConnectionWrapper = getPeerConnectionWrapper(ownSessionId);
if (ownPeerConnectionWrapper != null) {
ownPeerConnectionWrapper.send(dataChannelMessage);
}
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.webrtc.PeerConnectionWrapper;
import java.util.List;
/**
* Helper class to send messages to participants in a call when an MCU is not used.
*/
public class MessageSenderNoMcu extends MessageSender {
public MessageSenderNoMcu(List<PeerConnectionWrapper> peerConnectionWrappers) {
super(peerConnectionWrappers);
}
public void sendToAll(DataChannelMessage dataChannelMessage) {
for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) {
peerConnectionWrapper.send(dataChannelMessage);
}
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.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 ownPeerConnectionWrapper: PeerConnectionWrapper? = null
private var messageSender: MessageSenderMcu? = null
@Before
fun setUp() {
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)
ownPeerConnectionWrapper = Mockito.mock(PeerConnectionWrapper::class.java)
Mockito.`when`(ownPeerConnectionWrapper!!.sessionId).thenReturn("ownSessionId")
Mockito.`when`(ownPeerConnectionWrapper!!.videoStreamType).thenReturn("video")
peerConnectionWrappers!!.add(ownPeerConnectionWrapper)
messageSender = MessageSenderMcu(peerConnectionWrappers, "ownSessionId")
}
@Test
fun testSendDataChannelMessageToAll() {
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(ownPeerConnectionWrapper!!).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
}
@Test
fun testSendDataChannelMessageToAllWithoutOwnPeerConnection() {
peerConnectionWrappers!!.remove(ownPeerConnectionWrapper)
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message)
Mockito.verify(peerConnectionWrapper1!!, never()).send(message)
Mockito.verify(peerConnectionWrapper2!!, never()).send(message)
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.webrtc.PeerConnectionWrapper
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
class MessageSenderNoMcuTest {
private var peerConnectionWrappers: MutableList<PeerConnectionWrapper?>? = null
private var peerConnectionWrapper1: PeerConnectionWrapper? = null
private var peerConnectionWrapper2: PeerConnectionWrapper? = null
private var messageSender: MessageSenderNoMcu? = null
@Before
fun setUp() {
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)
messageSender = MessageSenderNoMcu(peerConnectionWrappers)
}
@Test
fun testSendDataChannelMessageToAll() {
val message = DataChannelMessage()
messageSender!!.sendToAll(message)
Mockito.verify(peerConnectionWrapper1!!).send(message)
Mockito.verify(peerConnectionWrapper2!!).send(message)
}
}