diff --git a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api index 5370b2c7b7..fd7ab7b159 100644 --- a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api +++ b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api @@ -1857,6 +1857,7 @@ public final class io/getstream/video/android/compose/ui/components/video/Compos } public final class io/getstream/video/android/compose/ui/components/video/VideoRendererKt { + public static final fun VideoRenderer (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState$Media;Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun VideoRenderer (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState$Media;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -1878,3 +1879,57 @@ public final class io/getstream/video/android/compose/ui/components/video/VideoS public static fun values ()[Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; } +public final class io/getstream/video/android/compose/ui/components/video/config/ComposableSingletons$VideoRendererConfigKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/video/config/ComposableSingletons$VideoRendererConfigKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig { + public static final field $stable I + public fun ()V + public fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun copy (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig;ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getFallbackContent ()Lkotlin/jvm/functions/Function3; + public final fun getMirrorStream ()Z + public final fun getScalingType ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public fun hashCode ()I + public final fun setMirrorStream (Z)V + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope { + public static final field $stable I + public fun ()V + public fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun copy (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope;ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope; + public fun equals (Ljava/lang/Object;)Z + public final fun getFallbackContent ()Lkotlin/jvm/functions/Function3; + public final fun getMirrorStream ()Z + public final fun getVideoScalingType ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public fun hashCode ()I + public final fun setFallbackContent (Lkotlin/jvm/functions/Function3;)V + public final fun setMirrorStream (Z)V + public final fun setVideoScalingType (Lio/getstream/video/android/compose/ui/components/video/VideoScalingType;)V + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigKt { + public static final fun videoRenderConfig (Lkotlin/jvm/functions/Function1;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public static synthetic fun videoRenderConfig$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; +} + diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index b54f34d2a7..bff570d7ab 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -254,19 +255,36 @@ internal fun DefaultPictureInPictureContent(call: Call) { video = video?.value, ) } else { - val activeSpeakers by call.state.activeSpeakers.collectAsStateWithLifecycle() val me by call.state.me.collectAsStateWithLifecycle() + val participants by call.state.participants.collectAsStateWithLifecycle() + val notMeOfTwo by remember { + // Special case where there are only two participants to take always the other participant, + // regardless of video track. + derivedStateOf { + participants.takeIf { + it.size == 2 + }?.firstOrNull { it.sessionId != me?.sessionId } + } + } + val activeSpeakers by call.state.activeSpeakers.collectAsStateWithLifecycle() + val dominantSpeaker by call.state.dominantSpeaker.collectAsStateWithLifecycle() + val notMeActiveOrDominant by remember { + derivedStateOf { + val activeNotMe = activeSpeakers.firstOrNull { + it.sessionId != me?.sessionId + } + val dominantNotMe = dominantSpeaker?.takeUnless { + it.sessionId == me?.sessionId + } - if (activeSpeakers.isNotEmpty()) { - ParticipantVideo( - call = call, - participant = activeSpeakers.first(), - style = RegularVideoRendererStyle(labelPosition = Alignment.BottomStart), - ) - } else if (me != null) { + activeNotMe ?: dominantNotMe + } + } + val participantToShow = notMeOfTwo ?: notMeActiveOrDominant ?: me + if (participantToShow != null) { ParticipantVideo( call = call, - participant = me!!, + participant = participantToShow, style = RegularVideoRendererStyle(labelPosition = Alignment.BottomStart), ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt index 40849aa16c..53496d75fe 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt @@ -82,7 +82,9 @@ import io.getstream.video.android.compose.ui.components.indicator.GenericIndicat import io.getstream.video.android.compose.ui.components.indicator.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.indicator.SoundIndicator import io.getstream.video.android.compose.ui.components.video.VideoRenderer +import io.getstream.video.android.compose.ui.components.video.config.videoRenderConfig import io.getstream.video.android.core.Call +import io.getstream.video.android.core.CameraDirection import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.model.NetworkQuality import io.getstream.video.android.core.model.Reaction @@ -246,11 +248,20 @@ public fun ParticipantVideoRenderer( } val video by participant.video.collectAsStateWithLifecycle() - + val cameraDirection by call.camera.direction.collectAsStateWithLifecycle() + val me by call.state.me.collectAsStateWithLifecycle() + val mirror by remember { + derivedStateOf { + participant.sessionId == me?.sessionId && cameraDirection == CameraDirection.Front + } + } VideoRenderer( call = call, video = video, - videoFallbackContent = videoFallbackContent, + videoRendererConfig = videoRenderConfig { + mirrorStream = mirror + this.fallbackContent = videoFallbackContent + }, ) } 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 e9f0da894c..4077f9e3c5 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 @@ -46,6 +46,8 @@ import androidx.compose.ui.viewinterop.AndroidView 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 +import io.getstream.video.android.compose.ui.components.video.config.VideoRendererConfig +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.model.MediaTrack @@ -55,28 +57,12 @@ import io.getstream.video.android.mock.previewCall import io.getstream.video.android.ui.common.renderer.StreamVideoTextureViewRenderer import io.getstream.webrtc.android.ui.VideoTextureViewRenderer -/** - * Renders a single video track based on the call state. - * - * @param call The call state that contains all the tracks and participants. - * @param video A media contains a video track or an audio track to be rendered. - * @param modifier Modifier for styling. - * @param videoScalingType Setup the video scale type of this renderer. - * @param videoFallbackContent Content is shown the video track is failed to load or not available. - * @param onRendered An interface that will be invoked when the video is rendered. - */ @Composable public fun VideoRenderer( + modifier: Modifier = Modifier, call: Call, video: ParticipantState.Media?, - modifier: Modifier = Modifier, - videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, - videoFallbackContent: @Composable (Call) -> Unit = { - DefaultMediaTrackFallbackContent( - modifier, - call, - ) - }, + videoRendererConfig: VideoRendererConfig = videoRenderConfig(), onRendered: (VideoTextureViewRenderer) -> Unit = {}, ) { if (LocalInspectionMode.current) { @@ -94,7 +80,7 @@ public fun VideoRenderer( } // Show avatar always behind the video. - videoFallbackContent.invoke(call) + videoRendererConfig.fallbackContent.invoke(call) if (video?.enabled == true) { val mediaTrack = video.track @@ -125,7 +111,10 @@ public fun VideoRenderer( trackType = trackType, onRendered = onRendered, ) - setScalingType(scalingType = videoScalingType.toCommonScalingType()) + setMirror(videoRendererConfig.mirrorStream) + setScalingType( + scalingType = videoRendererConfig.scalingType.toCommonScalingType(), + ) setupVideo(mediaTrack, this) view = this @@ -139,6 +128,43 @@ public fun VideoRenderer( } } +/** + * Renders a single video track based on the call state. + * + * @param call The call state that contains all the tracks and participants. + * @param video A media contains a video track or an audio track to be rendered. + * @param modifier Modifier for styling. + * @param videoScalingType Setup the video scale type of this renderer. + * @param videoFallbackContent Content is shown the video track is failed to load or not available. + * @param onRendered An interface that will be invoked when the video is rendered. + */ +@Deprecated("Use VideoRenderer which accepts `videoConfig` instead.") +@Composable +public fun VideoRenderer( + call: Call, + video: ParticipantState.Media?, + modifier: Modifier = Modifier, + videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + videoFallbackContent: @Composable (Call) -> Unit = { + DefaultMediaTrackFallbackContent( + modifier, + call, + ) + }, + onRendered: (VideoTextureViewRenderer) -> Unit = {}, +) { + VideoRenderer( + call = call, + video = video, + modifier = modifier, + videoRendererConfig = videoRenderConfig { + this.videoScalingType = videoScalingType + this.fallbackContent = videoFallbackContent + }, + onRendered = onRendered, + ) +} + private fun cleanTrack( view: VideoTextureViewRenderer?, mediaTrack: MediaTrack?, @@ -154,7 +180,6 @@ private fun cleanTrack( } } } - private fun setupVideo( mediaTrack: MediaTrack?, renderer: VideoTextureViewRenderer, @@ -171,7 +196,7 @@ private fun setupVideo( } @Composable -private fun DefaultMediaTrackFallbackContent( +internal fun DefaultMediaTrackFallbackContent( modifier: Modifier, call: Call, ) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt new file mode 100644 index 0000000000..e51d00ae44 --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt @@ -0,0 +1,58 @@ +/* + * 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.compose.ui.components.video.config + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import io.getstream.video.android.compose.ui.components.video.DefaultMediaTrackFallbackContent +import io.getstream.video.android.compose.ui.components.video.VideoScalingType +import io.getstream.video.android.core.Call + +@Immutable +public data class VideoRendererConfig( + var mirrorStream: Boolean = false, + val scalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + val fallbackContent: @Composable (Call) -> Unit = {}, +) + +@Immutable +public data class VideoRendererConfigCreationScope( + public var mirrorStream: Boolean = false, + public var videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + public var fallbackContent: @Composable (Call) -> Unit = { + DefaultMediaTrackFallbackContent( + modifier = Modifier, + call = it, + ) + }, +) + +/** + * A builder method for a video renderer config. + */ +public inline fun videoRenderConfig( + block: VideoRendererConfigCreationScope.() -> Unit = {}, +): VideoRendererConfig { + val scope = VideoRendererConfigCreationScope() + scope.block() + return VideoRendererConfig( + mirrorStream = scope.mirrorStream, + scalingType = scope.videoScalingType, + fallbackContent = scope.fallbackContent, + ) +}