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 98568aa6d6c..59114018cf8 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 @@ -113,8 +113,11 @@ 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.collectLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.openapitools.client.models.OwnCapability +import org.openapitools.client.models.TranscriptionSettingsResponse @OptIn(ExperimentalMaterialApi::class) @Composable @@ -218,6 +221,34 @@ fun CallScreen( } } + /** + * AUTO START/STOP TRANSCRIPTION LOGIC + * + * This code handles the automatic transcription logic, ensuring transcription starts or stops + * based on the current settings and state. While it usually behaves as expected, consider the following scenario: + * + * - Transcription is set to "Auto-On" in the settings. + * - The current transcription state (`isCurrentlyTranscribing`) is `false` because it was toggled by a participant. + * - A new participant joins the call. + * + * In this scenario, the transcription will automatically start, overriding the previous `false` state. + * This behavior is intentional for this demo-app ONLY and designed to prioritize the "Auto-On" setting over the current state. + * + * Please keep this behavior in mind, as it might appear unexpected at first glance. + * + * Note: Occasionally, when `call.startTranscription()` might throw a 400 error in the demo app when Transcription is set to "Auto-On", indicating that + * transcription is already in progress. This is expected and can safely be ignored as it does not impact + * the ongoing transcription functionality. + */ + val isCurrentlyTranscribing by call.state.transcribing.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + call.state.settings.map { it?.transcription } + .collectLatest { transcription -> + executeTranscriptionApis(call, isCurrentlyTranscribing, transcription) + } + } + VideoTheme { ChatDialog( state = chatState, @@ -682,6 +713,27 @@ fun CallScreen( } } +/** + * Executes the transcription APIs based on the current transcription state and settings. + * + * - Stops transcription if the mode is "Disabled" and transcription is currently active. + * - Starts transcription if the mode is "Auto-On" and transcription is not currently active. + * - Takes no action for other scenarios. + */ +private suspend fun executeTranscriptionApis( + call: Call, + transcribing: Boolean, + transcriptionSettingsResponse: + TranscriptionSettingsResponse?, +) { + val mode = transcriptionSettingsResponse?.mode + if (mode == TranscriptionSettingsResponse.Mode.Disabled && transcribing) { + call.stopTranscription() + } else if (mode == TranscriptionSettingsResponse.Mode.AutoOn && !transcribing) { + call.startTranscription() + } else { } +} + @Composable private fun SpeakingWhileMuted() { Snackbar { 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 a00fc1f572e..145044a4bd9 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 @@ -21,6 +21,7 @@ import android.os.Build import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.MobileScreenShare import androidx.compose.material.icons.automirrored.filled.ReadMore +import androidx.compose.material.icons.automirrored.filled.ReceiptLong import androidx.compose.material.icons.filled.AspectRatio import androidx.compose.material.icons.filled.Audiotrack import androidx.compose.material.icons.filled.AutoGraph @@ -54,6 +55,8 @@ 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 kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch /** * Defines the default Stream menu for the demo app. @@ -83,6 +86,9 @@ fun defaultStreamMenu( onSelectScaleType: (VideoScalingType) -> Unit, availableDevices: List, loadRecordings: suspend () -> List, + transcriptionUiState: TranscriptionUiState, + onToggleTranscription: suspend () -> Unit, + loadTranscriptions: suspend () -> List, onToggleClosedCaptions: () -> Unit = {}, closedCaptionUiState: ClosedCaptionUiState, ) = buildList { @@ -202,6 +208,34 @@ fun defaultStreamMenu( ), ), ) + + when (transcriptionUiState) { + is TranscriptionAvailableUiState, TranscriptionStoppedUiState -> { + add( + ActionMenuItem( + title = transcriptionUiState.text, + icon = transcriptionUiState.icon, + highlight = transcriptionUiState.highlight, + action = { + GlobalScope.launch { + onToggleTranscription.invoke() + } + }, + ), + ) + + add( + DynamicSubMenuItem( + title = "List Transcriptions", + icon = Icons.AutoMirrored.Filled.ReceiptLong, + itemsLoader = loadTranscriptions, + ), + ) + } + + else -> {} + } + add(getCCActionMenu(closedCaptionUiState, onToggleClosedCaptions)) if (showDebugOptions) { add( 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 6ad3c99373d..9092764cdce 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.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 +import io.getstream.video.android.ui.menu.transcriptions.TranscriptionUiStateManager import io.getstream.video.android.util.filters.SampleAudioFilter import kotlinx.coroutines.launch import java.nio.ByteBuffer @@ -194,6 +195,48 @@ internal fun SettingsMenu( } } + val isCurrentlyTranscribing by call.state.transcribing.collectAsStateWithLifecycle() + val settings by call.state.settings.collectAsStateWithLifecycle() + + // Use the manager to determine the UI state + val transcriptionUiStateManager = + TranscriptionUiStateManager(isCurrentlyTranscribing, settings) + val transcriptionUiState = transcriptionUiStateManager.getUiState() + + val onToggleTranscription: suspend () -> Unit = { + when (transcriptionUiState) { + TranscriptionAvailableUiState -> call.startTranscription() + TranscriptionStoppedUiState -> call.stopTranscription() + else -> { + throw IllegalStateException( + "Toggling of transcription should not work in state: $transcriptionUiState", + ) + } + } + } + + val onLoadTranscriptions: suspend () -> List = storagePermissionAndroidBellow10 { + when (it) { + is PermissionStatus.Granted -> { + { + call.listTranscription().getOrNull()?.transcriptions?.map { + ActionMenuItem( + title = it.filename, + icon = Icons.Default.VideoFile, // TODO Rahul check this later + action = { + context.downloadFile(it.url, it.filename) + onDismissed() + }, + ) + } ?: emptyList() + } + } + is PermissionStatus.Denied -> { + { emptyList() } + } + } + } + Popup( offset = IntOffset( 0, @@ -255,6 +298,9 @@ internal fun SettingsMenu( loadRecordings = onLoadRecordings, onToggleClosedCaptions = onClosedCaptionsToggle, closedCaptionUiState = closedCaptionUiState, + transcriptionUiState = transcriptionUiState, + onToggleTranscription = onToggleTranscription, + loadTranscriptions = onLoadTranscriptions, ), ) } @@ -324,6 +370,9 @@ private fun SettingsMenuPreview() { loadRecordings = { emptyList() }, onToggleClosedCaptions = { }, closedCaptionUiState = ClosedCaptionUiState.Available, + transcriptionUiState = TranscriptionAvailableUiState, + onToggleTranscription = {}, + loadTranscriptions = { emptyList() }, ), ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/TranscriptionUiState.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/TranscriptionUiState.kt new file mode 100644 index 00000000000..0e606a6941b --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/TranscriptionUiState.kt @@ -0,0 +1,52 @@ +/* + * 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.menu + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Description +import androidx.compose.ui.graphics.vector.ImageVector + +sealed class TranscriptionUiState( + val text: String, + val icon: ImageVector, // Assuming it's a drawable resource ID + val highlight: Boolean, +) + +/** + * Stop Transcription + * Start Transcription + * Transcription is disabled + * Transcription failed + */ + +data object TranscriptionAvailableUiState : TranscriptionUiState( + text = "Transcribe the call", + icon = Icons.Default.Description, + highlight = false, +) + +data object TranscriptionStoppedUiState : TranscriptionUiState( + text = "Stop Transcription", + icon = Icons.Default.Description, + highlight = true, +) + +data object TranscriptionDisabledUiState : TranscriptionUiState( + text = "Transcription not available", + icon = Icons.Default.Description, + highlight = false, +) 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 76bb9b73633..2672adc502f 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 @@ -47,6 +47,7 @@ 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.TranscriptionAvailableUiState import io.getstream.video.android.ui.menu.debugSubmenu import io.getstream.video.android.ui.menu.defaultStreamMenu import io.getstream.video.android.ui.menu.reconnectMenu @@ -232,6 +233,9 @@ private fun DynamicMenuPreview() { onToggleIncomingVideoEnabled = {}, onSelectScaleType = {}, loadRecordings = { emptyList() }, + transcriptionUiState = TranscriptionAvailableUiState, + onToggleTranscription = {}, + loadTranscriptions = { emptyList() }, onToggleClosedCaptions = {}, closedCaptionUiState = ClosedCaptionUiState.Available, ), @@ -269,6 +273,9 @@ private fun DynamicMenuDebugOptionPreview() { loadRecordings = { emptyList() }, onToggleClosedCaptions = {}, closedCaptionUiState = ClosedCaptionUiState.Available, + transcriptionUiState = TranscriptionAvailableUiState, + onToggleTranscription = {}, + loadTranscriptions = { emptyList() }, ), ) } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/transcriptions/TranscriptionUiStateManager.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/transcriptions/TranscriptionUiStateManager.kt new file mode 100644 index 00000000000..70e5815ea11 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/transcriptions/TranscriptionUiStateManager.kt @@ -0,0 +1,50 @@ +/* + * 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.menu.transcriptions + +import io.getstream.video.android.ui.menu.TranscriptionAvailableUiState +import io.getstream.video.android.ui.menu.TranscriptionDisabledUiState +import io.getstream.video.android.ui.menu.TranscriptionStoppedUiState +import io.getstream.video.android.ui.menu.TranscriptionUiState +import org.openapitools.client.models.CallSettingsResponse +import org.openapitools.client.models.TranscriptionSettingsResponse + +class TranscriptionUiStateManager( + private val isTranscribing: Boolean, + private val settings: CallSettingsResponse?, +) { + + fun getUiState(): TranscriptionUiState { + return if (settings != null) { + val mode = settings.transcription.mode + when (mode) { + TranscriptionSettingsResponse.Mode.Available, TranscriptionSettingsResponse.Mode.AutoOn -> { + if (isTranscribing) { + TranscriptionStoppedUiState + } else { + TranscriptionAvailableUiState + } + } + else -> { + TranscriptionDisabledUiState + } + } + } else { + TranscriptionDisabledUiState + } + } +}