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 52ac27132a..025b5522f3 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 @@ -102,6 +102,11 @@ import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall import io.getstream.video.android.tooling.extensions.toPx import io.getstream.video.android.tooling.util.StreamFlavors +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionUiState +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionUiState.Available.toClosedCaptionUiState +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionsContainer +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionsDefaults +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionsThemeConfig import io.getstream.video.android.ui.menu.SettingsMenu import io.getstream.video.android.ui.menu.VideoFilter import io.getstream.video.android.ui.menu.availableVideoFilters @@ -176,6 +181,43 @@ fun CallScreen( PaddingValues(0.dp) } + /** + * Logic to Closed Captions UI State and render UI accordingly + */ + + val ccMode by call.state.closedCaptionManager.ccMode.collectAsStateWithLifecycle() + val captioning by call.state.closedCaptionManager.closedCaptioning.collectAsStateWithLifecycle() + + var closedCaptionUiState: ClosedCaptionUiState by remember { + mutableStateOf(ccMode.toClosedCaptionUiState()) + } + + val updateClosedCaptionUiState: (ClosedCaptionUiState) -> Unit = { newState -> + closedCaptionUiState = newState + } + + val onLocalClosedCaptionsClick: () -> Unit = { + scope.launch { + when (closedCaptionUiState) { + is ClosedCaptionUiState.Running -> { + updateClosedCaptionUiState(ClosedCaptionUiState.Available) + } + is ClosedCaptionUiState.Available -> { + if (captioning) { + updateClosedCaptionUiState(ClosedCaptionUiState.Running) + } else { + call.startClosedCaptions() + } + } + else -> { + throw Exception( + "This state $closedCaptionUiState should not invoke any ui operation", + ) + } + } + } + } + VideoTheme { ChatDialog( state = chatState, @@ -379,6 +421,21 @@ fun CallScreen( } } }, + closedCaptionUi = { call -> + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + ClosedCaptionsContainer( + call, + ClosedCaptionsDefaults.config, + closedCaptionUiState, + ) + } else { + ClosedCaptionsContainer( + call, + ClosedCaptionsThemeConfig(yOffset = -80.dp), + closedCaptionUiState, + ) + } + }, ) if (orientation == Configuration.ORIENTATION_LANDSCAPE) { StreamIconToggleButton( @@ -531,6 +588,8 @@ fun CallScreen( isShowingStats = true isShowingSettingMenu = false }, + closedCaptionUiState = closedCaptionUiState, + onClosedCaptionsToggle = onLocalClosedCaptionsClick, ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUi.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUi.kt new file mode 100644 index 0000000000..a8a3efdf4f --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUi.kt @@ -0,0 +1,210 @@ +/* + * 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.ui.closedcaptions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.video.android.core.Call +import org.openapitools.client.models.CallClosedCaption + +/** + * A set of composables and supporting classes for displaying and customizing closed captions in a call. + * + * This collection includes a demo preview, the main container for closed captions, + * and UI elements for rendering individual captions and caption lists. + */ + +/** + * A preview function for displaying a demo of the closed captions list. + * + * Demonstrates how the [ClosedCaptionList] renders multiple captions with default configurations. + * Useful for testing and visualizing the closed captions UI in isolation. + */ +@Preview +@Composable +public fun ClosedCaptionListDemo() { + val config = ClosedCaptionsDefaults.config + ClosedCaptionList( + arrayListOf( + ClosedCaptionUiModel("Rahul", "This is closed captions text in Call Content"), + ClosedCaptionUiModel("Princy", "Hi I am Princy"), + ClosedCaptionUiModel("Meenu", "Hi I am Meenu, I am from Noida. I am a physiotherapist"), + ), + config, + ) +} + +/** + * A composable container for rendering closed captions in a call. + * + * This container adapts its behavior based on the environment: + * - In `LocalInspectionMode`, it displays a static demo of closed captions using [ClosedCaptionListDemo]. + * - During a live call, it listens to the state of the [Call]'s [ClosedCaptionManager] to render + * dynamically updated captions. + * + * @param call The current [Call] instance, providing state and caption data. + * @param config A [ClosedCaptionsThemeConfig] defining the styling and positioning of the container. + */ +@Composable +public fun ClosedCaptionsContainer( + call: Call, + config: ClosedCaptionsThemeConfig = ClosedCaptionsDefaults.config, + closedCaptionUiState: ClosedCaptionUiState, +) { + if (LocalInspectionMode.current) { + Box( + modifier = Modifier + .fillMaxSize() + .offset(y = config.yOffset) + .padding(horizontal = config.horizontalMargin), + + contentAlignment = Alignment.BottomCenter, + ) { + ClosedCaptionListDemo() + } + } else { + val closedCaptions by call.state.closedCaptionManager.closedCaptions + .collectAsStateWithLifecycle() + + if (closedCaptionUiState == ClosedCaptionUiState.Running && closedCaptions.isNotEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .offset(y = config.yOffset) + .padding(horizontal = config.horizontalMargin), + + contentAlignment = Alignment.BottomCenter, + ) { + ClosedCaptionList(closedCaptions.map { it.toClosedCaptionUiModel(call) }, config) + } + } + } +} + +/** + * A composable function for displaying a list of closed captions. + * + * This function uses a [LazyColumn] to display captions with a background, padding, + * and styling defined in the provided [config]. It limits the number of visible captions + * to [ClosedCaptionsThemeConfig.maxVisibleCaptions]. + * + * @param captions The list of [ClosedCaptionUiModel]s to display. + * @param config A [ClosedCaptionsThemeConfig] defining the layout and styling of the caption list. + */ + +@Composable +public fun ClosedCaptionList( + captions: List, + config: ClosedCaptionsThemeConfig, +) { + LazyColumn( + modifier = Modifier + .background( + color = Color.Black.copy(alpha = config.boxAlpha), + shape = RoundedCornerShape(16.dp), + ) + .fillMaxWidth() + .padding(config.boxPadding), + userScrollEnabled = false, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + itemsIndexed(captions.takeLast(config.maxVisibleCaptions)) { index, item -> + ClosedCaptionUi(item, index != captions.size - 1, config) + } + } +} + +/** + * A composable function for rendering an individual closed caption. + * + * Displays the speaker's name and their caption text, with optional semi-transparency for + * earlier captions (controlled by [semiFade]). + * + * @param closedCaptionUiModel The [ClosedCaptionUiModel] containing the speaker and text. + * @param semiFade Whether to render the caption with reduced opacity. + * @param config A [ClosedCaptionsThemeConfig] defining the text colors and styling. + */ + +@Composable +public fun ClosedCaptionUi( + closedCaptionUiModel: ClosedCaptionUiModel, + semiFade: Boolean, + config: ClosedCaptionsThemeConfig, +) { + val alpha = if (semiFade) 0.6f else 1f + + val formattedSpeakerText = closedCaptionUiModel.speaker + ":" + + Row( + modifier = Modifier.alpha(alpha), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(formattedSpeakerText, color = config.speakerColor) + Text( + closedCaptionUiModel.text, + color = config.textColor, + modifier = Modifier.wrapContentWidth(), + ) + } +} + +/** + * Represents a single closed caption with the speaker's name and their text. + * + * @property speaker The name of the speaker for this caption. + * @property text The text of the caption. + */ +public data class ClosedCaptionUiModel(val speaker: String, val text: String) + +/** + * Converts a [CallClosedCaption] into a [ClosedCaptionUiModel] for UI rendering. + * + * Maps the speaker's ID to their name using the participants in the given [Call]. + * If the speaker cannot be identified, the speaker is labeled as "N/A". + * + * @param call The [Call] instance containing the list of participants. + * @return A [ClosedCaptionUiModel] containing the speaker's name and caption text. + */ +public fun CallClosedCaption.toClosedCaptionUiModel(call: Call): ClosedCaptionUiModel { + val participants = call.state.participants.value + val user = participants.firstOrNull { it.userId.value == this.speakerId } + return ClosedCaptionUiModel( + speaker = user?.userNameOrId?.value ?: "N/A", + text = this.text, + ) +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUiState.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUiState.kt new file mode 100644 index 0000000000..df800f5376 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionUiState.kt @@ -0,0 +1,49 @@ +/* + * 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.ui.closedcaptions + +import org.openapitools.client.models.TranscriptionSettingsResponse + +sealed class ClosedCaptionUiState { + /** + * Indicates that closed captions are available for the current call but are not actively running/displaying. + * This state usually occurs when the captioning feature is supported but not yet activated/displayed. + */ + data object Available : ClosedCaptionUiState() + + /** + * Indicates that closed captions are actively running and displaying captions during the call. + */ + data object Running : ClosedCaptionUiState() + + /** + * Indicates that closed captions are unavailable for the current call. + * This state is used when the feature is disabled or not supported. + */ + data object UnAvailable : ClosedCaptionUiState() + + public fun TranscriptionSettingsResponse.ClosedCaptionMode.toClosedCaptionUiState(): ClosedCaptionUiState { + return when (this) { + is TranscriptionSettingsResponse.ClosedCaptionMode.Available, + is TranscriptionSettingsResponse.ClosedCaptionMode.AutoOn, + -> + Available + else -> + UnAvailable + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionsTheme.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionsTheme.kt new file mode 100644 index 0000000000..af59b3a5e8 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/closedcaptions/ClosedCaptionsTheme.kt @@ -0,0 +1,96 @@ +/* + * 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.ui.closedcaptions + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Provides default configurations for the Closed Captions UI. + * + * The [ClosedCaptionsDefaults] object contains a predefined instance of [ClosedCaptionsThemeConfig], + * which serves as the default styling and behavior configuration for the closed captions UI. + * Developers can use this default configuration or provide a custom one to override specific values. + */ + +public object ClosedCaptionsDefaults { + /** + * The default configuration for closed captions, defining layout, styling, and behavior. + * + * - `yOffset`: Vertical offset for positioning the closed captions container. + * - `horizontalMargin`: Horizontal margin around the container. + * - `boxAlpha`: Opacity of the background box containing the captions. + * - `boxPadding`: Padding inside the background box. + * - `speakerColor`: Color used for the speaker's name text. + * - `textColor`: Color used for the caption text. + * - `maxVisibleCaptions`: The maximum number of captions to display in the container at once. + * - `roundedCornerShape`: The corner radius of the background box. + */ + public val config: ClosedCaptionsThemeConfig = ClosedCaptionsThemeConfig( + yOffset = -50.dp, + horizontalMargin = 16.dp, + boxAlpha = 0.5f, + boxPadding = 12.dp, + speakerColor = Color.Yellow, + textColor = Color.White, + maxVisibleCaptions = 3, + roundedCornerShape = 16.dp, + ) +} + +/** + * Defines the configuration for Closed Captions UI, allowing customization of its layout, styling, and behavior. + * + * This configuration can be used to style the closed captions container and its contents. Developers can + * customize the appearance by overriding specific values as needed. + * + * @param yOffset Vertical offset for the closed captions container. Negative values move the container upwards. + * @param horizontalMargin Horizontal margin around the container. + * @param boxAlpha Background opacity of the closed captions container, where `0.0f` is fully transparent + * and `1.0f` is fully opaque. + * @param boxPadding Padding inside the background box of the closed captions container. + * @param speakerColor Color used for rendering the speaker's name text. + * @param textColor Color used for rendering the caption text. + * @param maxVisibleCaptions The maximum number of captions visible at one time in the closed captions container. + * Must be less than or equal to [ClosedCaptionsConfig.maxCaptions] to ensure consistency. + * @param roundedCornerShape Corner radius for the background box of the closed captions container. + * + * Example Usage: + * ``` + * val customConfig = ClosedCaptionsThemeConfig( + * yOffset = -100.dp, + * horizontalMargin = 20.dp, + * boxAlpha = 0.7f, + * boxPadding = 16.dp, + * speakerColor = Color.Cyan, + * textColor = Color.Green, + * maxVisibleCaptions = 5, + * roundedCornerShape = 12.dp, + * ) + * ``` + */ +public data class ClosedCaptionsThemeConfig( + val yOffset: Dp = -50.dp, + val horizontalMargin: Dp = 16.dp, + val boxAlpha: Float = 0.5f, + val boxPadding: Dp = 12.dp, + val speakerColor: Color = Color.Yellow, + val textColor: Color = Color.White, + val maxVisibleCaptions: Int = 3, + val roundedCornerShape: Dp = 16.dp, +) 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 9aa22b0473..889e9690df 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 @@ -26,6 +26,9 @@ import androidx.compose.material.icons.filled.Audiotrack import androidx.compose.material.icons.filled.AutoGraph import androidx.compose.material.icons.filled.Balance import androidx.compose.material.icons.filled.BluetoothAudio +import androidx.compose.material.icons.filled.ClosedCaption +import androidx.compose.material.icons.filled.ClosedCaptionDisabled +import androidx.compose.material.icons.filled.ClosedCaptionOff import androidx.compose.material.icons.filled.Crop import androidx.compose.material.icons.filled.CropFree import androidx.compose.material.icons.filled.Feedback @@ -46,6 +49,7 @@ 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.closedcaptions.ClosedCaptionUiState 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 @@ -79,6 +83,8 @@ fun defaultStreamMenu( onSelectScaleType: (VideoScalingType) -> Unit, availableDevices: List, loadRecordings: suspend () -> List, + onToggleClosedCaptions: () -> Unit = {}, + closedCaptionUiState: ClosedCaptionUiState, ) = buildList { add( DynamicSubMenuItem( @@ -215,6 +221,39 @@ fun defaultStreamMenu( ), ) } + + add(getCCActionMenu(closedCaptionUiState, onToggleClosedCaptions)) +} + +fun getCCActionMenu( + closedCaptionUiState: ClosedCaptionUiState, + onToggleClosedCaptions: () -> Unit, +): ActionMenuItem { + return when (closedCaptionUiState) { + is ClosedCaptionUiState.Available -> { + ActionMenuItem( + title = "Start Closed Caption", + icon = Icons.Default.ClosedCaptionOff, + action = onToggleClosedCaptions, + ) + } + + is ClosedCaptionUiState.Running -> { + ActionMenuItem( + title = "Stop Closed Caption", + icon = Icons.Default.ClosedCaption, + action = onToggleClosedCaptions, + ) + } + + is ClosedCaptionUiState.UnAvailable -> { + ActionMenuItem( + title = "Closed Caption are unavailable", + icon = Icons.Default.ClosedCaptionDisabled, + action = { }, + ) + } + } } /** 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 53d4582c37..6ad3c99373 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 @@ -57,6 +57,7 @@ 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.closedcaptions.ClosedCaptionUiState import io.getstream.video.android.ui.menu.base.ActionMenuItem import io.getstream.video.android.ui.menu.base.DynamicMenu import io.getstream.video.android.ui.menu.base.MenuItem @@ -82,6 +83,8 @@ internal fun SettingsMenu( onToggleIncomingVideoVisibility: (Boolean) -> Unit, onShowCallStats: () -> Unit, onSelectScaleType: (VideoScalingType) -> Unit, + closedCaptionUiState: ClosedCaptionUiState, + onClosedCaptionsToggle: () -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -250,6 +253,8 @@ internal fun SettingsMenu( isScreenShareEnabled = isScreenSharing, onSelectScaleType = onSelectScaleType, loadRecordings = onLoadRecordings, + onToggleClosedCaptions = onClosedCaptionsToggle, + closedCaptionUiState = closedCaptionUiState, ), ) } @@ -317,6 +322,8 @@ private fun SettingsMenuPreview() { isIncomingVideoEnabled = true, onToggleIncomingVideoEnabled = {}, loadRecordings = { emptyList() }, + onToggleClosedCaptions = { }, + closedCaptionUiState = ClosedCaptionUiState.Available, ), ) } 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 5037a728de..76bb9b7363 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 @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.base.StreamToggleButton import io.getstream.video.android.compose.ui.components.base.styling.StyleSize +import io.getstream.video.android.ui.closedcaptions.ClosedCaptionUiState import io.getstream.video.android.ui.menu.debugSubmenu import io.getstream.video.android.ui.menu.defaultStreamMenu import io.getstream.video.android.ui.menu.reconnectMenu @@ -231,6 +232,8 @@ private fun DynamicMenuPreview() { onToggleIncomingVideoEnabled = {}, onSelectScaleType = {}, loadRecordings = { emptyList() }, + onToggleClosedCaptions = {}, + closedCaptionUiState = ClosedCaptionUiState.Available, ), ) } @@ -264,6 +267,8 @@ private fun DynamicMenuDebugOptionPreview() { isIncomingVideoEnabled = true, onToggleIncomingVideoEnabled = {}, loadRecordings = { emptyList() }, + onToggleClosedCaptions = {}, + closedCaptionUiState = ClosedCaptionUiState.Available, ), ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt index 378c18201b..27e1dd48a6 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/internal/PortraitVideoRenderer.kt @@ -190,7 +190,7 @@ internal fun BoxScope.PortraitVideoRenderer( if (callParticipants.size in 2..4) { val currentLocal by call.state.me.collectAsStateWithLifecycle() - if (currentLocal != null || LocalInspectionMode.current) { + if (currentLocal != null) { floatingVideoRenderer?.invoke(this, call, parentSize) ?: DefaultFloatingParticipantVideo( call = call, @@ -199,6 +199,15 @@ internal fun BoxScope.PortraitVideoRenderer( parentSize = parentSize, style = style, ) + } else if (LocalInspectionMode.current) { + floatingVideoRenderer?.invoke(this, call, parentSize) + ?: DefaultFloatingParticipantVideo( + call = call, + me = callParticipants.first(), + callParticipants = callParticipants, + parentSize = parentSize, + style = style, + ) } } }