Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6994458
1. Expose CC start and stop Rest Apis
rahul-lohra Dec 12, 2024
df1b6cf
Closed Captions Demo-app implementation
rahul-lohra Dec 17, 2024
d09a77a
1. Expose CC start and stop Rest Apis
rahul-lohra Dec 12, 2024
7927385
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 17, 2024
c5a8414
Add comments
rahul-lohra Dec 19, 2024
63ddc39
Merge branch 'refs/heads/feature/rahullohra/expose_cc_apis' into feat…
rahul-lohra Dec 19, 2024
bb7ff51
1. Expose CC start and stop Rest Apis
rahul-lohra Dec 12, 2024
0126d04
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 23, 2024
85a07e2
Update api files
rahul-lohra Dec 23, 2024
d27e784
Merge remote-tracking branch 'origin/feature/rahullohra/closed_captio…
rahul-lohra Dec 23, 2024
526c185
Change access modifier of closedCaptionManager to public
rahul-lohra Dec 23, 2024
903f709
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 23, 2024
d369c6f
Expose isClosedCaptioning and closedCaptions from Call State to follo…
rahul-lohra Dec 24, 2024
58f889c
Expose isClosedCaptioning and closedCaptions from Call State to follo…
rahul-lohra Dec 24, 2024
8c7189e
Revert "Expose isClosedCaptioning and closedCaptions from Call State …
rahul-lohra Dec 24, 2024
3080172
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 24, 2024
bb1b14f
Expose ccMode from Call State
rahul-lohra Dec 24, 2024
9a4a554
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 24, 2024
0c8bd22
Minor update
rahul-lohra Dec 24, 2024
3772869
Add logic for deduplication of closed captions
rahul-lohra Dec 24, 2024
c2b08fb
Merge branch 'feature/rahullohra/expose_cc_apis' into feature/rahullo…
rahul-lohra Dec 24, 2024
55bb0d0
Merge branch 'refs/heads/develop' into feature/rahullohra/closed_capt…
aleksandar-apostolov Dec 24, 2024
e83b89d
Resolve conflict, regenerate api dump, move menu item above the debug…
aleksandar-apostolov Dec 24, 2024
08272be
Small update to the design
aleksandar-apostolov Dec 24, 2024
e217e18
Merge branch 'develop' into feature/rahullohra/closed_caption_demo_app
aleksandar-apostolov Dec 24, 2024
f6defc0
Small update to the design, spotless apidump
aleksandar-apostolov Dec 24, 2024
ec28fdb
Merge remote-tracking branch 'origin/feature/rahullohra/closed_captio…
aleksandar-apostolov Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import io.getstream.video.android.compose.ui.components.base.StreamIconToggleBut
import io.getstream.video.android.compose.ui.components.call.CallAppBar
import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
import io.getstream.video.android.compose.ui.components.call.controls.actions.ChatDialogAction
import io.getstream.video.android.compose.ui.components.call.controls.actions.ClosedCaptionsToggleAction
import io.getstream.video.android.compose.ui.components.call.controls.actions.DefaultOnCallActionHandler
import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction
import io.getstream.video.android.compose.ui.components.call.controls.actions.GenericAction
Expand All @@ -102,6 +103,10 @@ 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.menu.SettingsMenu
import io.getstream.video.android.ui.menu.VideoFilter
import io.getstream.video.android.ui.menu.availableVideoFilters
Expand Down Expand Up @@ -176,6 +181,43 @@ fun CallScreen(
PaddingValues(0.dp)
}

/**
* Logic to Closed Captions UI State and render UI accordingly
*/

val ccMode by call.state.ccMode.collectAsStateWithLifecycle()
val captioning by call.state.isCaptioning.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,
Expand Down Expand Up @@ -261,6 +303,13 @@ fun CallScreen(
},
)
Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM))
ClosedCaptionsToggleAction(
active = closedCaptionUiState == ClosedCaptionUiState.Running,
onCallAction = {
onLocalClosedCaptionsClick.invoke()
},
)
Spacer(modifier = Modifier.size(VideoTheme.dimens.spacingM))
if (call.hasCapability(OwnCapability.StartRecordCall) || call.hasCapability(
OwnCapability.StopRecordCall,
)
Expand Down Expand Up @@ -379,6 +428,23 @@ fun CallScreen(
}
}
},
closedCaptionUi = { call ->
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
ClosedCaptionsContainer(
call,
ClosedCaptionsDefaults.streamThemeConfig(),
closedCaptionUiState,
)
} else {
ClosedCaptionsContainer(
call,
ClosedCaptionsDefaults.streamThemeConfig().copy(
yOffset = (-80).dp,
),
closedCaptionUiState,
)
}
},
)
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
StreamIconToggleButton(
Expand Down Expand Up @@ -531,6 +597,8 @@ fun CallScreen(
isShowingStats = true
isShowingSettingMenu = false
},
closedCaptionUiState = closedCaptionUiState,
onClosedCaptionsToggle = onLocalClosedCaptionsClick,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.streamThemeConfig()
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.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<ClosedCaptionUiModel>,
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.Start,
) {
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,
)
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading
Loading