Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PBE-6219] Make number of participants more accurate in call session (lobby) #1220

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -270,23 +271,26 @@ 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,
)
}
}

@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),
)
Expand Down Expand Up @@ -384,8 +388,7 @@ private fun CallLobbyBodyPreview() {
onToggleMicrophone = {},
onToggleCamera = {},
) {
LobbyDescriptionContent(participantsSize = 0) {
}
LobbyDescriptionContent(participantCounts = ParticipantCount(1, 1)) {}
}
}
}
2 changes: 1 addition & 1 deletion demo-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<string name="join_description">Start a new call, join a meeting by \nentering the call ID or by scanning \na QR code.</string>
<string name="join_call">Join Call</string>
<string name="join_call_by_qr_code">Scan QR meeting code</string>
<string name="join_call_description">You are about to join a call. %d more people are in the call.</string>
<string name="join_call_description">You are about to join a call. %d more people are in the call (%d anonymous).</string>
<string name="join_call_no_id_hint">Don\'t have a Call ID?</string>
<string name="join_call_call_id_hint">Call ID</string>
<string name="start_a_new_call">Start a New Call</string>
Expand Down
44 changes: 35 additions & 9 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (ILjava/lang/String;Lorg/threeten/bp/OffsetDateTime;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (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 <init> (Ljava/lang/String;Lorg/threeten/bp/OffsetDateTime;Lorg/openapitools/client/models/CallParticipantResponse;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Lorg/threeten/bp/OffsetDateTime;Lorg/openapitools/client/models/CallParticipantResponse;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -7242,23 +7266,25 @@ public final class org/openapitools/client/models/CallSessionParticipantLeftEven
}

public final class org/openapitools/client/models/CallSessionResponse {
public fun <init> (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 <init> (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 <init> (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 <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -772,7 +773,7 @@ public class CallState(
}

is SFUHealthCheckEvent -> {
call.state._participantCounts.value = event.participantCount
updateParticipantCounts(sfuHealthCheckEvent = event)
}

is ICETrickleEvent -> {
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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<PinUpdate>) {
// 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)
}
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
aleksandar-apostolov marked this conversation as resolved.
Show resolved Hide resolved

@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<kotlin.String, kotlin.Int>,

/* 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.openapitools.client.infrastructure.Serializer
*
*
* @param acceptedBy
* @param anonymousParticipantCount
* @param id
* @param missedBy
* @param participants
Expand All @@ -58,6 +59,9 @@ data class CallSessionResponse (
@Json(name = "accepted_by")
val acceptedBy: kotlin.collections.Map<kotlin.String, org.threeten.bp.OffsetDateTime>,

@Json(name = "anonymous_participant_count")
val anonymousParticipantCount: kotlin.Int,

@Json(name = "id")
val id: kotlin.String,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ class VideoEventAdapter : JsonAdapter<VideoEvent>() {
"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
Expand Down
Loading