From b5e11da11101a2795f9c757b5bf27eed0b515046 Mon Sep 17 00:00:00 2001
From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com>
Date: Thu, 31 Oct 2024 17:26:57 +0200
Subject: [PATCH] Improve participant count logic
---
.../video/android/ui/lobby/CallLobbyScreen.kt | 19 ++++---
demo-app/src/main/res/values/strings.xml | 2 +-
.../getstream/video/android/core/CallState.kt | 51 ++++++++++++++-----
3 files changed, 51 insertions(+), 21 deletions(-)
diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt
index 91e0c44805..3633c583f2 100644
--- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt
+++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt
@@ -73,6 +73,7 @@ import io.getstream.video.android.compose.ui.components.call.lobby.CallLobby
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.call.state.ToggleCamera
import io.getstream.video.android.core.call.state.ToggleMicrophone
+import io.getstream.video.android.core.events.ParticipantCount
import io.getstream.video.android.mock.StreamPreviewDataUtils
import io.getstream.video.android.mock.previewCall
import io.getstream.video.android.mock.previewUsers
@@ -270,10 +271,9 @@ private fun CallLobbyBody(
private fun LobbyDescription(
callLobbyViewModel: CallLobbyViewModel,
) {
- val session by callLobbyViewModel.call.state.session.collectAsState()
- val participantsSize = session?.participants?.size ?: 0
+ val participantCounts by callLobbyViewModel.call.state.participantCounts.collectAsState()
- LobbyDescriptionContent(participantsSize = participantsSize) {
+ LobbyDescriptionContent(participantCounts = participantCounts) {
callLobbyViewModel.handleUiEvent(
CallLobbyEvent.JoinCall,
)
@@ -281,12 +281,16 @@ private fun LobbyDescription(
}
@Composable
-private fun LobbyDescriptionContent(participantsSize: Int, onClick: () -> Unit) {
- val text = if (participantsSize > 0) {
+private fun LobbyDescriptionContent(participantCounts: ParticipantCount?, onClick: () -> Unit) {
+ val totalParticipants = participantCounts?.total ?: 0
+ val anonParticipants = participantCounts?.anonymous ?: 0
+
+ val text = if (totalParticipants != 0) {
Pair(
stringResource(
id = R.string.join_call_description,
- participantsSize,
+ totalParticipants,
+ anonParticipants,
),
stringResource(id = R.string.join_call),
)
@@ -384,8 +388,7 @@ private fun CallLobbyBodyPreview() {
onToggleMicrophone = {},
onToggleCamera = {},
) {
- LobbyDescriptionContent(participantsSize = 0) {
- }
+ LobbyDescriptionContent(participantCounts = ParticipantCount(1, 1)) {}
}
}
}
diff --git a/demo-app/src/main/res/values/strings.xml b/demo-app/src/main/res/values/strings.xml
index eb8dd6e050..d0edf9b6db 100644
--- a/demo-app/src/main/res/values/strings.xml
+++ b/demo-app/src/main/res/values/strings.xml
@@ -31,7 +31,7 @@
Start a new call, join a meeting by \nentering the call ID or by scanning \na QR code.
Join Call
Scan QR meeting code
- You are about to join a call. %d more people are in the call.
+ You are about to join a call. %d more people are in the call (%d anonymous).
Don\'t have a Call ID?
Call ID
Start a New Call
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
index ccd72ea61d..d8107d3b20 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
@@ -773,7 +773,7 @@ public class CallState(
}
is SFUHealthCheckEvent -> {
- call.state._participantCounts.value = event.participantCount
+ updateParticipantCounts(sfuHealthCheckEvent = event)
}
is ICETrickleEvent -> {
@@ -877,23 +877,26 @@ public class CallState(
)
}
- // When in JOINED state, we should use the participant from SFU health check event, as it's more accurate.
- if (connection.value !is RealtimeConnection.Joined) {
- _participantCounts.value = ParticipantCount(
- total = event.anonymousParticipantCount + event.participantsCountByRole.values.sum(),
- anonymous = event.anonymousParticipantCount,
- )
- }
+ updateParticipantCounts(session = session.value)
}
is CallSessionParticipantLeftEvent -> {
_session.value?.let { callSessionResponse ->
val newList = callSessionResponse.participants.toMutableList()
newList.removeIf { it.userSessionId == event.participant.userSessionId }
+
+ val newMap = callSessionResponse.participantsCountByRole.toMutableMap()
+ newMap
+ .computeIfPresent(event.participant.role) { _, v -> maxOf(v - 1, 0) }
+ .also { if (it == 0) newMap.remove(event.participant.role) }
+
_session.value = callSessionResponse.copy(
- participants = newList.toList(),
+ participants = newList,
+ participantsCountByRole = newMap,
)
}
+
+ updateParticipantCounts(session = session.value)
}
is CallSessionParticipantJoinedEvent -> {
@@ -902,26 +905,37 @@ public class CallState(
val participant = CallParticipantResponse(
user = event.participant.user,
joinedAt = event.createdAt,
- role = "user",
+ role = event.participant.user.role,
userSessionId = event.participant.userSessionId,
)
+ val newMap = callSessionResponse.participantsCountByRole.toMutableMap()
+ newMap.merge(event.participant.role, 1, Int::plus)
+
+ // It could happen that the backend delivers the same participant more than once.
+ // Once with the call.session_started event and once again with the
+ // call.session_participant_joined event. In this case,
+ // we should update the existing participant and prevent duplicating it.
val index = newList.indexOfFirst { user.id == event.participant.user.id }
if (index == -1) {
newList.add(participant)
} else {
newList[index] = participant
}
+
_session.value = callSessionResponse.copy(
- participants = newList.toList(),
+ participants = newList,
+ participantsCountByRole = newMap,
)
}
+
+ updateParticipantCounts(session = session.value)
updateRingingState()
}
}
}
private fun updateServerSidePins(pins: List) {
- // Update particioants that are still in the call
+ // Update participants that are still in the call
val pinnedInCall = pins.filter {
_participants.value.containsKey(it.sessionId)
}
@@ -1045,6 +1059,19 @@ public class CallState(
upsertParticipants(participantStates)
}
+ private fun updateParticipantCounts(session: CallSessionResponse? = null, sfuHealthCheckEvent: SFUHealthCheckEvent? = null) {
+ // When in JOINED state, we should use the participant from SFU health check event, as it's more accurate.
+
+ if (sfuHealthCheckEvent != null) {
+ _participantCounts.value = sfuHealthCheckEvent.participantCount
+ } else if (session != null && connection.value !is RealtimeConnection.Joined) {
+ _participantCounts.value = ParticipantCount(
+ total = session.anonymousParticipantCount + session.participantsCountByRole.values.sum(),
+ anonymous = session.anonymousParticipantCount,
+ )
+ }
+ }
+
fun markSpeakingAsMuted() {
_speakingWhileMuted.value = true
speakingWhileMutedResetJob?.cancel()