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/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api
index caa2c7366d..3659f4d257 100644
--- a/stream-video-android-core/api/stream-video-android-core.api
+++ b/stream-video-android-core/api/stream-video-android-core.api
@@ -7197,6 +7197,30 @@ public final class org/openapitools/client/models/CallSessionEndedEvent : org/op
public fun toString ()Ljava/lang/String;
}
+public final class org/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent : org/openapitools/client/models/VideoEvent, org/openapitools/client/models/WSCallEvent {
+ public fun (ILjava/lang/String;Lorg/threeten/bp/OffsetDateTime;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)V
+ public synthetic fun (ILjava/lang/String;Lorg/threeten/bp/OffsetDateTime;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()I
+ public final fun component2 ()Ljava/lang/String;
+ public final fun component3 ()Lorg/threeten/bp/OffsetDateTime;
+ public final fun component4 ()Ljava/util/Map;
+ public final fun component5 ()Ljava/lang/String;
+ public final fun component6 ()Ljava/lang/String;
+ public final fun copy (ILjava/lang/String;Lorg/threeten/bp/OffsetDateTime;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)Lorg/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent;
+ public static synthetic fun copy$default (Lorg/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent;ILjava/lang/String;Lorg/threeten/bp/OffsetDateTime;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAnonymousParticipantCount ()I
+ public fun getCallCID ()Ljava/lang/String;
+ public final fun getCallCid ()Ljava/lang/String;
+ public final fun getCreatedAt ()Lorg/threeten/bp/OffsetDateTime;
+ public fun getEventType ()Ljava/lang/String;
+ public final fun getParticipantsCountByRole ()Ljava/util/Map;
+ public final fun getSessionId ()Ljava/lang/String;
+ public final fun getType ()Ljava/lang/String;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
public final class org/openapitools/client/models/CallSessionParticipantJoinedEvent : org/openapitools/client/models/VideoEvent, org/openapitools/client/models/WSCallEvent {
public fun (Ljava/lang/String;Lorg/threeten/bp/OffsetDateTime;Lorg/openapitools/client/models/CallParticipantResponse;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun (Ljava/lang/String;Lorg/threeten/bp/OffsetDateTime;Lorg/openapitools/client/models/CallParticipantResponse;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -7242,23 +7266,25 @@ public final class org/openapitools/client/models/CallSessionParticipantLeftEven
}
public final class org/openapitools/client/models/CallSessionResponse {
- public fun (Ljava/util/Map;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;)V
- public synthetic fun (Ljava/util/Map;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Ljava/util/Map;ILjava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;)V
+ public synthetic fun (Ljava/util/Map;ILjava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/Map;
public final fun component10 ()Lorg/threeten/bp/OffsetDateTime;
public final fun component11 ()Lorg/threeten/bp/OffsetDateTime;
- public final fun component2 ()Ljava/lang/String;
- public final fun component3 ()Ljava/util/Map;
- public final fun component4 ()Ljava/util/List;
- public final fun component5 ()Ljava/util/Map;
+ public final fun component12 ()Lorg/threeten/bp/OffsetDateTime;
+ public final fun component2 ()I
+ public final fun component3 ()Ljava/lang/String;
+ public final fun component4 ()Ljava/util/Map;
+ public final fun component5 ()Ljava/util/List;
public final fun component6 ()Ljava/util/Map;
- public final fun component7 ()Lorg/threeten/bp/OffsetDateTime;
+ public final fun component7 ()Ljava/util/Map;
public final fun component8 ()Lorg/threeten/bp/OffsetDateTime;
public final fun component9 ()Lorg/threeten/bp/OffsetDateTime;
- public final fun copy (Ljava/util/Map;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;)Lorg/openapitools/client/models/CallSessionResponse;
- public static synthetic fun copy$default (Lorg/openapitools/client/models/CallSessionResponse;Ljava/util/Map;Ljava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;ILjava/lang/Object;)Lorg/openapitools/client/models/CallSessionResponse;
+ public final fun copy (Ljava/util/Map;ILjava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;)Lorg/openapitools/client/models/CallSessionResponse;
+ public static synthetic fun copy$default (Lorg/openapitools/client/models/CallSessionResponse;Ljava/util/Map;ILjava/lang/String;Ljava/util/Map;Ljava/util/List;Ljava/util/Map;Ljava/util/Map;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;ILjava/lang/Object;)Lorg/openapitools/client/models/CallSessionResponse;
public fun equals (Ljava/lang/Object;)Z
public final fun getAcceptedBy ()Ljava/util/Map;
+ public final fun getAnonymousParticipantCount ()I
public final fun getEndedAt ()Lorg/threeten/bp/OffsetDateTime;
public final fun getId ()Ljava/lang/String;
public final fun getLiveEndedAt ()Lorg/threeten/bp/OffsetDateTime;
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 aef7bd876b..cc1f49bbe7 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
@@ -87,6 +87,7 @@ import org.openapitools.client.models.CallRejectedEvent
import org.openapitools.client.models.CallResponse
import org.openapitools.client.models.CallRingEvent
import org.openapitools.client.models.CallSessionEndedEvent
+import org.openapitools.client.models.CallSessionParticipantCountsUpdatedEvent
import org.openapitools.client.models.CallSessionParticipantJoinedEvent
import org.openapitools.client.models.CallSessionParticipantLeftEvent
import org.openapitools.client.models.CallSessionResponse
@@ -772,7 +773,7 @@ public class CallState(
}
is SFUHealthCheckEvent -> {
- call.state._participantCounts.value = event.participantCount
+ updateParticipantCounts(sfuHealthCheckEvent = event)
}
is ICETrickleEvent -> {
@@ -868,14 +869,34 @@ public class CallState(
_session.value = event.call.session
}
+ is CallSessionParticipantCountsUpdatedEvent -> {
+ _session.value?.let {
+ _session.value = it.copy(
+ participantsCountByRole = event.participantsCountByRole,
+ anonymousParticipantCount = 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 -> {
@@ -884,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)
}
@@ -1027,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()
diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent.kt
new file mode 100644
index 0000000000..2487c30568
--- /dev/null
+++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionParticipantCountsUpdatedEvent.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress(
+ "ArrayInDataClass",
+ "EnumEntryName",
+ "RemoveRedundantQualifierName",
+ "UnusedImport"
+)
+
+package org.openapitools.client.models
+
+
+
+
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.ToJson
+import org.openapitools.client.infrastructure.Serializer
+
+/**
+ * This event is sent when the participant counts in a call session are updated
+ *
+ * @param anonymousParticipantCount
+ * @param callCid
+ * @param createdAt
+ * @param participantsCountByRole
+ * @param sessionId Call session ID
+ * @param type The type of event: \"call.session_participant_count_updated\" in this case
+ */
+
+
+data class CallSessionParticipantCountsUpdatedEvent (
+
+ @Json(name = "anonymous_participant_count")
+ val anonymousParticipantCount: kotlin.Int,
+
+ @Json(name = "call_cid")
+ val callCid: kotlin.String,
+
+ @Json(name = "created_at")
+ val createdAt: org.threeten.bp.OffsetDateTime,
+
+ @Json(name = "participants_count_by_role")
+ val participantsCountByRole: kotlin.collections.Map,
+
+ /* Call session ID */
+ @Json(name = "session_id")
+ val sessionId: kotlin.String,
+
+ /* The type of event: \"call.session_participant_count_updated\" in this case */
+ @Json(name = "type")
+ val type: kotlin.String = "call.session_participant_count_updated"
+
+) : VideoEvent(), WSCallEvent {
+
+ override fun getCallCID(): String {
+ return callCid
+ }
+
+ override fun getEventType(): String {
+ return type
+ }
+}
diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionResponse.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionResponse.kt
index f238f0c453..5eef60e403 100644
--- a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionResponse.kt
+++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/CallSessionResponse.kt
@@ -40,6 +40,7 @@ import org.openapitools.client.infrastructure.Serializer
*
*
* @param acceptedBy
+ * @param anonymousParticipantCount
* @param id
* @param missedBy
* @param participants
@@ -58,6 +59,9 @@ data class CallSessionResponse (
@Json(name = "accepted_by")
val acceptedBy: kotlin.collections.Map,
+ @Json(name = "anonymous_participant_count")
+ val anonymousParticipantCount: kotlin.Int,
+
@Json(name = "id")
val id: kotlin.String,
diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt
index caaa237fda..480557f5d4 100644
--- a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt
+++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt
@@ -141,6 +141,7 @@ class VideoEventAdapter : JsonAdapter() {
"call.rejected" -> CallRejectedEvent::class.java
"call.ring" -> CallRingEvent::class.java
"call.session_ended" -> CallSessionEndedEvent::class.java
+ "call.session_participant_count_updated" -> CallSessionParticipantCountsUpdatedEvent::class.java
"call.session_participant_joined" -> CallSessionParticipantJoinedEvent::class.java
"call.session_participant_left" -> CallSessionParticipantLeftEvent::class.java
"call.session_started" -> CallSessionStartedEvent::class.java