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()