Display RTT with captions
This commit is contained in:
Родитель
c91d18afac
Коммит
7c59a60ddb
|
@ -49,6 +49,7 @@ import com.azure.android.communication.ui.calling.redux.reducer.CallDiagnosticsR
|
|||
import com.azure.android.communication.ui.calling.redux.reducer.CallScreenInformationHeaderReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.CallStateReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.CaptionsReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.DeviceConfigurationReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.ErrorReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.LifecycleReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.LocalParticipantStateReducerImpl
|
||||
|
@ -58,7 +59,6 @@ import com.azure.android.communication.ui.calling.redux.reducer.PermissionStateR
|
|||
import com.azure.android.communication.ui.calling.redux.reducer.PipReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.Reducer
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.RttReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.DeviceConfigurationReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.reducer.ToastNotificationReducerImpl
|
||||
import com.azure.android.communication.ui.calling.redux.state.AppReduxState
|
||||
import com.azure.android.communication.ui.calling.redux.state.ReduxState
|
||||
|
@ -92,7 +92,11 @@ internal class DependencyInjectionContainerImpl(
|
|||
}
|
||||
|
||||
override val captionsDataManager by lazy {
|
||||
CaptionsDataManager(callingService, appStore)
|
||||
CaptionsDataManager(
|
||||
callingService,
|
||||
appStore,
|
||||
avatarViewManager,
|
||||
)
|
||||
}
|
||||
|
||||
override val navigationRouter by lazy {
|
||||
|
|
|
@ -77,6 +77,7 @@ internal class CallCompositeActivityViewModel(
|
|||
debugInfoManager = container.debugInfoManager,
|
||||
capabilitiesManager = container.capabilitiesManager,
|
||||
updatableOptionsManager = container.updatableOptionsManager,
|
||||
captionsDataManager = container.captionsDataManager,
|
||||
showSupportFormOption = container.configuration.callCompositeEventsHandler.getOnUserReportedHandlers().any(),
|
||||
enableMultitasking = container.configuration.enableMultitasking,
|
||||
isTelecomManagerEnabled = container.configuration.telecomManagerOptions != null,
|
||||
|
|
|
@ -212,14 +212,10 @@ internal class CallingViewModel(
|
|||
captionsLanguageSelectionListViewModel.init(state.captionsState, state.visibilityState)
|
||||
isCaptionsVisibleMutableFlow.value = shouldShowCaptionsUI(state.visibilityState, state.captionsState, state.rttState)
|
||||
captionsLayoutViewModel.init(
|
||||
coroutineScope,
|
||||
state.captionsState,
|
||||
isCaptionsVisibleMutableFlow.value,
|
||||
captionsDataManager,
|
||||
localUserIdentifier,
|
||||
avatarViewManager,
|
||||
state.deviceConfigurationState,
|
||||
)
|
||||
)
|
||||
|
||||
moreCallOptionsListViewModel.init(state.visibilityState, state.buttonState)
|
||||
super.init(coroutineScope)
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.azure.android.communication.ui.calling.presentation.fragment.calling.captions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
internal data class CaptionsRttEntryModel(
|
||||
val displayName: String,
|
||||
val displayText: String,
|
||||
val avatarBitmap: Bitmap?,
|
||||
val speakerRawId: String,
|
||||
val languageCode: String?,
|
||||
)
|
|
@ -4,8 +4,6 @@
|
|||
package com.azure.android.communication.ui.calling.presentation.fragment.calling.captions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.azure.android.communication.common.CommunicationIdentifier
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.AvatarViewManager
|
||||
import java.util.Date
|
||||
|
||||
internal enum class CaptionsRttType {
|
||||
|
@ -13,7 +11,8 @@ internal enum class CaptionsRttType {
|
|||
RTT,
|
||||
}
|
||||
|
||||
internal data class CaptionsRecord(
|
||||
internal data class CaptionsRttRecord(
|
||||
val avatarBitmap: Bitmap?,
|
||||
val displayName: String,
|
||||
val displayText: String,
|
||||
val speakerRawId: String,
|
||||
|
@ -22,26 +21,3 @@ internal data class CaptionsRecord(
|
|||
val timestamp: Date,
|
||||
val type: CaptionsRttType,
|
||||
)
|
||||
|
||||
internal fun CaptionsRecord.into(avatarViewManager: AvatarViewManager, identifier: CommunicationIdentifier?): CaptionsRttEntryModel {
|
||||
var speakerName = this.displayName
|
||||
var bitMap: Bitmap? = null
|
||||
|
||||
val remoteParticipantViewData = avatarViewManager.getRemoteParticipantViewData(this.speakerRawId)
|
||||
if (remoteParticipantViewData != null) {
|
||||
speakerName = remoteParticipantViewData.displayName
|
||||
bitMap = remoteParticipantViewData.avatarBitmap
|
||||
}
|
||||
val localParticipantViewData = avatarViewManager.callCompositeLocalOptions?.participantViewData
|
||||
if (localParticipantViewData != null && identifier?.rawId == this.speakerRawId) {
|
||||
speakerName = localParticipantViewData.displayName
|
||||
bitMap = localParticipantViewData.avatarBitmap
|
||||
}
|
||||
return CaptionsRttEntryModel(
|
||||
displayName = speakerName,
|
||||
displayText = this.displayText,
|
||||
avatarBitmap = bitMap,
|
||||
speakerRawId = this.speakerRawId,
|
||||
languageCode = this.languageCode
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import com.azure.android.communication.ui.calling.implementation.R
|
|||
import com.microsoft.fluentui.persona.AvatarView
|
||||
|
||||
internal class CaptionsRecyclerViewAdapter(
|
||||
private val captionsData: List<CaptionsRttEntryModel>
|
||||
private val captionsData: List<CaptionsRttRecord>
|
||||
) : RecyclerView.Adapter<CaptionsRecyclerViewAdapter.CaptionsViewHolder>() {
|
||||
class CaptionsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val messageTextView: TextView =
|
||||
|
|
|
@ -24,11 +24,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.azure.android.communication.ui.calling.implementation.R
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.CallingFragment
|
||||
import com.azure.android.communication.ui.calling.utilities.LocaleHelper
|
||||
import com.azure.android.communication.ui.calling.utilities.isTablet
|
||||
import com.azure.android.communication.ui.calling.utilities.launchAll
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
@ -48,7 +47,7 @@ internal class CaptionsView : FrameLayout {
|
|||
private lateinit var captionsStartProgressLayout: LinearLayout
|
||||
private lateinit var recyclerViewAdapter: CaptionsRecyclerViewAdapter
|
||||
|
||||
private val captionsData = mutableListOf<CaptionsRttEntryModel>()
|
||||
// private val captionsData = mutableListOf<CaptionsRttEntryModel>()
|
||||
private var isAtBottom = true
|
||||
private var isMaximized = false
|
||||
|
||||
|
@ -115,7 +114,7 @@ internal class CaptionsView : FrameLayout {
|
|||
viewModel: CaptionsViewModel,
|
||||
) {
|
||||
this.viewModel = viewModel
|
||||
recyclerViewAdapter = CaptionsRecyclerViewAdapter(captionsData)
|
||||
recyclerViewAdapter = CaptionsRecyclerViewAdapter(viewModel.captionsAndRttData)
|
||||
recyclerView.adapter = recyclerViewAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this.context)
|
||||
|
||||
|
@ -126,95 +125,75 @@ internal class CaptionsView : FrameLayout {
|
|||
}
|
||||
})
|
||||
|
||||
viewModel.captionsAndRttDataCache.let { data ->
|
||||
captionsData.addAll(data)
|
||||
recyclerViewAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
recyclerView.post {
|
||||
recyclerView.scrollToPosition(recyclerViewAdapter.itemCount - 1)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.isVisibleFlow.collect {
|
||||
if (it) {
|
||||
captionsLinearLayout.visibility = View.VISIBLE
|
||||
captionsLinearLayout.post { scrollToBottom() }
|
||||
} else {
|
||||
captionsLinearLayout.visibility = View.GONE
|
||||
viewLifecycleOwner.lifecycleScope.launchAll(
|
||||
{
|
||||
viewModel.isVisibleFlow.collect {
|
||||
if (it) {
|
||||
captionsLinearLayout.visibility = View.VISIBLE
|
||||
captionsLinearLayout.post { scrollToBottom() }
|
||||
} else {
|
||||
captionsLinearLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
viewModel.recordUpdatedAtPositionSharedFlow.collect {
|
||||
onItemUpdated(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
viewModel.recordInsertedAtPositionSharedFlow.collect {
|
||||
onItemAdded(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
viewModel.recordRemovedAtPositionSharedFlow.collect {
|
||||
onItemRemoved(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
viewModel.captionsStartProgressStateFlow.collect {
|
||||
captionsStartProgressLayout.isVisible = it
|
||||
}
|
||||
},
|
||||
{
|
||||
viewModel.softwareKeyboardStateFlow.collect {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.onLastCaptionsDataUpdatedStateFlow.collect {
|
||||
it?.let {
|
||||
updateLastCaptionsData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.onNewCaptionsDataAddedStateFlow.collect { it ->
|
||||
it?.let {
|
||||
applyLayoutDirection(it)
|
||||
addNewCaptionsData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.captionsStartProgressStateFlow.collect {
|
||||
captionsStartProgressLayout.isVisible = it
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.softwareKeyboardStateFlow.collect {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateLastCaptionsData(lastCaptionsData: CaptionsRttEntryModel) {
|
||||
val index = captionsData.size - 1
|
||||
if (index >= 0 && captionsData[index].speakerRawId == lastCaptionsData.speakerRawId) {
|
||||
private fun onItemUpdated(index: Int) {
|
||||
if (index >= 0) {
|
||||
val shouldScrollToBottom = isAtBottom
|
||||
captionsData[index] = lastCaptionsData
|
||||
updateRecyclerViewItem(index)
|
||||
recyclerViewAdapter.notifyItemChanged(index)
|
||||
requestAccessibilityFocus(index)
|
||||
if (shouldScrollToBottom) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNewCaptionsData(newCaptionsData: CaptionsRttEntryModel) {
|
||||
if (captionsData.size >= CallingFragment.MAX_CAPTIONS_DATA_SIZE) {
|
||||
captionsData.removeAt(0)
|
||||
recyclerViewAdapter.notifyItemRemoved(0)
|
||||
}
|
||||
private fun onItemRemoved(index: Int) {
|
||||
recyclerViewAdapter.notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
private fun onItemAdded(index: Int) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val shouldScrollToBottom = isAtBottom || layoutManager.findLastVisibleItemPosition() == layoutManager.itemCount - 1
|
||||
|
||||
captionsData.add(newCaptionsData)
|
||||
insertRecyclerViewItem(captionsData.size - 1)
|
||||
recyclerViewAdapter.notifyItemInserted(index)
|
||||
requestAccessibilityFocus(index)
|
||||
if (shouldScrollToBottom) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRecyclerViewItem(position: Int) {
|
||||
recyclerViewAdapter.notifyItemChanged(position)
|
||||
requestAccessibilityFocus(position)
|
||||
}
|
||||
|
||||
private fun insertRecyclerViewItem(position: Int) {
|
||||
recyclerViewAdapter.notifyItemInserted(position)
|
||||
requestAccessibilityFocus(position)
|
||||
}
|
||||
|
||||
private fun requestAccessibilityFocus(position: Int) {
|
||||
val accessibilityManager = this.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
if (accessibilityManager.isEnabled) {
|
||||
|
@ -230,7 +209,7 @@ internal class CaptionsView : FrameLayout {
|
|||
}
|
||||
|
||||
// required when RTL language is selected for captions text
|
||||
private fun applyLayoutDirection(captionsRecord: CaptionsRttEntryModel) {
|
||||
private fun applyLayoutDirection(captionsRecord: CaptionsRttRecord) {
|
||||
if (LocaleHelper.isRTL(captionsRecord.languageCode) && layoutDirection != LAYOUT_DIRECTION_RTL) {
|
||||
captionsLinearLayout.layoutDirection = LAYOUT_DIRECTION_RTL
|
||||
} else if (!LocaleHelper.isRTL(captionsRecord.languageCode) && layoutDirection != LAYOUT_DIRECTION_LTR) {
|
||||
|
@ -239,7 +218,6 @@ internal class CaptionsView : FrameLayout {
|
|||
}
|
||||
|
||||
fun stop() {
|
||||
captionsData.clear()
|
||||
recyclerView.adapter = null
|
||||
recyclerView.layoutManager = null
|
||||
recyclerView.removeAllViews()
|
||||
|
|
|
@ -3,34 +3,28 @@
|
|||
|
||||
package com.azure.android.communication.ui.calling.presentation.fragment.calling.captions
|
||||
|
||||
import com.azure.android.communication.common.CommunicationIdentifier
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.AvatarViewManager
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.CaptionsDataManager
|
||||
import com.azure.android.communication.ui.calling.redux.action.Action
|
||||
import com.azure.android.communication.ui.calling.redux.action.RttAction
|
||||
import com.azure.android.communication.ui.calling.redux.state.CaptionsState
|
||||
import com.azure.android.communication.ui.calling.redux.state.CaptionsStatus
|
||||
import com.azure.android.communication.ui.calling.redux.state.DeviceConfigurationState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class CaptionsViewModel(
|
||||
private val dispatch: (Action) -> Unit,
|
||||
captionsDataManager: CaptionsDataManager,
|
||||
) {
|
||||
private lateinit var isVisibleMutableFlow: MutableStateFlow<Boolean>
|
||||
private lateinit var captionsStartInProgressStateMutableFlow: MutableStateFlow<Boolean>
|
||||
private lateinit var softwareKeyboardStateMutableFlow: MutableStateFlow<Boolean>
|
||||
|
||||
private val captionsData = mutableListOf<CaptionsRttEntryModel>()
|
||||
private val onLastCaptionsDataUpdatedMutableStateFlow = MutableStateFlow<CaptionsRttEntryModel?>(null)
|
||||
private val onNewCaptionsDataAddedMutableStateFlow = MutableStateFlow<CaptionsRttEntryModel?>(null)
|
||||
val captionsAndRttData = captionsDataManager.captionsAndRttData
|
||||
val recordUpdatedAtPositionSharedFlow = captionsDataManager.recordUpdatedAtPositionSharedFlow
|
||||
val recordInsertedAtPositionSharedFlow = captionsDataManager.recordInsertedAtPositionSharedFlow
|
||||
val recordRemovedAtPositionSharedFlow = captionsDataManager.recordRemovedAtPositionSharedFlow
|
||||
|
||||
val captionsAndRttDataCache: List<CaptionsRttEntryModel> = captionsData
|
||||
val onLastCaptionsDataUpdatedStateFlow: StateFlow<CaptionsRttEntryModel?> = onLastCaptionsDataUpdatedMutableStateFlow
|
||||
val onNewCaptionsDataAddedStateFlow: StateFlow<CaptionsRttEntryModel?> = onNewCaptionsDataAddedMutableStateFlow
|
||||
val softwareKeyboardStateFlow: StateFlow<Boolean>
|
||||
get() = softwareKeyboardStateMutableFlow
|
||||
|
||||
|
@ -50,38 +44,13 @@ internal class CaptionsViewModel(
|
|||
}
|
||||
|
||||
fun init(
|
||||
coroutineScope: CoroutineScope,
|
||||
captionsState: CaptionsState,
|
||||
isVisible: Boolean,
|
||||
captionsDataManager: CaptionsDataManager,
|
||||
localParticipantIdentifier: CommunicationIdentifier?,
|
||||
avatarViewManager: AvatarViewManager,
|
||||
deviceConfigurationState: DeviceConfigurationState,
|
||||
) {
|
||||
isVisibleMutableFlow = MutableStateFlow(isVisible)
|
||||
captionsStartInProgressStateMutableFlow = MutableStateFlow(canShowCaptionsStartInProgressUI(captionsState))
|
||||
softwareKeyboardStateMutableFlow = MutableStateFlow(deviceConfigurationState.isSoftwareKeyboardVisible)
|
||||
|
||||
captionsData.addAll(
|
||||
captionsDataManager.captionsDataCache.map { it.into(avatarViewManager, localParticipantIdentifier) }
|
||||
)
|
||||
|
||||
captionsDataManager.resetFlows()
|
||||
|
||||
coroutineScope.launch {
|
||||
captionsDataManager.getOnLastCaptionsDataUpdatedStateFlow().collect { data ->
|
||||
data?.let {
|
||||
onLastCaptionsDataUpdatedMutableStateFlow.value = it.into(avatarViewManager, localParticipantIdentifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineScope.launch {
|
||||
captionsDataManager.getOnNewCaptionsDataAddedStateFlow().collect { data ->
|
||||
data?.let {
|
||||
onNewCaptionsDataAddedMutableStateFlow.value = it.into(avatarViewManager, localParticipantIdentifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canShowCaptionsStartInProgressUI(
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.azure.android.communication.ui.calling.presentation.fragment.calling.
|
|||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.participantlist.ParticipantListViewModel
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.common.audiodevicelist.AudioDeviceListViewModel
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.CapabilitiesManager
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.CaptionsDataManager
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.DebugInfoManager
|
||||
import com.azure.android.communication.ui.calling.presentation.manager.UpdatableOptionsManager
|
||||
import com.azure.android.communication.ui.calling.redux.Store
|
||||
|
@ -45,6 +46,7 @@ internal class CallingViewModelFactory(
|
|||
private val debugInfoManager: DebugInfoManager,
|
||||
private val capabilitiesManager: CapabilitiesManager,
|
||||
private val updatableOptionsManager: UpdatableOptionsManager,
|
||||
private val captionsDataManager: CaptionsDataManager,
|
||||
private val showSupportFormOption: Boolean = false,
|
||||
private val enableMultitasking: Boolean,
|
||||
private val isTelecomManagerEnabled: Boolean = false,
|
||||
|
@ -164,5 +166,10 @@ internal class CallingViewModelFactory(
|
|||
)
|
||||
}
|
||||
val captionsLanguageSelectionListViewModel by lazy { CaptionsLanguageSelectionListViewModel(store::dispatch) }
|
||||
val captionsViewModel by lazy { CaptionsViewModel(store::dispatch) }
|
||||
val captionsViewModel by lazy {
|
||||
CaptionsViewModel(
|
||||
store::dispatch,
|
||||
captionsDataManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
|
||||
package com.azure.android.communication.ui.calling.presentation.manager
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.azure.android.communication.ui.calling.models.CallCompositeCaptionsData
|
||||
import com.azure.android.communication.ui.calling.models.CaptionsResultType
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.CallingFragment
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.captions.CaptionsRecord
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.captions.CaptionsRttRecord
|
||||
import com.azure.android.communication.ui.calling.presentation.fragment.calling.captions.CaptionsRttType
|
||||
import com.azure.android.communication.ui.calling.redux.AppStore
|
||||
import com.azure.android.communication.ui.calling.redux.state.ReduxState
|
||||
import com.azure.android.communication.ui.calling.service.CallingService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
@ -22,23 +24,19 @@ import java.time.Instant
|
|||
|
||||
internal class CaptionsDataManager(
|
||||
private val callingService: CallingService,
|
||||
private val appStore: AppStore<ReduxState>
|
||||
private val appStore: AppStore<ReduxState>,
|
||||
private val avatarViewManager: AvatarViewManager,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
private val captionsNewDataStateFlow = MutableStateFlow<CaptionsRecord?>(null)
|
||||
private val captionsLastDataUpdatedStateFlow = MutableStateFlow<CaptionsRecord?>(null)
|
||||
private val captionsAndRttMutableList = mutableListOf<CaptionsRttRecord>()
|
||||
private val recordUpdatedAtPositionMutableSharedFlow = MutableSharedFlow<Int>()
|
||||
private val recordInsertedAtPositionMutableSharedFlow = MutableSharedFlow<Int>()
|
||||
private val recordRemovedAtPositionMutableSharedFlow = MutableSharedFlow<Int>()
|
||||
|
||||
fun getOnNewCaptionsDataAddedStateFlow() = captionsNewDataStateFlow
|
||||
|
||||
fun getOnLastCaptionsDataUpdatedStateFlow() = captionsLastDataUpdatedStateFlow
|
||||
|
||||
// cache to get last captions on screen rotation
|
||||
val captionsDataCache = mutableListOf<CaptionsRecord>()
|
||||
|
||||
fun resetFlows() {
|
||||
captionsNewDataStateFlow.value = null
|
||||
captionsLastDataUpdatedStateFlow.value = null
|
||||
}
|
||||
val captionsAndRttData: List<CaptionsRttRecord> = captionsAndRttMutableList
|
||||
val recordUpdatedAtPositionSharedFlow: SharedFlow<Int> = recordUpdatedAtPositionMutableSharedFlow
|
||||
val recordInsertedAtPositionSharedFlow: SharedFlow<Int> = recordInsertedAtPositionMutableSharedFlow
|
||||
val recordRemovedAtPositionSharedFlow: SharedFlow<Int> = recordRemovedAtPositionMutableSharedFlow
|
||||
|
||||
fun start(coroutineScope: CoroutineScope) {
|
||||
coroutineScope.launch {
|
||||
|
@ -46,21 +44,21 @@ internal class CaptionsDataManager(
|
|||
mutex.withLock {
|
||||
if (shouldSkipCaption(captionData)) return@collect
|
||||
|
||||
removeOverflownCaptionsFromCache()
|
||||
|
||||
val (captionText, languageCode) = getCaptionTextAndLanguage(captionData)
|
||||
|
||||
val captionsRecord = CaptionsRecord(
|
||||
captionData.speakerName,
|
||||
captionText,
|
||||
captionData.speakerRawId,
|
||||
languageCode,
|
||||
captionData.resultType == CaptionsResultType.FINAL,
|
||||
captionData.timestamp,
|
||||
CaptionsRttType.CAPTIONS
|
||||
val (customizedDisplayName, avatar) = getParticipantCustomizationsBitmap(captionData.speakerRawId)
|
||||
val record = CaptionsRttRecord(
|
||||
avatarBitmap = avatar,
|
||||
displayName = customizedDisplayName ?: captionData.speakerName,
|
||||
displayText = captionText,
|
||||
speakerRawId = captionData.speakerRawId,
|
||||
languageCode = languageCode,
|
||||
isFinal = captionData.resultType == CaptionsResultType.FINAL,
|
||||
timestamp = captionData.timestamp,
|
||||
type = CaptionsRttType.CAPTIONS
|
||||
)
|
||||
|
||||
handleCaptionData(captionsRecord)
|
||||
removeOverflownCaptionsFromCache()
|
||||
handleCaptionData(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,17 +66,19 @@ internal class CaptionsDataManager(
|
|||
coroutineScope.launch {
|
||||
callingService.getRttStateFlow().collect { rttRecord ->
|
||||
mutex.withLock {
|
||||
removeOverflownCaptionsFromCache()
|
||||
val captionsRecord = CaptionsRecord(
|
||||
rttRecord.senderName,
|
||||
rttRecord.message,
|
||||
rttRecord.senderUserRawId,
|
||||
null,
|
||||
rttRecord.isFinalized,
|
||||
rttRecord.localCreatedTime,
|
||||
CaptionsRttType.RTT
|
||||
val (customizedDisplayName, avatar) = getParticipantCustomizationsBitmap(rttRecord.senderUserRawId)
|
||||
val captionsRecord = CaptionsRttRecord(
|
||||
avatarBitmap = avatar,
|
||||
displayName = customizedDisplayName ?: rttRecord.senderName,
|
||||
displayText = rttRecord.message,
|
||||
speakerRawId = rttRecord.senderUserRawId,
|
||||
languageCode = null,
|
||||
isFinal = rttRecord.isFinalized,
|
||||
timestamp = rttRecord.localCreatedTime,
|
||||
type = CaptionsRttType.RTT
|
||||
)
|
||||
|
||||
removeOverflownCaptionsFromCache()
|
||||
handleRttData(captionsRecord)
|
||||
}
|
||||
}
|
||||
|
@ -91,9 +91,10 @@ internal class CaptionsDataManager(
|
|||
return !activeCaptionLanguage.isNullOrEmpty() && captionData.captionLanguage.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun removeOverflownCaptionsFromCache() {
|
||||
if (captionsDataCache.size >= CallingFragment.MAX_CAPTIONS_DATA_SIZE) {
|
||||
captionsDataCache.removeAt(0)
|
||||
private suspend fun removeOverflownCaptionsFromCache() {
|
||||
if (captionsAndRttMutableList.size >= CallingFragment.MAX_CAPTIONS_DATA_SIZE) {
|
||||
captionsAndRttMutableList.removeAt(0)
|
||||
recordRemovedAtPositionMutableSharedFlow.emit(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,8 +106,8 @@ internal class CaptionsDataManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleRttData(newCaptionsRecord: CaptionsRecord) {
|
||||
val lastCaptionFromSameUser: CaptionsRecord? = getLastCaptionFromUser(newCaptionsRecord.speakerRawId, CaptionsRttType.RTT)
|
||||
private suspend fun handleRttData(newCaptionsRecord: CaptionsRttRecord) {
|
||||
val lastCaptionFromSameUser = getLastCaptionFromUser(newCaptionsRecord.speakerRawId, CaptionsRttType.RTT)
|
||||
|
||||
if (lastCaptionFromSameUser?.isFinal == false) {
|
||||
updateLastCaption(lastCaptionFromSameUser, newCaptionsRecord)
|
||||
|
@ -115,8 +116,8 @@ internal class CaptionsDataManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCaptionData(newCaptionsRecord: CaptionsRecord) {
|
||||
var lastCaptionFromSameUser: CaptionsRecord? = getLastCaptionFromUser(newCaptionsRecord.speakerRawId, CaptionsRttType.CAPTIONS)
|
||||
private suspend fun handleCaptionData(newCaptionsRecord: CaptionsRttRecord) {
|
||||
var lastCaptionFromSameUser = getLastCaptionFromUser(newCaptionsRecord.speakerRawId, CaptionsRttType.CAPTIONS)
|
||||
|
||||
if (lastCaptionFromSameUser != null && shouldFinalizeLastCaption(lastCaptionFromSameUser, newCaptionsRecord)) {
|
||||
lastCaptionFromSameUser = finalizeLastCaption(lastCaptionFromSameUser)
|
||||
|
@ -129,31 +130,39 @@ internal class CaptionsDataManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getLastCaptionFromUser(speakerRawId: String, type: CaptionsRttType): CaptionsRecord? {
|
||||
return captionsDataCache.lastOrNull { it.type == type && it.speakerRawId == speakerRawId }
|
||||
private fun getLastCaptionFromUser(speakerRawId: String, type: CaptionsRttType): CaptionsRttRecord? {
|
||||
return captionsAndRttMutableList.lastOrNull { it.type == type && it.speakerRawId == speakerRawId }
|
||||
}
|
||||
|
||||
private fun shouldFinalizeLastCaption(lastCaption: CaptionsRecord, newCaptionsRecord: CaptionsRecord): Boolean {
|
||||
val duration = Duration.between(Instant.ofEpochMilli(lastCaption.timestamp.time), Instant.ofEpochMilli(newCaptionsRecord.timestamp.time))
|
||||
private fun shouldFinalizeLastCaption(lastCaption: CaptionsRttRecord, newCaptionsRecord: CaptionsRttRecord): Boolean {
|
||||
val duration = Duration.between(
|
||||
Instant.ofEpochMilli(lastCaption.timestamp.time),
|
||||
Instant.ofEpochMilli(newCaptionsRecord.timestamp.time)
|
||||
)
|
||||
return duration.toMillis() > CallingFragment.MAX_CAPTIONS_PARTIAL_DATA_TIME_LIMIT
|
||||
}
|
||||
|
||||
private fun addNewCaption(data: CaptionsRecord) {
|
||||
captionsNewDataStateFlow.value = data
|
||||
captionsDataCache.add(data)
|
||||
private suspend fun addNewCaption(data: CaptionsRttRecord) {
|
||||
captionsAndRttMutableList.add(data)
|
||||
recordInsertedAtPositionMutableSharedFlow.emit(captionsAndRttMutableList.size - 1)
|
||||
}
|
||||
|
||||
private fun updateLastCaption(lastCaptionFromSameUser: CaptionsRecord, captionsRecord: CaptionsRecord) {
|
||||
val lastCaptionIndex = captionsDataCache.indexOf(lastCaptionFromSameUser)
|
||||
captionsDataCache[lastCaptionIndex] = captionsRecord
|
||||
captionsLastDataUpdatedStateFlow.value = captionsRecord
|
||||
private suspend fun updateLastCaption(lastCaptionFromSameUser: CaptionsRttRecord, captionsRecord: CaptionsRttRecord) {
|
||||
val lastCaptionIndex = captionsAndRttMutableList.indexOf(lastCaptionFromSameUser)
|
||||
captionsAndRttMutableList[lastCaptionIndex] = captionsRecord
|
||||
recordUpdatedAtPositionMutableSharedFlow.emit(lastCaptionIndex)
|
||||
}
|
||||
|
||||
private fun finalizeLastCaption(captionsRecord: CaptionsRecord): CaptionsRecord {
|
||||
val captionIndex = captionsDataCache.indexOf(captionsRecord)
|
||||
private suspend fun finalizeLastCaption(captionsRecord: CaptionsRttRecord): CaptionsRttRecord {
|
||||
val captionIndex = captionsAndRttMutableList.indexOf(captionsRecord)
|
||||
val finalizedCaptionsRecord = captionsRecord.copy(isFinal = true)
|
||||
captionsDataCache[captionIndex] = finalizedCaptionsRecord
|
||||
captionsLastDataUpdatedStateFlow.value = captionsRecord
|
||||
captionsAndRttMutableList[captionIndex] = finalizedCaptionsRecord
|
||||
recordUpdatedAtPositionMutableSharedFlow.emit(captionIndex)
|
||||
return finalizedCaptionsRecord
|
||||
}
|
||||
|
||||
private fun getParticipantCustomizationsBitmap(speakerRawId: String): Pair<String?, Bitmap?> {
|
||||
val remoteParticipantViewData = avatarViewManager.getRemoteParticipantViewData(speakerRawId)
|
||||
return Pair(remoteParticipantViewData?.displayName, remoteParticipantViewData?.avatarBitmap)
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче