From 2bed3e3a834fc7b64965f97b517640c844dda60b Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:16:24 +0200 Subject: [PATCH 01/10] Add base implementation --- .../getstream/video/android/CallActivity.kt | 4 ++ .../video/android/ui/call/CallScreen.kt | 5 +++ .../video/android/ui/menu/MenuDefinitions.kt | 16 ++++++++ .../video/android/ui/menu/SettingsMenu.kt | 4 ++ .../video/android/ui/menu/base/DynamicMenu.kt | 2 + .../api/stream-video-android-core.api | 3 ++ .../io/getstream/video/android/core/Call.kt | 20 ++++++++++ .../getstream/video/android/core/CallStats.kt | 4 +- .../video/android/core/call/RtcSession.kt | 40 ++++++++++++++----- 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt index 2155e82c31..46f09dee95 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt @@ -19,10 +19,14 @@ package io.getstream.video.android import android.content.Intent import android.os.Bundle import android.os.PersistableBundle +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.querysort.QuerySortByField diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index de97cbf15c..3cefcd1066 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -111,6 +111,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.openapitools.client.models.OwnCapability +import org.openapitools.client.models.VideoResolution @OptIn(ExperimentalMaterialApi::class) @Composable @@ -504,6 +505,10 @@ fun CallScreen( onNoiseCancellation = { isNoiseCancellationEnabled = call.toggleAudioProcessing() }, + onIncomingResolutionChanged = { + call.setPreferredIncomingVideoResolution(it) + isShowingSettingMenu = false + }, onSelectScaleType = { preferredScaleType = it isShowingSettingMenu = false diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index df032d9791..7708b7cb0c 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -47,6 +47,7 @@ import io.getstream.video.android.ui.menu.base.ActionMenuItem import io.getstream.video.android.ui.menu.base.DynamicSubMenuItem import io.getstream.video.android.ui.menu.base.MenuItem import io.getstream.video.android.ui.menu.base.SubMenuItem +import org.openapitools.client.models.VideoResolution /** * Defines the default Stream menu for the demo app. @@ -66,6 +67,7 @@ fun defaultStreamMenu( onSwitchSfuClick: () -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, + onIncomingResolutionChanged: (VideoResolution) -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, onSfuRejoinClick: () -> Unit, onSfuFastReconnectClick: () -> Unit, @@ -130,6 +132,20 @@ fun defaultStreamMenu( ), ) } + add( + ActionMenuItem( + title = "Change incoming resolution (144)", + icon = Icons.Default.AspectRatio, + action = { onIncomingResolutionChanged(VideoResolution(height = 144, width = 144)) }, + ) + ) + add( + ActionMenuItem( + title = "Change incoming resolution (2160p)", + icon = Icons.Default.AspectRatio, + action = { onIncomingResolutionChanged(VideoResolution(2160, 3840)) }, + ) + ) if (showDebugOptions) { add( SubMenuItem( diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 7429927535..9ea8d2b6f0 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -61,6 +61,7 @@ import io.getstream.video.android.ui.menu.base.DynamicMenu import io.getstream.video.android.ui.menu.base.MenuItem import io.getstream.video.android.util.filters.SampleAudioFilter import kotlinx.coroutines.launch +import org.openapitools.client.models.VideoResolution import java.nio.ByteBuffer @OptIn(ExperimentalPermissionsApi::class) @@ -75,6 +76,7 @@ internal fun SettingsMenu( onSelectVideoFilter: (Int) -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, + onIncomingResolutionChanged: (VideoResolution) -> Unit, onShowCallStats: () -> Unit, onSelectScaleType: (VideoScalingType) -> Unit, ) { @@ -236,6 +238,7 @@ internal fun SettingsMenu( onSwitchSfuClick = onSwitchSfuClick, onShowCallStats = onShowCallStats, onNoiseCancellation = onNoiseCancellation, + onIncomingResolutionChanged = { onIncomingResolutionChanged(it) }, onSfuRejoinClick = onSfuRejoinClick, onSfuFastReconnectClick = onSfuFastReconnectClick, isScreenShareEnabled = isScreenSharing, @@ -303,6 +306,7 @@ private fun SettingsMenuPreview() { onShowFeedback = {}, onSelectScaleType = {}, onNoiseCancellation = {}, + onIncomingResolutionChanged = {}, loadRecordings = { emptyList() }, ), ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt index 463ca2625a..b1da1f8d31 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -225,6 +225,7 @@ private fun DynamicMenuPreview() { onDeviceSelected = {}, onShowFeedback = {}, onNoiseCancellation = {}, + onIncomingResolutionChanged = {}, onSelectScaleType = {}, loadRecordings = { emptyList() }, ), @@ -255,6 +256,7 @@ private fun DynamicMenuDebugOptionPreview() { onShowFeedback = {}, onSelectScaleType = { }, onNoiseCancellation = {}, + onIncomingResolutionChanged = {}, loadRecordings = { emptyList() }, ), ) 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 2c865fa4c5..647b82958f 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -65,6 +65,9 @@ public final class io/getstream/video/android/core/Call { public static synthetic fun sendReaction$default (Lio/getstream/video/android/core/Call;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun setAudioFilter (Lio/getstream/video/android/core/call/audio/InputAudioFilter;)V public final fun setAudioProcessingEnabled (Z)V + public final fun setIncomingVideoEnabled (Z)V + public final fun setPreferredIncomingVideoResolution (Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;)V + public static synthetic fun setPreferredIncomingVideoResolution$default (Lio/getstream/video/android/core/Call;Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;ILjava/lang/Object;)V public final fun setSessionId (Ljava/lang/String;)V public final fun setVideoFilter (Lio/getstream/video/android/core/call/video/VideoFilter;)V public final fun setVisibility (Ljava/lang/String;Lstream/video/sfu/models/TrackType;Z)V diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 30b61976fe..c4ed30aeed 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -81,6 +81,7 @@ import org.openapitools.client.models.UpdateCallRequest import org.openapitools.client.models.UpdateCallResponse import org.openapitools.client.models.UpdateUserPermissionsResponse import org.openapitools.client.models.VideoEvent +import org.openapitools.client.models.VideoResolution import org.openapitools.client.models.VideoSettingsResponse import org.threeten.bp.OffsetDateTime import org.webrtc.PeerConnection @@ -1247,6 +1248,25 @@ public class Call( return clientImpl.toggleAudioProcessing() } + fun setPreferredIncomingVideoResolution(resolution: VideoResolution, sessionIds: List? = null) { + val targetSessionIds = sessionIds ?: state.remoteParticipants.value.map { it.sessionId } + val overrides = targetSessionIds.associateWith { resolution } + +// session?.setVideoSubscriptions(manualResolutionOverrides = overrides) + // TODO-mqs: test with several incoming video tracks + targetSessionIds.forEach { sessionId -> + session?.updateTrackDimensions( + sessionId, + TrackType.TRACK_TYPE_VIDEO, + true, + VideoDimension(resolution.width, resolution.height), + ) + } + } + + fun setIncomingVideoEnabled(enabled: Boolean) { + } + @InternalStreamVideoApi public val debug = Debug(this) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallStats.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallStats.kt index ef66bfa5de..08c466816b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallStats.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallStats.kt @@ -195,8 +195,8 @@ public class CallStats(val call: Call, val callScope: CoroutineScope) { val received = it.members["framesReceived"] as? Long val duration = it.members["totalFramesDuration"] as? Long if (participantId != null) { - logger.i { - "receiving video for $participantId at $frameWidth: ${it.members["frameWidth"]} and rendering it at ${visibleAt?.dimensions?.width} visible: ${visibleAt?.visible}" + logger.v { + "[stats] #manual-quality-selection; receiving video for $participantId at $frameWidth: ${it.members["frameWidth"]} and rendering it at ${visibleAt?.dimensions?.width} visible: ${visibleAt?.visible}" } } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 0287768d4f..6f14a5c8eb 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -95,6 +95,7 @@ import kotlinx.serialization.json.Json import okio.IOException import org.openapitools.client.models.OwnCapability import org.openapitools.client.models.VideoEvent +import org.openapitools.client.models.VideoResolution import org.webrtc.CameraEnumerationAndroid.CaptureFormat import org.webrtc.MediaConstraints import org.webrtc.MediaStream @@ -1050,12 +1051,15 @@ public class RtcSession internal constructor( * - it sends the resolutions we're displaying the video at so the SFU can decide which track to send * - when switching SFU we should repeat this info * - http calls failing here breaks the call. (since you won't receive the video) - * - we should retry continously until it works and after it continues to fail, raise an error that shuts down the call + * - we should retry continuously until it works and after it continues to fail, raise an error that shuts down the call * - we retry when: * -- error isn't permanent, SFU didn't change, the mute/publish state didn't change * -- we cap at 30 retries to prevent endless loops */ - private fun setVideoSubscriptions(useDefaults: Boolean = false) { + internal fun setVideoSubscriptions( + useDefaults: Boolean = false, + manualResolutionOverrides: Map = emptyMap(), + ) { logger.d { "[setVideoSubscriptions] #sfu; #track; useDefaults: $useDefaults" } // default is to subscribe to the top 5 sorted participants var tracks = if (useDefaults) { @@ -1064,18 +1068,29 @@ public class RtcSession internal constructor( // if we're not using the default, sub to visible tracks visibleTracks() } + // TODO-mqs: test default tracks and scrolling // TODO: // This is a hotfix to help with performance. Most devices struggle even with H resolution // if there are more than 2 remote participants and especially if there is a H264 participant. // We just report a very small window so force SFU to deliver us Q resolution. This will // be later less visible to the user once we make the participant grid smaller - if (tracks.size > 2) { - tracks = tracks.map { - it.copy(dimension = it.dimension?.copy(width = 200, height = 200)) - } +// if (tracks.size > 2) { +// tracks = tracks.map { +// it.copy(dimension = it.dimension?.copy(width = 200, height = 200)) +// } +// } + + logger.v { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; manualResolutionOverrides: $manualResolutionOverrides" } - logger.v { "[setVideoSubscriptions] #sfu; #track; tracks.size: ${tracks.size}" } + + // TODO-mqs: store overrides +// tracks = tracks.map { +// manualResolutionOverrides[it.session_id]?.let { resolution -> +// it.copy(dimension = VideoDimension(resolution.width, resolution.height)) +// } ?: it +// } val new = tracks.toList() subscriptions.value = new @@ -1092,12 +1107,17 @@ public class RtcSession internal constructor( session_id = sessionId, tracks = subscriptions.value, ) - println("request $request") + dynascaleLogger.d { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; UpdateSubscriptionsRequest: $request" + } val sessionToDimension = tracks.map { it.session_id to it.dimension } dynascaleLogger.v { - "[setVideoSubscriptions] $useDefaults #sfu; #track; $sessionId subscribing to : $sessionToDimension" + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Subscribing to: $sessionToDimension" } val result = updateSubscriptions(request) + dynascaleLogger.v { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Result: $result" + } emit(result.getOrThrow()) }.flowOn(DispatcherProvider.IO).retryWhen { cause, attempt -> val sameValue = new == subscriptions.value @@ -1772,7 +1792,7 @@ public class RtcSession internal constructor( dimensions: VideoDimension = defaultVideoDimension, ) { logger.v { - "[updateTrackDimensions] #track; #sfu; sessionId: $sessionId, trackType: $trackType, visible: $visible, dimensions: $dimensions" + "[updateTrackDimensions] #track; #sfu; #manual-quality-selection; sessionId: $sessionId, trackType: $trackType, visible: $visible, dimensions: $dimensions" } // The map contains all track dimensions for all participants dynascaleLogger.d { "updating dimensions $sessionId $visible $dimensions" } From 54931b654e5a4730171f62406b25c5724f1dcc40 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:35:31 +0200 Subject: [PATCH 02/10] Add Call methods --- .../io/getstream/video/android/core/Call.kt | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index c4ed30aeed..0d3529adfe 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -26,6 +26,7 @@ import io.getstream.result.Result import io.getstream.result.Result.Failure import io.getstream.result.Result.Success import io.getstream.video.android.core.call.RtcSession +import io.getstream.video.android.core.call.TrackDimensions import io.getstream.video.android.core.call.audio.InputAudioFilter import io.getstream.video.android.core.call.utils.SoundInputProcessor import io.getstream.video.android.core.call.video.VideoFilter @@ -1248,23 +1249,53 @@ public class Call( return clientImpl.toggleAudioProcessing() } - fun setPreferredIncomingVideoResolution(resolution: VideoResolution, sessionIds: List? = null) { - val targetSessionIds = sessionIds ?: state.remoteParticipants.value.map { it.sessionId } - val overrides = targetSessionIds.associateWith { resolution } + fun setPreferredIncomingVideoResolution(resolution: VideoResolution?, sessionIds: List? = null) { + session?.let { session -> + val targetSessionIds = sessionIds ?: state.remoteParticipants.value.map { it.sessionId } -// session?.setVideoSubscriptions(manualResolutionOverrides = overrides) - // TODO-mqs: test with several incoming video tracks - targetSessionIds.forEach { sessionId -> - session?.updateTrackDimensions( - sessionId, - TrackType.TRACK_TYPE_VIDEO, - true, - VideoDimension(resolution.width, resolution.height), + updateTrackDimensionsOverrides( + sessionIds = targetSessionIds, + dimensions = resolution?.let { VideoDimension(width = it.width, height = it.height) }, ) + session.setVideoSubscriptions() } } fun setIncomingVideoEnabled(enabled: Boolean) { + // TODO-neg: profile network usage + session?.let { session -> + val targetSessionIds = state.remoteParticipants.value.map { it.sessionId } + + updateTrackDimensionsOverrides(targetSessionIds, visible = enabled) + updateParticipantVideoEnabled(targetSessionIds, enabled) + session.setVideoSubscriptions() + } + } + + // TODO-neg: cand faci on/off, ramane rezolutia setata manual? Cand sterg din overrides? + + internal fun updateTrackDimensionsOverrides( + sessionIds: List, + dimensions: VideoDimension? = null, + visible: Boolean? = null, + ) { + session?.let { session -> + with(session.trackDimensionsOverrides) { + sessionIds.forEach { sessionId -> + val existingOverride = get(sessionId) + val putDimensions = dimensions ?: existingOverride?.dimensions ?: session.defaultVideoDimension + val putVisibility = visible ?: existingOverride?.visible ?: true + + put(sessionId, TrackDimensions(dimensions = putDimensions, visible = putVisibility)) + } + } + } + } + + internal fun updateParticipantVideoEnabled(sessionIds: List, enabled: Boolean) { + sessionIds.forEach { sessionId -> + state.getParticipantBySessionId(sessionId)?.let { it._videoEnabled.value = enabled } + } } @InternalStreamVideoApi From 3085f07ea05a1d357aebfb893a34784d85d6cdc3 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:33:20 +0200 Subject: [PATCH 03/10] Improve override logic --- .../io/getstream/video/android/core/Call.kt | 61 ++----- .../video/android/core/ParticipantState.kt | 9 +- .../video/android/core/call/RtcSession.kt | 124 +++++--------- .../core/call/utils/TrackOverridesHandler.kt | 160 ++++++++++++++++++ 4 files changed, 233 insertions(+), 121 deletions(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 0d3529adfe..dcef6e3b01 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -26,7 +26,6 @@ import io.getstream.result.Result import io.getstream.result.Result.Failure import io.getstream.result.Result.Success import io.getstream.video.android.core.call.RtcSession -import io.getstream.video.android.core.call.TrackDimensions import io.getstream.video.android.core.call.audio.InputAudioFilter import io.getstream.video.android.core.call.utils.SoundInputProcessor import io.getstream.video.android.core.call.video.VideoFilter @@ -1249,53 +1248,29 @@ public class Call( return clientImpl.toggleAudioProcessing() } + /** + * Sets the preferred incoming video resolution. + * + * @param resolution The preferred resolution. Set to `null` to switch back to auto. + * @param sessionIds The participant session IDs to apply the resolution to. If `null`, the resolution will be applied to all participants. + */ fun setPreferredIncomingVideoResolution(resolution: VideoResolution?, sessionIds: List? = null) { - session?.let { session -> - val targetSessionIds = sessionIds ?: state.remoteParticipants.value.map { it.sessionId } - - updateTrackDimensionsOverrides( - sessionIds = targetSessionIds, - dimensions = resolution?.let { VideoDimension(width = it.width, height = it.height) }, - ) - session.setVideoSubscriptions() - } - } - - fun setIncomingVideoEnabled(enabled: Boolean) { - // TODO-neg: profile network usage session?.let { session -> - val targetSessionIds = state.remoteParticipants.value.map { it.sessionId } - - updateTrackDimensionsOverrides(targetSessionIds, visible = enabled) - updateParticipantVideoEnabled(targetSessionIds, enabled) - session.setVideoSubscriptions() - } - } - - // TODO-neg: cand faci on/off, ramane rezolutia setata manual? Cand sterg din overrides? - - internal fun updateTrackDimensionsOverrides( - sessionIds: List, - dimensions: VideoDimension? = null, - visible: Boolean? = null, - ) { - session?.let { session -> - with(session.trackDimensionsOverrides) { - sessionIds.forEach { sessionId -> - val existingOverride = get(sessionId) - val putDimensions = dimensions ?: existingOverride?.dimensions ?: session.defaultVideoDimension - val putVisibility = visible ?: existingOverride?.visible ?: true - - put(sessionId, TrackDimensions(dimensions = putDimensions, visible = putVisibility)) - } - } + session.trackOverridesHandler.updateOverrides( + sessionIds = sessionIds, + dimensions = resolution?.let { VideoDimension(width = it.width, height = it.height) } + ) } } - internal fun updateParticipantVideoEnabled(sessionIds: List, enabled: Boolean) { - sessionIds.forEach { sessionId -> - state.getParticipantBySessionId(sessionId)?.let { it._videoEnabled.value = enabled } - } + /** + * Enables/disables incoming video feed. + * + * @param enabled Whether the video feed should be enabled or disabled. Set to `null` to switch back to auto. + * @param sessionIds The participant session IDs to enable/disable the video feed for. If `null`, the setting will be applied to all participants. + */ + fun setIncomingVideoEnabled(enabled: Boolean?, sessionIds: List? = null) { + session?.trackOverridesHandler?.updateOverrides(sessionIds, visible = enabled) } @InternalStreamVideoApi diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt index 5babb2d88a..6639ba4ae3 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt @@ -221,7 +221,14 @@ public data class ParticipantState( // _dominantSpeaker.value = participant.is_dominant_speaker. we ignore this and only handle the event updateAudioLevel(participant.audio_level) _audioEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_AUDIO) - _videoEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_VIDEO) + + val hasVideoTrack = participant.published_tracks.contains(TrackType.TRACK_TYPE_VIDEO) + _videoEnabled.value = call.session?.trackOverridesHandler?.applyOverrides( + participant.session_id, + hasVideoTrack + ) ?: hasVideoTrack + + _screenSharingEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_SCREEN_SHARE) _roles.value = participant.roles diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 6f14a5c8eb..222a3e2369 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -36,6 +36,7 @@ import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.StreamVideoClient import io.getstream.video.android.core.call.connection.StreamPeerConnection import io.getstream.video.android.core.call.stats.model.RtcStatsReport +import io.getstream.video.android.core.call.utils.TrackOverridesHandler import io.getstream.video.android.core.call.utils.stringify import io.getstream.video.android.core.dispatchers.DispatcherProvider import io.getstream.video.android.core.errors.RtcException @@ -74,7 +75,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -95,13 +95,11 @@ import kotlinx.serialization.json.Json import okio.IOException import org.openapitools.client.models.OwnCapability import org.openapitools.client.models.VideoEvent -import org.openapitools.client.models.VideoResolution import org.webrtc.CameraEnumerationAndroid.CaptureFormat import org.webrtc.MediaConstraints import org.webrtc.MediaStream import org.webrtc.MediaStreamTrack import org.webrtc.PeerConnection -import org.webrtc.PeerConnection.PeerConnectionState import org.webrtc.RTCStatsReport import org.webrtc.RtpParameters.Encoding import org.webrtc.RtpTransceiver @@ -224,16 +222,12 @@ public class RtcSession internal constructor( null, ) - val trackDimensions = - MutableStateFlow>>( - emptyMap(), - ) - val trackDimensionsDebounced = trackDimensions.debounce(100) - // run all calls on a supervisor job so we can easily cancel them private val supervisorJob = SupervisorJob() private val coroutineScope = CoroutineScope(clientImpl.scope.coroutineContext + supervisorJob) + internal val defaultVideoDimension = VideoDimension(1080, 2340) + // participants by session id -> participant state private val trackPrefixToSessionIdMap = call.state.participants.mapState { it.associate { it.trackLookupPrefix to it.sessionId } } @@ -241,6 +235,16 @@ public class RtcSession internal constructor( // We need to update tracks for all participants // It's cleaner to store here and have the participant state reference to it var tracks: MutableMap> = mutableMapOf() + val trackDimensions = MutableStateFlow>>( + emptyMap(), + ) + val trackDimensionsDebounced = trackDimensions.debounce(100) + internal val trackOverridesHandler = TrackOverridesHandler( + getParticipantList = { call.state.remoteParticipants.value }, + getParticipant = { call.state.getParticipantBySessionId(it) }, + onOverridesUpdate = { setVideoSubscriptions() }, + logger = logger, + ) private fun getTrack(sessionId: String, type: TrackType): MediaTrack? { if (!tracks.containsKey(sessionId)) { @@ -982,8 +986,6 @@ public class RtcSession internal constructor( } } - private val defaultVideoDimension = VideoDimension(1080, 2340) - /** * This is called when you are look at a different set of participants * or at a different size @@ -1056,82 +1058,50 @@ public class RtcSession internal constructor( * -- error isn't permanent, SFU didn't change, the mute/publish state didn't change * -- we cap at 30 retries to prevent endless loops */ - internal fun setVideoSubscriptions( - useDefaults: Boolean = false, - manualResolutionOverrides: Map = emptyMap(), - ) { + internal fun setVideoSubscriptions(useDefaults: Boolean = false) { logger.d { "[setVideoSubscriptions] #sfu; #track; useDefaults: $useDefaults" } - // default is to subscribe to the top 5 sorted participants var tracks = if (useDefaults) { + // default is to subscribe to the top 5 sorted participants defaultTracks() } else { // if we're not using the default, sub to visible tracks visibleTracks() - } - // TODO-mqs: test default tracks and scrolling + }.let(trackOverridesHandler::applyOverrides) - // TODO: - // This is a hotfix to help with performance. Most devices struggle even with H resolution - // if there are more than 2 remote participants and especially if there is a H264 participant. - // We just report a very small window so force SFU to deliver us Q resolution. This will - // be later less visible to the user once we make the participant grid smaller -// if (tracks.size > 2) { -// tracks = tracks.map { -// it.copy(dimension = it.dimension?.copy(width = 200, height = 200)) -// } -// } - - logger.v { - "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; manualResolutionOverrides: $manualResolutionOverrides" - } - - // TODO-mqs: store overrides -// tracks = tracks.map { -// manualResolutionOverrides[it.session_id]?.let { resolution -> -// it.copy(dimension = VideoDimension(resolution.width, resolution.height)) -// } ?: it -// } - - val new = tracks.toList() - subscriptions.value = new + subscriptions.value = tracks val currentSfu = sfuUrl subscriptionSyncJob?.cancel() - - if (new.isNotEmpty()) { - // start a new job - // this code is a bit more complicated due to the retry behaviour - subscriptionSyncJob = coroutineScope.launch { - flow { - val request = UpdateSubscriptionsRequest( - session_id = sessionId, - tracks = subscriptions.value, - ) - dynascaleLogger.d { - "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; UpdateSubscriptionsRequest: $request" - } - val sessionToDimension = tracks.map { it.session_id to it.dimension } - dynascaleLogger.v { - "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Subscribing to: $sessionToDimension" - } - val result = updateSubscriptions(request) - dynascaleLogger.v { - "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Result: $result" - } - emit(result.getOrThrow()) - }.flowOn(DispatcherProvider.IO).retryWhen { cause, attempt -> - val sameValue = new == subscriptions.value - val sameSfu = currentSfu == sfuUrl - val isPermanent = isPermanentError(cause) - val willRetry = !isPermanent && sameValue && sameSfu && attempt < 30 - val delayInMs = if (attempt <= 1) 100L else if (attempt <= 3) 300L else 2500L - logger.w { - "updating subscriptions failed with error $cause, retry attempt: $attempt. will retry $willRetry in $delayInMs ms" - } - delay(delayInMs) - willRetry - }.collect() - } + subscriptionSyncJob = coroutineScope.launch { + flow { + val request = UpdateSubscriptionsRequest( + session_id = sessionId, + tracks = subscriptions.value, + ) + dynascaleLogger.d { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; UpdateSubscriptionsRequest: $request" + } + val sessionToDimension = tracks.map { it.session_id to it.dimension } + dynascaleLogger.v { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Subscribing to: $sessionToDimension" + } + val result = updateSubscriptions(request) + dynascaleLogger.v { + "[setVideoSubscriptions] #sfu; #track; #manual-quality-selection; Result: $result" + } + emit(result.getOrThrow()) + }.flowOn(DispatcherProvider.IO).retryWhen { cause, attempt -> + val sameValue = tracks == subscriptions.value + val sameSfu = currentSfu == sfuUrl + val isPermanent = isPermanentError(cause) + val willRetry = !isPermanent && sameValue && sameSfu && attempt < 30 + val delayInMs = if (attempt <= 1) 100L else if (attempt <= 3) 300L else 2500L + logger.w { + "updating subscriptions failed with error $cause, retry attempt: $attempt. will retry $willRetry in $delayInMs ms" + } + delay(delayInMs) + willRetry + }.collect() } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt new file mode 100644 index 0000000000..f88edc35a0 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt @@ -0,0 +1,160 @@ +package io.getstream.video.android.core.call.utils + +import io.getstream.log.TaggedLogger +import io.getstream.video.android.core.ParticipantState +import stream.video.sfu.models.VideoDimension +import stream.video.sfu.signal.TrackSubscriptionDetails + +/** + * Handles incoming video track overrides (resolution and visibility). + * + * @param getParticipantList Lambda used to get the list of call participants. + * @param getParticipant Lambda used to get a call participant by session ID. Used to take advantage of a potential O(1) participant lookup, e.g. in a HashMap. + * @param onOverridesUpdate Lambda used to notify the caller when the overrides are updated. + * @param logger Logger to be used. + */ +internal class TrackOverridesHandler( + private val getParticipantList: () -> List, + private val getParticipant: (sessionId: String) -> ParticipantState?, + private val onOverridesUpdate: (overrides: Map) -> Unit, + private val logger: TaggedLogger? = null, +) { + + private val trackOverrides: MutableMap = mutableMapOf() + + internal data class TrackOverride( + val dimensions: VideoDimension? = null, + val visible: Boolean? = null, + ) + + /** + * Updates incoming video dimensions overrides. + * + * @param sessionIds List of session IDs to update. If `null`, the override will be applied to all participants. + * @param dimensions Video dimensions to set. Set to `null` to switch back to auto. + */ + internal fun updateOverrides( + sessionIds: List? = null, + dimensions: VideoDimension? = null, + ) { + val putDimensions = dimensions + + if (sessionIds == null) { + putOrRemoveDimensionsOverride( + sessionId = ALL_PARTICIPANTS, + putDimensions = putDimensions, + existingVisibility = trackOverrides[ALL_PARTICIPANTS]?.visible + ) + } else { + sessionIds.forEach { sessionId -> + putOrRemoveDimensionsOverride( + sessionId = sessionId, + putDimensions = putDimensions, + existingVisibility = trackOverrides[sessionId]?.visible + ) + } + } + + onOverridesUpdate(trackOverrides) + + logger?.d { "[updateOverrides] #manual-quality-selection; Overrides: $trackOverrides" } + } + + private fun putOrRemoveDimensionsOverride(sessionId: String, putDimensions: VideoDimension?, existingVisibility: Boolean?) { + if (putDimensions == null && (existingVisibility == true || existingVisibility == null)) { + trackOverrides.remove(sessionId) + } else { + trackOverrides.put(sessionId, TrackOverride(dimensions = putDimensions, visible = existingVisibility)) + } + } + + /** + * Updates incoming video visibility overrides. + * + * @param sessionIds List of session IDs to update. If `null`, the override will be applied to all participants. + * @param visible Video visibility to set. Set to `null` to switch back to auto. + */ + internal fun updateOverrides( + sessionIds: List? = null, + visible: Boolean? = null, + ) { + val putVisibility = visible + + if (sessionIds == null) { + putOrRemoveVisibilityOverride( + sessionId = ALL_PARTICIPANTS, + putVisibility = putVisibility, + existingDimensions = trackOverrides[ALL_PARTICIPANTS]?.dimensions + ) + + getParticipantList().forEach { + val override = applyOverrides(it.sessionId, true) + it._videoEnabled.value = override + } + } else { + sessionIds.forEach { sessionId -> + putOrRemoveVisibilityOverride( + sessionId = sessionId, + putVisibility = putVisibility, + existingDimensions = trackOverrides[sessionId]?.dimensions + ) + + getParticipant(sessionId)?.let { + val override = applyOverrides(it.sessionId, true) + it._videoEnabled.value = override + } + } + } + + onOverridesUpdate(trackOverrides) + + logger?.d { "[updateOverrides] #manual-quality-selection; Overrides: $trackOverrides" } + } + + private fun putOrRemoveVisibilityOverride(sessionId: String, putVisibility: Boolean?, existingDimensions: VideoDimension?) { + if (existingDimensions == null && (putVisibility == true || putVisibility == null)) { + trackOverrides.remove(sessionId) + } else { + trackOverrides.put(sessionId, TrackOverride(dimensions = existingDimensions, visible = putVisibility)) + } + } + + /** + * Applies overrides to the given participant's video enabled property. + * + * @return The overridden video enabled property or the original value if no override is found. + */ + internal fun applyOverrides(sessionId: String, videoEnabledFallback: Boolean): Boolean { + val override = trackOverrides[sessionId] ?: trackOverrides[ALL_PARTICIPANTS] + + return if (override?.visible == false) { + false + } else { + videoEnabledFallback + } + } + + + /** + * Applies overrides to given list of tracks. + * + * @return List of tracks with visibility and dimensions overrides applied. + */ + internal fun applyOverrides(tracks: List): List { + return tracks.mapNotNull { track -> + val override = trackOverrides[track.session_id] ?: trackOverrides[ALL_PARTICIPANTS] + + if (override == null) { + track + } else { + if (override.visible == false) { + null + } else { + override.dimensions?.let { track.copy(dimension = it) } ?: track + } + } + } + } +} + +private const val ALL_PARTICIPANTS = "all" \ No newline at end of file From 2f33b77f6f4b68b8f193d3957b0d8051e251086a Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:33:36 +0200 Subject: [PATCH 04/10] Add demo app controls --- .../video/android/ui/call/CallScreen.kt | 17 ++++- .../video/android/ui/menu/MenuDefinitions.kt | 64 +++++++++++++++---- .../video/android/ui/menu/SettingsMenu.kt | 15 ++++- .../video/android/ui/menu/base/DynamicMenu.kt | 10 ++- 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 3cefcd1066..25e2439d2f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -108,7 +108,6 @@ import io.getstream.video.android.ui.menu.availableVideoFilters import io.getstream.video.android.util.config.AppConfig import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.openapitools.client.models.OwnCapability import org.openapitools.client.models.VideoResolution @@ -151,7 +150,9 @@ fun CallScreen( val scope = rememberCoroutineScope() val messageScope = rememberCoroutineScope() var showingLandscapeControls by remember { mutableStateOf(false) } - var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FILL) } + var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FIT) } + var selectedIncomingVideoResolution by remember { mutableStateOf(null) } + var isIncomingVideoEnabled by remember { mutableStateOf(true) } val connection by call.state.connection.collectAsStateWithLifecycle() val me by call.state.me.collectAsState() @@ -505,8 +506,18 @@ fun CallScreen( onNoiseCancellation = { isNoiseCancellationEnabled = call.toggleAudioProcessing() }, - onIncomingResolutionChanged = { + selectedIncomingVideoResolution = selectedIncomingVideoResolution, + onSelectIncomingVideoResolution = { call.setPreferredIncomingVideoResolution(it) + + selectedIncomingVideoResolution = it + isShowingSettingMenu = false + }, + isIncomingVideoEnabled = isIncomingVideoEnabled, + onToggleIncomingVideoVisibility = { + call.setIncomingVideoEnabled(it) + + isIncomingVideoEnabled = it isShowingSettingMenu = false }, onSelectScaleType = { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index 7708b7cb0c..6b3ceceef5 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -41,6 +41,8 @@ import androidx.compose.material.icons.filled.SwitchLeft import androidx.compose.material.icons.filled.VideoFile import androidx.compose.material.icons.filled.VideoLibrary import androidx.compose.material.icons.filled.VideoSettings +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.icons.filled.VideocamOff import io.getstream.video.android.compose.ui.components.video.VideoScalingType import io.getstream.video.android.core.audio.StreamAudioDevice import io.getstream.video.android.ui.menu.base.ActionMenuItem @@ -67,7 +69,10 @@ fun defaultStreamMenu( onSwitchSfuClick: () -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, - onIncomingResolutionChanged: (VideoResolution) -> Unit, + selectedIncomingVideoResolution: VideoResolution?, + onSelectIncomingVideoResolution: (VideoResolution?) -> Unit, + isIncomingVideoEnabled: Boolean, + onToggleIncomingVideoEnabled: (Boolean) -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, onSfuRejoinClick: () -> Unit, onSfuFastReconnectClick: () -> Unit, @@ -133,17 +138,52 @@ fun defaultStreamMenu( ) } add( - ActionMenuItem( - title = "Change incoming resolution (144)", - icon = Icons.Default.AspectRatio, - action = { onIncomingResolutionChanged(VideoResolution(height = 144, width = 144)) }, - ) - ) - add( - ActionMenuItem( - title = "Change incoming resolution (2160p)", - icon = Icons.Default.AspectRatio, - action = { onIncomingResolutionChanged(VideoResolution(2160, 3840)) }, + SubMenuItem( + title = "Incoming video settings", + icon = Icons.Default.VideoSettings, + items = listOf( + ActionMenuItem( + title = "Auto Quality", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == null, + action = { onSelectIncomingVideoResolution(null) }, + ), + ActionMenuItem( + title = "4K 2160p", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == VideoResolution(2160, 3840), + action = { onSelectIncomingVideoResolution(VideoResolution(2160, 3840)) }, + ), + ActionMenuItem( + title = "Full HD 1080p", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == VideoResolution(1080, 1920), + action = { onSelectIncomingVideoResolution(VideoResolution(1080, 1920)) }, + ), + ActionMenuItem( + title = "HD 720p", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == VideoResolution(720, 1280), + action = { onSelectIncomingVideoResolution(VideoResolution(720, 1280)) }, + ), + ActionMenuItem( + title = "SD 480p", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == VideoResolution(480, 640), + action = { onSelectIncomingVideoResolution(VideoResolution(480, 640)) }, + ), + ActionMenuItem( + title = "Data Saver 144p", + icon = Icons.Default.AspectRatio, + highlight = selectedIncomingVideoResolution == VideoResolution(144, 256), + action = { onSelectIncomingVideoResolution(VideoResolution(144, 256)) }, + ), + ActionMenuItem( + title = if (isIncomingVideoEnabled) "Disable incoming video" else "Enable incoming video", + icon = if (isIncomingVideoEnabled) Icons.Default.VideocamOff else Icons.Default.Videocam, + action = { onToggleIncomingVideoEnabled(!isIncomingVideoEnabled) }, + ), + ) ) ) if (showDebugOptions) { diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 9ea8d2b6f0..ea6a4ac1ee 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -76,7 +76,10 @@ internal fun SettingsMenu( onSelectVideoFilter: (Int) -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, - onIncomingResolutionChanged: (VideoResolution) -> Unit, + selectedIncomingVideoResolution: VideoResolution?, + onSelectIncomingVideoResolution: (VideoResolution?) -> Unit, + isIncomingVideoEnabled: Boolean, + onToggleIncomingVideoVisibility: (Boolean) -> Unit, onShowCallStats: () -> Unit, onSelectScaleType: (VideoScalingType) -> Unit, ) { @@ -238,7 +241,10 @@ internal fun SettingsMenu( onSwitchSfuClick = onSwitchSfuClick, onShowCallStats = onShowCallStats, onNoiseCancellation = onNoiseCancellation, - onIncomingResolutionChanged = { onIncomingResolutionChanged(it) }, + selectedIncomingVideoResolution = selectedIncomingVideoResolution, + onSelectIncomingVideoResolution = { onSelectIncomingVideoResolution(it) }, + isIncomingVideoEnabled = isIncomingVideoEnabled, + onToggleIncomingVideoEnabled = { onToggleIncomingVideoVisibility(it) }, onSfuRejoinClick = onSfuRejoinClick, onSfuFastReconnectClick = onSfuFastReconnectClick, isScreenShareEnabled = isScreenSharing, @@ -306,7 +312,10 @@ private fun SettingsMenuPreview() { onShowFeedback = {}, onSelectScaleType = {}, onNoiseCancellation = {}, - onIncomingResolutionChanged = {}, + selectedIncomingVideoResolution = null, + onSelectIncomingVideoResolution = {}, + isIncomingVideoEnabled = true, + onToggleIncomingVideoEnabled = {}, loadRecordings = { emptyList() }, ), ) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt index b1da1f8d31..5037a728de 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/base/DynamicMenu.kt @@ -225,7 +225,10 @@ private fun DynamicMenuPreview() { onDeviceSelected = {}, onShowFeedback = {}, onNoiseCancellation = {}, - onIncomingResolutionChanged = {}, + selectedIncomingVideoResolution = null, + onSelectIncomingVideoResolution = {}, + isIncomingVideoEnabled = true, + onToggleIncomingVideoEnabled = {}, onSelectScaleType = {}, loadRecordings = { emptyList() }, ), @@ -256,7 +259,10 @@ private fun DynamicMenuDebugOptionPreview() { onShowFeedback = {}, onSelectScaleType = { }, onNoiseCancellation = {}, - onIncomingResolutionChanged = {}, + selectedIncomingVideoResolution = null, + onSelectIncomingVideoResolution = {}, + isIncomingVideoEnabled = true, + onToggleIncomingVideoEnabled = {}, loadRecordings = { emptyList() }, ), ) From e05391e850afcc6981a9e2c85ab37d9ec73d651d Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:37:55 +0200 Subject: [PATCH 05/10] Apply spotless and apiDump --- .../getstream/video/android/CallActivity.kt | 4 -- .../video/android/ui/menu/MenuDefinitions.kt | 4 +- .../api/stream-video-android-core.api | 3 +- .../io/getstream/video/android/core/Call.kt | 7 +++- .../video/android/core/ParticipantState.kt | 3 +- .../core/call/utils/TrackOverridesHandler.kt | 41 +++++++++++++++---- 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt index 46f09dee95..2155e82c31 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt @@ -19,14 +19,10 @@ package io.getstream.video.android import android.content.Intent import android.os.Bundle import android.os.PersistableBundle -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.querysort.QuerySortByField diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index 6b3ceceef5..5af6f801ff 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -183,8 +183,8 @@ fun defaultStreamMenu( icon = if (isIncomingVideoEnabled) Icons.Default.VideocamOff else Icons.Default.Videocam, action = { onToggleIncomingVideoEnabled(!isIncomingVideoEnabled) }, ), - ) - ) + ), + ), ) if (showDebugOptions) { add( 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 647b82958f..c0633776a9 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -65,7 +65,8 @@ public final class io/getstream/video/android/core/Call { public static synthetic fun sendReaction$default (Lio/getstream/video/android/core/Call;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun setAudioFilter (Lio/getstream/video/android/core/call/audio/InputAudioFilter;)V public final fun setAudioProcessingEnabled (Z)V - public final fun setIncomingVideoEnabled (Z)V + public final fun setIncomingVideoEnabled (Ljava/lang/Boolean;Ljava/util/List;)V + public static synthetic fun setIncomingVideoEnabled$default (Lio/getstream/video/android/core/Call;Ljava/lang/Boolean;Ljava/util/List;ILjava/lang/Object;)V public final fun setPreferredIncomingVideoResolution (Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;)V public static synthetic fun setPreferredIncomingVideoResolution$default (Lio/getstream/video/android/core/Call;Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;ILjava/lang/Object;)V public final fun setSessionId (Ljava/lang/String;)V diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index dcef6e3b01..1e0705a1d0 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -1254,11 +1254,14 @@ public class Call( * @param resolution The preferred resolution. Set to `null` to switch back to auto. * @param sessionIds The participant session IDs to apply the resolution to. If `null`, the resolution will be applied to all participants. */ - fun setPreferredIncomingVideoResolution(resolution: VideoResolution?, sessionIds: List? = null) { + fun setPreferredIncomingVideoResolution( + resolution: VideoResolution?, + sessionIds: List? = null, + ) { session?.let { session -> session.trackOverridesHandler.updateOverrides( sessionIds = sessionIds, - dimensions = resolution?.let { VideoDimension(width = it.width, height = it.height) } + dimensions = resolution?.let { VideoDimension(it.width, it.height) }, ) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt index 6639ba4ae3..052aa07217 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt @@ -225,10 +225,9 @@ public data class ParticipantState( val hasVideoTrack = participant.published_tracks.contains(TrackType.TRACK_TYPE_VIDEO) _videoEnabled.value = call.session?.trackOverridesHandler?.applyOverrides( participant.session_id, - hasVideoTrack + hasVideoTrack, ) ?: hasVideoTrack - _screenSharingEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_SCREEN_SHARE) _roles.value = participant.roles diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt index f88edc35a0..def678a55e 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt @@ -1,3 +1,19 @@ +/* + * 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. + */ + package io.getstream.video.android.core.call.utils import io.getstream.log.TaggedLogger @@ -43,14 +59,14 @@ internal class TrackOverridesHandler( putOrRemoveDimensionsOverride( sessionId = ALL_PARTICIPANTS, putDimensions = putDimensions, - existingVisibility = trackOverrides[ALL_PARTICIPANTS]?.visible + existingVisibility = trackOverrides[ALL_PARTICIPANTS]?.visible, ) } else { sessionIds.forEach { sessionId -> putOrRemoveDimensionsOverride( sessionId = sessionId, putDimensions = putDimensions, - existingVisibility = trackOverrides[sessionId]?.visible + existingVisibility = trackOverrides[sessionId]?.visible, ) } } @@ -64,7 +80,10 @@ internal class TrackOverridesHandler( if (putDimensions == null && (existingVisibility == true || existingVisibility == null)) { trackOverrides.remove(sessionId) } else { - trackOverrides.put(sessionId, TrackOverride(dimensions = putDimensions, visible = existingVisibility)) + trackOverrides.put( + sessionId, + TrackOverride(dimensions = putDimensions, visible = existingVisibility), + ) } } @@ -84,7 +103,7 @@ internal class TrackOverridesHandler( putOrRemoveVisibilityOverride( sessionId = ALL_PARTICIPANTS, putVisibility = putVisibility, - existingDimensions = trackOverrides[ALL_PARTICIPANTS]?.dimensions + existingDimensions = trackOverrides[ALL_PARTICIPANTS]?.dimensions, ) getParticipantList().forEach { @@ -96,7 +115,7 @@ internal class TrackOverridesHandler( putOrRemoveVisibilityOverride( sessionId = sessionId, putVisibility = putVisibility, - existingDimensions = trackOverrides[sessionId]?.dimensions + existingDimensions = trackOverrides[sessionId]?.dimensions, ) getParticipant(sessionId)?.let { @@ -115,7 +134,10 @@ internal class TrackOverridesHandler( if (existingDimensions == null && (putVisibility == true || putVisibility == null)) { trackOverrides.remove(sessionId) } else { - trackOverrides.put(sessionId, TrackOverride(dimensions = existingDimensions, visible = putVisibility)) + trackOverrides.put( + sessionId, + TrackOverride(dimensions = existingDimensions, visible = putVisibility), + ) } } @@ -134,13 +156,14 @@ internal class TrackOverridesHandler( } } - /** * Applies overrides to given list of tracks. * * @return List of tracks with visibility and dimensions overrides applied. */ - internal fun applyOverrides(tracks: List): List { + internal fun applyOverrides( + tracks: List, + ): List { return tracks.mapNotNull { track -> val override = trackOverrides[track.session_id] ?: trackOverrides[ALL_PARTICIPANTS] @@ -157,4 +180,4 @@ internal class TrackOverridesHandler( } } -private const val ALL_PARTICIPANTS = "all" \ No newline at end of file +private const val ALL_PARTICIPANTS = "all" From fe3fedffd15eba412620b6c48937d8909b98e5ba Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:33:46 +0200 Subject: [PATCH 06/10] Revert aspect to FILL --- .../kotlin/io/getstream/video/android/ui/call/CallScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 25e2439d2f..75d9412ce8 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -150,7 +150,7 @@ fun CallScreen( val scope = rememberCoroutineScope() val messageScope = rememberCoroutineScope() var showingLandscapeControls by remember { mutableStateOf(false) } - var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FIT) } + var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FILL) } var selectedIncomingVideoResolution by remember { mutableStateOf(null) } var isIncomingVideoEnabled by remember { mutableStateOf(true) } From eeffd068ed361c883e823713fc0b01f634c0729d Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:11:50 +0200 Subject: [PATCH 07/10] Watch for video enabled overrides in VideoRenderer --- .../api/stream-video-android-core.api | 5 + .../getstream/video/android/core/CallState.kt | 8 +- .../video/android/core/ParticipantState.kt | 8 +- .../video/android/core/call/RtcSession.kt | 7 +- .../core/call/utils/TrackOverridesHandler.kt | 40 +------ .../ui/components/video/VideoRenderer.kt | 102 ++++++++++-------- 6 files changed, 77 insertions(+), 93 deletions(-) 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 c0633776a9..bc07fe68c3 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -148,6 +148,7 @@ public final class io/getstream/video/android/core/CallState { public final fun getOwnCapabilities ()Lkotlinx/coroutines/flow/StateFlow; public final fun getParticipantBySessionId (Ljava/lang/String;)Lio/getstream/video/android/core/ParticipantState; public final fun getParticipantCounts ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getParticipantVideoEnabledOverrides ()Lkotlinx/coroutines/flow/StateFlow; public final fun getParticipants ()Lkotlinx/coroutines/flow/StateFlow; public final fun getPermissionRequests ()Lkotlinx/coroutines/flow/StateFlow; public final fun getPinnedParticipants ()Lkotlinx/coroutines/flow/StateFlow; @@ -2856,6 +2857,10 @@ public final class io/getstream/video/android/core/call/stats/model/discriminato public final fun fromAlias (Ljava/lang/String;)Lio/getstream/video/android/core/call/stats/model/discriminator/RtcReportType; } +public final class io/getstream/video/android/core/call/utils/TrackOverridesHandlerKt { + public static final field ALL_PARTICIPANTS Ljava/lang/String; +} + public abstract class io/getstream/video/android/core/call/video/BitmapVideoFilter : io/getstream/video/android/core/call/video/VideoFilter { public fun ()V public abstract fun applyFilter (Landroid/graphics/Bitmap;)V 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 1adac85b87..583b888db2 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 @@ -59,6 +59,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -556,10 +557,13 @@ public class CallState( internal val _reactions = MutableStateFlow>(emptyList()) val reactions: StateFlow> = _reactions - private val _errors: MutableStateFlow> = - MutableStateFlow(emptyList()) + private val _errors: MutableStateFlow> = MutableStateFlow(emptyList()) public val errors: StateFlow> = _errors + internal val _participantVideoEnabledOverrides = + MutableStateFlow>(emptyMap()) + val participantVideoEnabledOverrides = _participantVideoEnabledOverrides.asStateFlow() + private var speakingWhileMutedResetJob: Job? = null private var autoJoiningCall: Job? = null private var ringingTimerJob: Job? = null diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt index 052aa07217..5babb2d88a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ParticipantState.kt @@ -221,13 +221,7 @@ public data class ParticipantState( // _dominantSpeaker.value = participant.is_dominant_speaker. we ignore this and only handle the event updateAudioLevel(participant.audio_level) _audioEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_AUDIO) - - val hasVideoTrack = participant.published_tracks.contains(TrackType.TRACK_TYPE_VIDEO) - _videoEnabled.value = call.session?.trackOverridesHandler?.applyOverrides( - participant.session_id, - hasVideoTrack, - ) ?: hasVideoTrack - + _videoEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_VIDEO) _screenSharingEnabled.value = participant.published_tracks.contains(TrackType.TRACK_TYPE_SCREEN_SHARE) _roles.value = participant.roles diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 222a3e2369..9ce60111a7 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -240,9 +240,10 @@ public class RtcSession internal constructor( ) val trackDimensionsDebounced = trackDimensions.debounce(100) internal val trackOverridesHandler = TrackOverridesHandler( - getParticipantList = { call.state.remoteParticipants.value }, - getParticipant = { call.state.getParticipantBySessionId(it) }, - onOverridesUpdate = { setVideoSubscriptions() }, + onOverridesUpdate = { + setVideoSubscriptions() + call.state._participantVideoEnabledOverrides.value = it.mapValues { it.value.visible } + }, logger = logger, ) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt index def678a55e..9bdd20ffd0 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/utils/TrackOverridesHandler.kt @@ -17,28 +17,23 @@ package io.getstream.video.android.core.call.utils import io.getstream.log.TaggedLogger -import io.getstream.video.android.core.ParticipantState import stream.video.sfu.models.VideoDimension import stream.video.sfu.signal.TrackSubscriptionDetails /** * Handles incoming video track overrides (resolution and visibility). * - * @param getParticipantList Lambda used to get the list of call participants. - * @param getParticipant Lambda used to get a call participant by session ID. Used to take advantage of a potential O(1) participant lookup, e.g. in a HashMap. * @param onOverridesUpdate Lambda used to notify the caller when the overrides are updated. * @param logger Logger to be used. */ internal class TrackOverridesHandler( - private val getParticipantList: () -> List, - private val getParticipant: (sessionId: String) -> ParticipantState?, private val onOverridesUpdate: (overrides: Map) -> Unit, private val logger: TaggedLogger? = null, ) { private val trackOverrides: MutableMap = mutableMapOf() - internal data class TrackOverride( + data class TrackOverride( val dimensions: VideoDimension? = null, val visible: Boolean? = null, ) @@ -49,7 +44,7 @@ internal class TrackOverridesHandler( * @param sessionIds List of session IDs to update. If `null`, the override will be applied to all participants. * @param dimensions Video dimensions to set. Set to `null` to switch back to auto. */ - internal fun updateOverrides( + fun updateOverrides( sessionIds: List? = null, dimensions: VideoDimension? = null, ) { @@ -93,7 +88,7 @@ internal class TrackOverridesHandler( * @param sessionIds List of session IDs to update. If `null`, the override will be applied to all participants. * @param visible Video visibility to set. Set to `null` to switch back to auto. */ - internal fun updateOverrides( + fun updateOverrides( sessionIds: List? = null, visible: Boolean? = null, ) { @@ -105,11 +100,6 @@ internal class TrackOverridesHandler( putVisibility = putVisibility, existingDimensions = trackOverrides[ALL_PARTICIPANTS]?.dimensions, ) - - getParticipantList().forEach { - val override = applyOverrides(it.sessionId, true) - it._videoEnabled.value = override - } } else { sessionIds.forEach { sessionId -> putOrRemoveVisibilityOverride( @@ -117,11 +107,6 @@ internal class TrackOverridesHandler( putVisibility = putVisibility, existingDimensions = trackOverrides[sessionId]?.dimensions, ) - - getParticipant(sessionId)?.let { - val override = applyOverrides(it.sessionId, true) - it._videoEnabled.value = override - } } } @@ -141,27 +126,12 @@ internal class TrackOverridesHandler( } } - /** - * Applies overrides to the given participant's video enabled property. - * - * @return The overridden video enabled property or the original value if no override is found. - */ - internal fun applyOverrides(sessionId: String, videoEnabledFallback: Boolean): Boolean { - val override = trackOverrides[sessionId] ?: trackOverrides[ALL_PARTICIPANTS] - - return if (override?.visible == false) { - false - } else { - videoEnabledFallback - } - } - /** * Applies overrides to given list of tracks. * * @return List of tracks with visibility and dimensions overrides applied. */ - internal fun applyOverrides( + fun applyOverrides( tracks: List, ): List { return tracks.mapNotNull { track -> @@ -180,4 +150,4 @@ internal class TrackOverridesHandler( } } -private const val ALL_PARTICIPANTS = "all" +const val ALL_PARTICIPANTS = "all" diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt index 62fa843d8b..68057f7b5c 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.log.StreamLog import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.video.VideoScalingType.Companion.toCommonScalingType @@ -52,6 +53,7 @@ import io.getstream.video.android.compose.ui.components.video.config.VideoRender import io.getstream.video.android.compose.ui.components.video.config.videoRenderConfig import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState +import io.getstream.video.android.core.call.utils.ALL_PARTICIPANTS import io.getstream.video.android.core.model.MediaTrack import io.getstream.video.android.core.model.VideoTrack import io.getstream.video.android.mock.StreamPreviewDataUtils @@ -90,60 +92,64 @@ public fun VideoRenderer( videoRendererConfig.fallbackContent.invoke(call) if (video?.enabled == true) { - val mediaTrack = video.track val sessionId = video.sessionId - val trackType = video.type + val videoEnabledOverrides by call.state.participantVideoEnabledOverrides.collectAsStateWithLifecycle() - var view: VideoTextureViewRenderer? by remember { mutableStateOf(null) } + if (isIncomingVideoEnabled(call, sessionId, videoEnabledOverrides)) { + val mediaTrack = video.track + val trackType = video.type - DisposableEffect(call, video) { - // inform the call that we want to render this video track. (this will trigger a subscription to the track) - call.setVisibility(sessionId, trackType, true) + var view: VideoTextureViewRenderer? by remember { mutableStateOf(null) } - onDispose { - cleanTrack(view, mediaTrack) - // inform the call that we no longer want to render this video track - call.setVisibility(sessionId, trackType, false) + DisposableEffect(call, video) { + // inform the call that we want to render this video track. (this will trigger a subscription to the track) + call.setVisibility(sessionId, trackType, true) + + onDispose { + cleanTrack(view, mediaTrack) + // inform the call that we no longer want to render this video track + call.setVisibility(sessionId, trackType, false) + } } - } - if (mediaTrack != null) { - Box( - modifier = videoRendererConfig.modifiers.containerModifier.invoke(this), - contentAlignment = Alignment.Center, - ) { - AndroidView( - factory = { context -> - StreamVideoTextureViewRenderer(context).apply { - call.initRenderer( - videoRenderer = this, - sessionId = sessionId, - trackType = trackType, - onRendered = onRendered, - ) - setMirror(videoRendererConfig.mirrorStream) - setScalingType( + if (mediaTrack != null) { + Box( + modifier = videoRendererConfig.modifiers.containerModifier.invoke(this), + contentAlignment = Alignment.Center, + ) { + AndroidView( + factory = { context -> + StreamVideoTextureViewRenderer(context).apply { + call.initRenderer( + videoRenderer = this, + sessionId = sessionId, + trackType = trackType, + onRendered = onRendered, + ) + setMirror(videoRendererConfig.mirrorStream) + setScalingType( + videoRendererConfig.scalingType.toCommonScalingType(), + ) + setupVideo(mediaTrack, this) + + view = this + } + }, + update = { v -> + v.setMirror(videoRendererConfig.mirrorStream) + v.setScalingType( videoRendererConfig.scalingType.toCommonScalingType(), ) - setupVideo(mediaTrack, this) - - view = this - } - }, - update = { v -> - v.setMirror(videoRendererConfig.mirrorStream) - v.setScalingType( - videoRendererConfig.scalingType.toCommonScalingType(), - ) - setupVideo(mediaTrack, v) - }, - modifier = videoRendererConfig - .modifiers - .componentModifier( - this, - ) - .testTag("video_renderer"), - ) + setupVideo(mediaTrack, v) + }, + modifier = videoRendererConfig + .modifiers + .componentModifier( + this, + ) + .testTag("video_renderer"), + ) + } } } } @@ -187,6 +193,10 @@ public fun VideoRenderer( ) } +private fun isIncomingVideoEnabled(call: Call, sessionId: String, videoEnabledOverrides: Map) = + (videoEnabledOverrides[sessionId] ?: videoEnabledOverrides[ALL_PARTICIPANTS]) != false + || call.state.me.value?.sessionId == sessionId + private fun cleanTrack( view: VideoTextureViewRenderer?, mediaTrack: MediaTrack?, From a3f82228a5dc5e4a2f2d577edba0e4e8a944475e Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:13:47 +0200 Subject: [PATCH 08/10] Enable video in demo app when changing incoming resolution --- .../io/getstream/video/android/ui/call/CallScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 75d9412ce8..e0b2ed5e95 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -508,16 +508,19 @@ fun CallScreen( }, selectedIncomingVideoResolution = selectedIncomingVideoResolution, onSelectIncomingVideoResolution = { - call.setPreferredIncomingVideoResolution(it) + call.setIncomingVideoEnabled(true) + isIncomingVideoEnabled = true + call.setPreferredIncomingVideoResolution(it) selectedIncomingVideoResolution = it + isShowingSettingMenu = false }, isIncomingVideoEnabled = isIncomingVideoEnabled, onToggleIncomingVideoVisibility = { call.setIncomingVideoEnabled(it) - isIncomingVideoEnabled = it + isShowingSettingMenu = false }, onSelectScaleType = { From 347efb5af7321939828f0700912f505c69ab9d1b Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:32:31 +0200 Subject: [PATCH 09/10] Replace resolution class --- .../video/android/ui/call/CallScreen.kt | 3 ++- .../video/android/ui/menu/MenuDefinitions.kt | 25 ++++++++++--------- .../video/android/ui/menu/SettingsMenu.kt | 5 ++-- .../io/getstream/video/android/core/Call.kt | 3 ++- .../core/model/PreferredVideoResolution.kt | 6 +++++ 5 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index e0b2ed5e95..8bbc253289 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -95,6 +95,7 @@ import io.getstream.video.android.compose.ui.components.video.VideoScalingType import io.getstream.video.android.core.Call import io.getstream.video.android.core.RealtimeConnection import io.getstream.video.android.core.call.state.ChooseLayout +import io.getstream.video.android.core.model.PreferredVideoResolution import io.getstream.video.android.core.utils.isEnabled import io.getstream.video.android.filters.video.BlurredBackgroundVideoFilter import io.getstream.video.android.filters.video.VirtualBackgroundVideoFilter @@ -151,7 +152,7 @@ fun CallScreen( val messageScope = rememberCoroutineScope() var showingLandscapeControls by remember { mutableStateOf(false) } var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FILL) } - var selectedIncomingVideoResolution by remember { mutableStateOf(null) } + var selectedIncomingVideoResolution by remember { mutableStateOf(null) } var isIncomingVideoEnabled by remember { mutableStateOf(true) } val connection by call.state.connection.collectAsStateWithLifecycle() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index 5af6f801ff..b924aeb54a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -45,6 +45,7 @@ import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.icons.filled.VideocamOff import io.getstream.video.android.compose.ui.components.video.VideoScalingType import io.getstream.video.android.core.audio.StreamAudioDevice +import io.getstream.video.android.core.model.PreferredVideoResolution import io.getstream.video.android.ui.menu.base.ActionMenuItem import io.getstream.video.android.ui.menu.base.DynamicSubMenuItem import io.getstream.video.android.ui.menu.base.MenuItem @@ -69,8 +70,8 @@ fun defaultStreamMenu( onSwitchSfuClick: () -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, - selectedIncomingVideoResolution: VideoResolution?, - onSelectIncomingVideoResolution: (VideoResolution?) -> Unit, + selectedIncomingVideoResolution: PreferredVideoResolution?, + onSelectIncomingVideoResolution: (PreferredVideoResolution?) -> Unit, isIncomingVideoEnabled: Boolean, onToggleIncomingVideoEnabled: (Boolean) -> Unit, onDeviceSelected: (StreamAudioDevice) -> Unit, @@ -151,32 +152,32 @@ fun defaultStreamMenu( ActionMenuItem( title = "4K 2160p", icon = Icons.Default.AspectRatio, - highlight = selectedIncomingVideoResolution == VideoResolution(2160, 3840), - action = { onSelectIncomingVideoResolution(VideoResolution(2160, 3840)) }, + highlight = selectedIncomingVideoResolution == PreferredVideoResolution(3840, 2160), + action = { onSelectIncomingVideoResolution(PreferredVideoResolution(3840, 2160)) }, ), ActionMenuItem( title = "Full HD 1080p", icon = Icons.Default.AspectRatio, - highlight = selectedIncomingVideoResolution == VideoResolution(1080, 1920), - action = { onSelectIncomingVideoResolution(VideoResolution(1080, 1920)) }, + highlight = selectedIncomingVideoResolution == PreferredVideoResolution(1920, 1080), + action = { onSelectIncomingVideoResolution(PreferredVideoResolution(1920, 1080)) }, ), ActionMenuItem( title = "HD 720p", icon = Icons.Default.AspectRatio, - highlight = selectedIncomingVideoResolution == VideoResolution(720, 1280), - action = { onSelectIncomingVideoResolution(VideoResolution(720, 1280)) }, + highlight = selectedIncomingVideoResolution == PreferredVideoResolution(1280, 720), + action = { onSelectIncomingVideoResolution(PreferredVideoResolution(1280, 720)) }, ), ActionMenuItem( title = "SD 480p", icon = Icons.Default.AspectRatio, - highlight = selectedIncomingVideoResolution == VideoResolution(480, 640), - action = { onSelectIncomingVideoResolution(VideoResolution(480, 640)) }, + highlight = selectedIncomingVideoResolution == PreferredVideoResolution(640, 480), + action = { onSelectIncomingVideoResolution(PreferredVideoResolution(640, 480)) }, ), ActionMenuItem( title = "Data Saver 144p", icon = Icons.Default.AspectRatio, - highlight = selectedIncomingVideoResolution == VideoResolution(144, 256), - action = { onSelectIncomingVideoResolution(VideoResolution(144, 256)) }, + highlight = selectedIncomingVideoResolution == PreferredVideoResolution(256, 144), + action = { onSelectIncomingVideoResolution(PreferredVideoResolution(256, 144)) }, ), ActionMenuItem( title = if (isIncomingVideoEnabled) "Disable incoming video" else "Enable incoming video", diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index ea6a4ac1ee..4de392821a 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -54,6 +54,7 @@ import io.getstream.video.android.compose.ui.components.video.VideoScalingType import io.getstream.video.android.core.Call import io.getstream.video.android.core.call.audio.InputAudioFilter import io.getstream.video.android.core.mapper.ReactionMapper +import io.getstream.video.android.core.model.PreferredVideoResolution import io.getstream.video.android.tooling.extensions.toPx import io.getstream.video.android.ui.call.ReactionsMenu import io.getstream.video.android.ui.menu.base.ActionMenuItem @@ -76,8 +77,8 @@ internal fun SettingsMenu( onSelectVideoFilter: (Int) -> Unit, onShowFeedback: () -> Unit, onNoiseCancellation: () -> Unit, - selectedIncomingVideoResolution: VideoResolution?, - onSelectIncomingVideoResolution: (VideoResolution?) -> Unit, + selectedIncomingVideoResolution: PreferredVideoResolution?, + onSelectIncomingVideoResolution: (PreferredVideoResolution?) -> Unit, isIncomingVideoEnabled: Boolean, onToggleIncomingVideoVisibility: (Boolean) -> Unit, onShowCallStats: () -> Unit, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 1e0705a1d0..a98693e2b7 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -36,6 +36,7 @@ import io.getstream.video.android.core.events.VideoEventListener import io.getstream.video.android.core.internal.InternalStreamVideoApi import io.getstream.video.android.core.internal.network.NetworkStateProvider import io.getstream.video.android.core.model.MuteUsersData +import io.getstream.video.android.core.model.PreferredVideoResolution import io.getstream.video.android.core.model.QueriedMembers import io.getstream.video.android.core.model.RejectReason import io.getstream.video.android.core.model.SortField @@ -1255,7 +1256,7 @@ public class Call( * @param sessionIds The participant session IDs to apply the resolution to. If `null`, the resolution will be applied to all participants. */ fun setPreferredIncomingVideoResolution( - resolution: VideoResolution?, + resolution: PreferredVideoResolution?, sessionIds: List? = null, ) { session?.let { session -> diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt new file mode 100644 index 0000000000..894bda9f7a --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt @@ -0,0 +1,6 @@ +package io.getstream.video.android.core.model + +data class PreferredVideoResolution( + val width: Int, + val height: Int +) \ No newline at end of file From ee5fdade12c29806b4453eff588381db07730cd3 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:34:26 +0200 Subject: [PATCH 10/10] Apply spotless --- .../video/android/ui/call/CallScreen.kt | 5 +++-- .../video/android/ui/menu/MenuDefinitions.kt | 21 +++++++++++++------ .../video/android/ui/menu/SettingsMenu.kt | 1 - .../api/stream-video-android-core.api | 17 +++++++++++++-- .../io/getstream/video/android/core/Call.kt | 1 - .../core/model/PreferredVideoResolution.kt | 20 ++++++++++++++++-- .../ui/components/video/VideoRenderer.kt | 4 ++-- 7 files changed, 53 insertions(+), 16 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 8bbc253289..f4edf4e612 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -111,7 +111,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.openapitools.client.models.OwnCapability -import org.openapitools.client.models.VideoResolution @OptIn(ExperimentalMaterialApi::class) @Composable @@ -152,7 +151,9 @@ fun CallScreen( val messageScope = rememberCoroutineScope() var showingLandscapeControls by remember { mutableStateOf(false) } var preferredScaleType by remember { mutableStateOf(VideoScalingType.SCALE_ASPECT_FILL) } - var selectedIncomingVideoResolution by remember { mutableStateOf(null) } + var selectedIncomingVideoResolution by remember { + mutableStateOf(null) + } var isIncomingVideoEnabled by remember { mutableStateOf(true) } val connection by call.state.connection.collectAsStateWithLifecycle() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt index b924aeb54a..9aa22b0473 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/MenuDefinitions.kt @@ -50,7 +50,6 @@ import io.getstream.video.android.ui.menu.base.ActionMenuItem import io.getstream.video.android.ui.menu.base.DynamicSubMenuItem import io.getstream.video.android.ui.menu.base.MenuItem import io.getstream.video.android.ui.menu.base.SubMenuItem -import org.openapitools.client.models.VideoResolution /** * Defines the default Stream menu for the demo app. @@ -153,31 +152,41 @@ fun defaultStreamMenu( title = "4K 2160p", icon = Icons.Default.AspectRatio, highlight = selectedIncomingVideoResolution == PreferredVideoResolution(3840, 2160), - action = { onSelectIncomingVideoResolution(PreferredVideoResolution(3840, 2160)) }, + action = { + onSelectIncomingVideoResolution(PreferredVideoResolution(3840, 2160)) + }, ), ActionMenuItem( title = "Full HD 1080p", icon = Icons.Default.AspectRatio, highlight = selectedIncomingVideoResolution == PreferredVideoResolution(1920, 1080), - action = { onSelectIncomingVideoResolution(PreferredVideoResolution(1920, 1080)) }, + action = { + onSelectIncomingVideoResolution(PreferredVideoResolution(1920, 1080)) + }, ), ActionMenuItem( title = "HD 720p", icon = Icons.Default.AspectRatio, highlight = selectedIncomingVideoResolution == PreferredVideoResolution(1280, 720), - action = { onSelectIncomingVideoResolution(PreferredVideoResolution(1280, 720)) }, + action = { + onSelectIncomingVideoResolution(PreferredVideoResolution(1280, 720)) + }, ), ActionMenuItem( title = "SD 480p", icon = Icons.Default.AspectRatio, highlight = selectedIncomingVideoResolution == PreferredVideoResolution(640, 480), - action = { onSelectIncomingVideoResolution(PreferredVideoResolution(640, 480)) }, + action = { + onSelectIncomingVideoResolution(PreferredVideoResolution(640, 480)) + }, ), ActionMenuItem( title = "Data Saver 144p", icon = Icons.Default.AspectRatio, highlight = selectedIncomingVideoResolution == PreferredVideoResolution(256, 144), - action = { onSelectIncomingVideoResolution(PreferredVideoResolution(256, 144)) }, + action = { + onSelectIncomingVideoResolution(PreferredVideoResolution(256, 144)) + }, ), ActionMenuItem( title = if (isIncomingVideoEnabled) "Disable incoming video" else "Enable incoming video", diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index 4de392821a..53d4582c37 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -62,7 +62,6 @@ import io.getstream.video.android.ui.menu.base.DynamicMenu import io.getstream.video.android.ui.menu.base.MenuItem import io.getstream.video.android.util.filters.SampleAudioFilter import kotlinx.coroutines.launch -import org.openapitools.client.models.VideoResolution import java.nio.ByteBuffer @OptIn(ExperimentalPermissionsApi::class) 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 bc07fe68c3..b7405a72d4 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -67,8 +67,8 @@ public final class io/getstream/video/android/core/Call { public final fun setAudioProcessingEnabled (Z)V public final fun setIncomingVideoEnabled (Ljava/lang/Boolean;Ljava/util/List;)V public static synthetic fun setIncomingVideoEnabled$default (Lio/getstream/video/android/core/Call;Ljava/lang/Boolean;Ljava/util/List;ILjava/lang/Object;)V - public final fun setPreferredIncomingVideoResolution (Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;)V - public static synthetic fun setPreferredIncomingVideoResolution$default (Lio/getstream/video/android/core/Call;Lorg/openapitools/client/models/VideoResolution;Ljava/util/List;ILjava/lang/Object;)V + public final fun setPreferredIncomingVideoResolution (Lio/getstream/video/android/core/model/PreferredVideoResolution;Ljava/util/List;)V + public static synthetic fun setPreferredIncomingVideoResolution$default (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/model/PreferredVideoResolution;Ljava/util/List;ILjava/lang/Object;)V public final fun setSessionId (Ljava/lang/String;)V public final fun setVideoFilter (Lio/getstream/video/android/core/call/video/VideoFilter;)V public final fun setVisibility (Ljava/lang/String;Lstream/video/sfu/models/TrackType;Z)V @@ -3963,6 +3963,19 @@ public final class io/getstream/video/android/core/model/NetworkQuality$UnSpecif public fun toString ()Ljava/lang/String; } +public final class io/getstream/video/android/core/model/PreferredVideoResolution { + public fun (II)V + public final fun component1 ()I + public final fun component2 ()I + public final fun copy (II)Lio/getstream/video/android/core/model/PreferredVideoResolution; + public static synthetic fun copy$default (Lio/getstream/video/android/core/model/PreferredVideoResolution;IIILjava/lang/Object;)Lio/getstream/video/android/core/model/PreferredVideoResolution; + public fun equals (Ljava/lang/Object;)Z + public final fun getHeight ()I + public final fun getWidth ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/video/android/core/model/QueriedCalls { public fun (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V public final fun component1 ()Ljava/util/List; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index a98693e2b7..52a6c6e3f0 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -82,7 +82,6 @@ import org.openapitools.client.models.UpdateCallRequest import org.openapitools.client.models.UpdateCallResponse import org.openapitools.client.models.UpdateUserPermissionsResponse import org.openapitools.client.models.VideoEvent -import org.openapitools.client.models.VideoResolution import org.openapitools.client.models.VideoSettingsResponse import org.threeten.bp.OffsetDateTime import org.webrtc.PeerConnection diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt index 894bda9f7a..d2dfb1c65e 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/model/PreferredVideoResolution.kt @@ -1,6 +1,22 @@ +/* + * 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. + */ + package io.getstream.video.android.core.model data class PreferredVideoResolution( val width: Int, - val height: Int -) \ No newline at end of file + val height: Int, +) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt index 68057f7b5c..1abe4828e4 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt @@ -194,8 +194,8 @@ public fun VideoRenderer( } private fun isIncomingVideoEnabled(call: Call, sessionId: String, videoEnabledOverrides: Map) = - (videoEnabledOverrides[sessionId] ?: videoEnabledOverrides[ALL_PARTICIPANTS]) != false - || call.state.me.value?.sessionId == sessionId + (videoEnabledOverrides[sessionId] ?: videoEnabledOverrides[ALL_PARTICIPANTS]) != false || + call.state.me.value?.sessionId == sessionId private fun cleanTrack( view: VideoTextureViewRenderer?,