Skip to content

Commit

Permalink
fix: Calling video not streamed when enabling camera on preview scree…
Browse files Browse the repository at this point in the history
…n (WPB-7114) - cherrypick (#2808)
  • Loading branch information
ohassine committed Mar 25, 2024
1 parent 4dc00e6 commit 6833d5f
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 59 deletions.
Expand Up @@ -37,7 +37,8 @@ import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase
import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase
import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase
import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
Expand Down Expand Up @@ -167,6 +168,13 @@ class CallsModule {
): UpdateVideoStateUseCase =
callsScope.updateVideoState

@ViewModelScoped
@Provides
fun provideSetVideoSendStateUseCase(
callsScope: CallsScope
): SetVideoSendStateUseCase =
callsScope.setVideoSendState

@ViewModelScoped
@Provides
fun provideIsCallRunningUseCase(callsScope: CallsScope) =
Expand Down
Expand Up @@ -52,7 +52,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase
import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase
import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
import com.wire.kalium.logic.util.PlatformView
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -127,8 +127,9 @@ class SharedCallingViewModel @Inject constructor(

private suspend fun observeScreenState() {
currentScreenManager.observeCurrentScreen(viewModelScope).collect {
if (it == CurrentScreen.InBackground) {
stopVideo()
// clear video preview when the screen is in background to avoid memory leaks
if (it == CurrentScreen.InBackground && callState.isCameraOn) {
clearVideoPreview()
}
}
}
Expand Down Expand Up @@ -279,14 +280,18 @@ class SharedCallingViewModel @Inject constructor(
callState = callState.copy(
isCameraOn = !callState.isCameraOn
)
if (callState.isCameraOn) {
updateVideoState(conversationId, VideoState.STARTED)
} else {
updateVideoState(conversationId, VideoState.STOPPED)
}
}
}

fun clearVideoPreview() {
viewModelScope.launch {
appLogger.i("SharedCallingViewModel: clearing video preview..")
setVideoPreview(conversationId, PlatformView(null))
updateVideoState(conversationId, VideoState.STOPPED)
}
}

Expand All @@ -295,18 +300,6 @@ class SharedCallingViewModel @Inject constructor(
appLogger.i("SharedCallingViewModel: setting video preview..")
setVideoPreview(conversationId, PlatformView(null))
setVideoPreview(conversationId, PlatformView(view))
updateVideoState(conversationId, VideoState.STARTED)
}
}

fun stopVideo() {
viewModelScope.launch {
if (callState.isCameraOn) {
appLogger.i("SharedCallingViewModel: stopping video..")
callState = callState.copy(isCameraOn = false, isSpeakerOn = false)
clearVideoPreview()
turnLoudSpeakerOff()
}
}
}
}
Expand Up @@ -122,8 +122,14 @@ fun OngoingCallScreen(
hangUpCall = { sharedCallingViewModel.hangUpCall(navigator::navigateBack) },
toggleVideo = sharedCallingViewModel::toggleVideo,
flipCamera = sharedCallingViewModel::flipCamera,
setVideoPreview = sharedCallingViewModel::setVideoPreview,
clearVideoPreview = sharedCallingViewModel::clearVideoPreview,
setVideoPreview = {
sharedCallingViewModel.setVideoPreview(it)
ongoingCallViewModel.startSendingVideoFeed()
},
clearVideoPreview = {
sharedCallingViewModel.clearVideoPreview()
ongoingCallViewModel.stopSendingVideoFeed()
},
navigateBack = navigator::navigateBack,
requestVideoStreams = ongoingCallViewModel::requestVideoStreams,
hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast
Expand Down
Expand Up @@ -33,11 +33,14 @@ import com.wire.android.ui.calling.model.UICallParticipant
import com.wire.android.ui.navArgs
import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallClient
import com.wire.kalium.logic.data.call.VideoState
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase
import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
Expand All @@ -54,7 +57,8 @@ class OngoingCallViewModel @Inject constructor(
private val globalDataStore: GlobalDataStore,
private val establishedCalls: ObserveEstablishedCallsUseCase,
private val requestVideoStreams: RequestVideoStreamsUseCase,
private val currentScreenManager: CurrentScreenManager,
private val setVideoSendState: SetVideoSendStateUseCase,
private val currentScreenManager: CurrentScreenManager
) : ViewModel() {

private val ongoingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs()
Expand All @@ -70,13 +74,36 @@ class OngoingCallViewModel @Inject constructor(
init {
viewModelScope.launch {
establishedCalls().first { it.isNotEmpty() }.run {
initCameraState(this)
// We start observing once we have an ongoing call
observeCurrentCall()
}
}
showDoubleTapToast()
}

private fun initCameraState(calls: List<Call>) {
val currentCall = calls.find { call -> call.conversationId == conversationId }
currentCall?.let {
if (it.isCameraOn) {
startSendingVideoFeed()
} else {
stopSendingVideoFeed()
}
}
}

fun startSendingVideoFeed() {
viewModelScope.launch {
setVideoSendState(conversationId, VideoState.STARTED)
}
}
fun stopSendingVideoFeed() {
viewModelScope.launch {
setVideoSendState(conversationId, VideoState.STOPPED)
}
}

private suspend fun observeCurrentCall() {
establishedCalls()
.distinctUntilChanged()
Expand Down
Expand Up @@ -31,12 +31,14 @@ import com.wire.android.util.CurrentScreenManager
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallClient
import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.call.VideoState
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase
import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
Expand Down Expand Up @@ -68,6 +70,9 @@ class OngoingCallViewModelTest {
@MockK
private lateinit var currentScreenManager: CurrentScreenManager

@MockK
private lateinit var setVideoSendState: SetVideoSendStateUseCase

@MockK
private lateinit var globalDataStore: GlobalDataStore

Expand All @@ -80,17 +85,33 @@ class OngoingCallViewModelTest {
coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall()))
coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther)
coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false
coEvery { setVideoSendState.invoke(any(), any()) } returns Unit

ongoingCallViewModel = OngoingCallViewModel(
savedStateHandle = savedStateHandle,
establishedCalls = establishedCall,
requestVideoStreams = requestVideoStreams,
currentScreenManager = currentScreenManager,
currentUserId = currentUserId,
setVideoSendState = setVideoSendState,
globalDataStore = globalDataStore,
)
}

@Test
fun givenAnOngoingCall_WhenTurningOnCamera_ThenSetVideoSendStateToStarted() = runTest {
ongoingCallViewModel.startSendingVideoFeed()

coVerify(exactly = 1) { setVideoSendState.invoke(any(), VideoState.STARTED) }
}

@Test
fun givenAnOngoingCall_WhenTurningOffCamera_ThenSetVideoSendStateToStopped() = runTest {
ongoingCallViewModel.stopSendingVideoFeed()

coVerify { setVideoSendState.invoke(any(), VideoState.STOPPED) }
}

@Test
fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = runTest {
val expectedClients = listOf(
Expand Down
Expand Up @@ -42,7 +42,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase
import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase
import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase
import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
Expand Down Expand Up @@ -261,6 +261,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo false
coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) }
}

@Test
Expand All @@ -272,6 +273,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo true
coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) }
}

