Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Calling video not streamed when enabling camera on preview screen (WPB-7114) #2801

Merged
merged 11 commits into from
Mar 25, 2024
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
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 @@

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()

Check warning on line 132 in app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt#L132

Added line #L132 was not covered by tests
}
}
}
Expand Down Expand Up @@ -279,14 +280,18 @@
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 @@
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()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,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
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@
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 @@
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 @@
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()

Check warning on line 89 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt#L89

Added line #L89 was not covered by tests
} 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion kalium
Loading