@Test
Expand Down Expand Up @@ -315,57 +317,24 @@ class SharedCallingViewModelTest {
}

@Test
fun `given an active call, when setVideoPreview is called, then set the video preview and update video state to STARTED`() =
fun `given a call, when setVideoPreview is called, then set the video preview`() =
runTest {
coEvery { setVideoPreview(any(), any()) } returns Unit
coEvery { updateVideoState(any(), any()) } returns Unit

sharedCallingViewModel.setVideoPreview(view)
advanceUntilIdle()

coVerify(exactly = 2) { setVideoPreview(any(), any()) }
coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) }
}

@Test
fun `given an active call, when clearVideoPreview is called, then update video state to STOPPED`() =
runTest {
coEvery { setVideoPreview(any(), any()) } returns Unit
coEvery { updateVideoState(any(), any()) } returns Unit

sharedCallingViewModel.clearVideoPreview()
advanceUntilIdle()

coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) }
}

@Test
fun `given a video call, when stopping video, then clear Video Preview and turn off speaker`() =
runTest {
sharedCallingViewModel.callState =
sharedCallingViewModel.callState.copy(isCameraOn = true)
coEvery { setVideoPreview(any(), any()) } returns Unit
coEvery { updateVideoState(any(), any()) } returns Unit
coEvery { turnLoudSpeakerOff() } returns Unit

sharedCallingViewModel.stopVideo()
advanceUntilIdle()

coVerify(exactly = 1) { setVideoPreview(any(), any()) }
coVerify(exactly = 1) { turnLoudSpeakerOff() }
}

@Test
fun `given an audio call, when stopVideo is invoked, then do not do anything`() = runTest {
sharedCallingViewModel.callState = sharedCallingViewModel.callState.copy(isCameraOn = false)
fun `given a call, when clearVideoPreview is called, then clear view`() = runTest {
coEvery { setVideoPreview(any(), any()) } returns Unit
coEvery { turnLoudSpeakerOff() } returns Unit

sharedCallingViewModel.stopVideo()
sharedCallingViewModel.clearVideoPreview()
advanceUntilIdle()

coVerify(inverse = true) { setVideoPreview(any(), any()) }
coVerify(inverse = true) { turnLoudSpeakerOff() }
coVerify(exactly = 1) { setVideoPreview(any(), any()) }
}

companion object {
Expand Down

0 comments on commit 6833d5f

Please sign in to comment.