Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -258,7 +258,7 @@ class OngoingCallViewModel @AssistedInject constructor(
viewModelScope.launch {
participants
.filter {
(it.isCameraOn || it.isSharingScreen) && !state.othersVideosDisabled
it.isSharingScreen || (it.isCameraOn && !state.othersVideosDisabled)
}
.also {
val clients: List<CallClient> = it.map { uiParticipant ->
Expand Down Expand Up @@ -357,6 +357,6 @@ private suspend fun SharedFlow<Call?>.senderName(userId: QualifiedID) =
filterNotNull().filter { it.participants.isNotEmpty() }.first().senderName(userId)

private fun OngoingCallState.currentOrderType(): CallingParticipantsOrderType = when (othersVideosDisabled) {
true -> CallingParticipantsOrderType.ALPHABETICALLY
false -> CallingParticipantsOrderType.VIDEOS_FIRST
true -> CallingParticipantsOrderType.PRESENTERS_FIRST
false -> CallingParticipantsOrderType.ALL_MEDIA_FIRST
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -93,6 +94,7 @@ import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.data.id.QualifiedID

@Suppress("CyclomaticComplexMethod")
@Composable
fun ParticipantTile(
participantTitleState: UICallParticipant,
Expand Down Expand Up @@ -121,34 +123,36 @@ fun ParticipantTile(
val activeSpeakerBorderPadding = dimensions().spacing6x

Box(modifier = Modifier.fillMaxSize()) {
// Layer 1: Video/Camera background
if (participantTitleState.isSelfUser) {
CameraPreview(
isCameraOn = isSelfUserCameraOn,
shouldFill = shouldFillSelfUserCameraPreview,
onSelfUserVideoPreviewCreated = onSelfUserVideoPreviewCreated,
onClearSelfUserVideoPreview = onClearSelfUserVideoPreview
)
} else {
OthersVideoRenderer(
participantId = participantTitleState.id.toString(),
clientId = participantTitleState.clientId,
isCameraOn = participantTitleState.isCameraOn,
isSharingScreen = participantTitleState.isSharingScreen,
shouldFill = shouldFillOthersVideoPreview,
isZoomingEnabled = isZoomingEnabled,
othersVideosDisabled = othersVideosDisabled,
)
val shouldRenderVideo = when {
participantTitleState.isSelfUser -> isSelfUserCameraOn // for self user render video when self camera is on
participantTitleState.isSharingScreen -> true // if sharing screen, always render the shared screen
participantTitleState.isCameraOn -> !othersVideosDisabled // if camera is on, render video if others videos are enabled
else -> false // otherwise do not render video
}

// Layer 2: Avatar centered (only show when video is off)
val shouldShowAvatar = when {
participantTitleState.isSelfUser -> !isSelfUserCameraOn // for self user show avatar when self camera is off
othersVideosDisabled -> true // if others videos are disabled, show avatar because their videos will be off anyway
else -> !participantTitleState.isCameraOn && !participantTitleState.isSharingScreen // otherwise show avatar if no video
LaunchedEffect(participantTitleState.isSelfUser, isSelfUserCameraOn) {
if (participantTitleState.isSelfUser && !isSelfUserCameraOn) {
onClearSelfUserVideoPreview()
}
}

if (shouldShowAvatar) {
if (shouldRenderVideo) {
// Layer 1: Video/Camera background
if (participantTitleState.isSelfUser) {
CameraPreview(
shouldFill = shouldFillSelfUserCameraPreview,
onSelfUserVideoPreviewCreated = onSelfUserVideoPreviewCreated,
)
} else {
OthersVideoRenderer(
participantId = participantTitleState.id.toString(),
clientId = participantTitleState.clientId,
shouldFill = shouldFillOthersVideoPreview,
isZoomingEnabled = isZoomingEnabled,
)
}
} else {
// Layer 2: Avatar centered (only show when video is off)
AvatarTile(
modifier = Modifier
.alpha(if (participantTitleState.hasEstablishedAudio) 1f else 0.5f)
Expand Down Expand Up @@ -320,45 +324,31 @@ private fun CleanUpRendererIfNeeded(videoRenderer: VideoRenderer) {

@Composable
private fun CameraPreview(
isCameraOn: Boolean,
onSelfUserVideoPreviewCreated: (view: View) -> Unit,
shouldFill: Boolean = false,
onClearSelfUserVideoPreview: () -> Unit
) {
var isCameraStopped by remember { mutableStateOf(isCameraOn) }

if (isCameraOn) {
isCameraStopped = false
val context = LocalContext.current
val backgroundColor = darkColorsScheme().surfaceContainer.value.toInt()
val videoPreview = remember {
CameraPreviewBuilder(context)
.setBackgroundColor(backgroundColor)
.shouldFill(shouldFill)
.build()
}
AndroidView(
factory = {
onSelfUserVideoPreviewCreated(videoPreview)
videoPreview
}
)
} else {
if (isCameraStopped) return
isCameraStopped = true
onClearSelfUserVideoPreview()
val context = LocalContext.current
val backgroundColor = darkColorsScheme().surfaceContainer.value.toInt()
val videoPreview = remember {
CameraPreviewBuilder(context)
.setBackgroundColor(backgroundColor)
.shouldFill(shouldFill)
.build()
}
AndroidView(
factory = {
onSelfUserVideoPreviewCreated(videoPreview)
videoPreview
}
)
}

@Composable
private fun OthersVideoRenderer(
participantId: String,
clientId: String,
isCameraOn: Boolean,
isSharingScreen: Boolean,
shouldFill: Boolean,
isZoomingEnabled: Boolean,
othersVideosDisabled: Boolean,
) {
var size by remember { mutableStateOf(IntSize.Zero) }
var zoom by remember { mutableStateOf(1f) }
Expand All @@ -367,57 +357,55 @@ private fun OthersVideoRenderer(

val context = LocalContext.current
val rendererFillColor = (darkColorsScheme().surfaceContainer.value shr 32).toLong()
if ((isCameraOn || isSharingScreen) && !othersVideosDisabled) {

val videoRenderer = remember {
VideoRenderer(
context,
participantId,
clientId,
false
).apply {
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setFillColor(rendererFillColor)
setShouldFill(shouldFill)
}

val videoRenderer = remember {
VideoRenderer(
context,
participantId,
clientId,
false
).apply {
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
setFillColor(rendererFillColor)
setShouldFill(shouldFill)
}
}

CleanUpRendererIfNeeded(videoRenderer)
CleanUpRendererIfNeeded(videoRenderer)

AndroidView(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
size = it
}
.pointerInput(Unit) {
// enable zooming on full screen and when video is on
if (isZoomingEnabled) {
detectTransformGestures { _, gesturePan, gestureZoom, _ ->
zoom = (zoom * gestureZoom).coerceIn(1f, 3f)
val maxX = (size.width * (zoom - 1)) / 2
val minX = -maxX
offsetX = maxOf(minX, minOf(maxX, offsetX + gesturePan.x))
val maxY = (size.height * (zoom - 1)) / 2
val minY = -maxY
offsetY = maxOf(minY, minOf(maxY, offsetY + gesturePan.y))
}
AndroidView(
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
size = it
}
.pointerInput(Unit) {
// enable zooming on full screen and when video is on
if (isZoomingEnabled) {
detectTransformGestures { _, gesturePan, gestureZoom, _ ->
zoom = (zoom * gestureZoom).coerceIn(1f, 3f)
val maxX = (size.width * (zoom - 1)) / 2
val minX = -maxX
offsetX = maxOf(minX, minOf(maxX, offsetX + gesturePan.x))
val maxY = (size.height * (zoom - 1)) / 2
val minY = -maxY
offsetY = maxOf(minY, minOf(maxY, offsetY + gesturePan.y))
}
}
.graphicsLayer(
scaleX = zoom,
scaleY = zoom,
translationX = offsetX,
translationY = offsetY
),

factory = {
val frameLayout = FrameLayout(it)
frameLayout.addView(videoRenderer)
frameLayout
}
)
}
.graphicsLayer(
scaleX = zoom,
scaleY = zoom,
translationX = offsetX,
translationY = offsetY
),

factory = {
val frameLayout = FrameLayout(it)
frameLayout.addView(videoRenderer)
frameLayout
}
)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,39 +309,41 @@ class OngoingCallViewModelTest {
@Test
fun givenParticipantsList_WhenRequestingVideoStreamForAllParticipant_ThenRequestItInLowQuality() =
runTest {
val expectedClients = listOf(
CallClient(uiParticipant1.id.toString(), uiParticipant1.clientId, false, CallResolutionQuality.LOW),
CallClient(uiParticipant3.id.toString(), uiParticipant3.clientId, false, CallResolutionQuality.LOW)
)
val (arrangement, ongoingCallViewModel) = Arrangement()
.withLastActiveCall(provideCall().copy(participants = participants))
.arrange()

ongoingCallViewModel.setOthersVideosDisabled(true)
ongoingCallViewModel.onSelectedParticipant(null)
ongoingCallViewModel.requestVideoStreams(uiParticipants)

coVerify(exactly = 1) {
arrangement.requestVideoStreams(conversationId, emptyList())
arrangement.requestVideoStreams(
conversationId,
expectedClients
)
}
}

@Test
fun givenParticipantsListAndOthersVideosDisabled_WhenRequestingVideoStreamForAllParticipant_ThenRequestEmptyList() =
fun givenParticipantsListAndOthersVideosDisabled_WhenRequestingVideoStreamForAllParticipant_ThenRequestOnlyPresenters() =
runTest {
val expectedClients = listOf(
CallClient(uiParticipant1.id.toString(), uiParticipant1.clientId, false, CallResolutionQuality.LOW),
CallClient(uiParticipant3.id.toString(), uiParticipant3.clientId, false, CallResolutionQuality.LOW)
)

val (arrangement, ongoingCallViewModel) = Arrangement()
.withLastActiveCall(provideCall())
.withLastActiveCall(provideCall().copy(participants = participants))
.arrange()

ongoingCallViewModel.setOthersVideosDisabled(true)
ongoingCallViewModel.onSelectedParticipant(null)
ongoingCallViewModel.requestVideoStreams(uiParticipants)

coVerify(exactly = 1) {
arrangement.requestVideoStreams(
conversationId,
expectedClients
)
arrangement.requestVideoStreams(conversationId, expectedClients)
}
}

Expand Down Expand Up @@ -554,28 +556,28 @@ class OngoingCallViewModelTest {

@Test
fun givenCall_WhenDisablingOthersVideos_ThenParticipantsOrderIsUpdated() = runTest {
val participantsVideosFirst = listOf(participant3, participant1, participant2)
val participantsAlphabetically = listOf(participant1, participant2, participant3)
val allMediaFirst = listOf(participant3, participant2, participant1)
val presentersFirst = listOf(participant3, participant1, participant2)
val (arrangement, ongoingCallViewModel) = Arrangement()
.withLastActiveCall(provideCall().copy(participants = participantsVideosFirst), CallingParticipantsOrderType.VIDEOS_FIRST)
.withLastActiveCall(provideCall().copy(participants = participantsAlphabetically), CallingParticipantsOrderType.ALPHABETICALLY)
.withLastActiveCall(provideCall().copy(participants = allMediaFirst), CallingParticipantsOrderType.ALL_MEDIA_FIRST)
.withLastActiveCall(provideCall().copy(participants = presentersFirst), CallingParticipantsOrderType.PRESENTERS_FIRST)
.arrange()
advanceUntilIdle()

ongoingCallViewModel.setOthersVideosDisabled(false)
advanceUntilIdle()
val expectedVideosFirst = listOf(uiParticipant3, uiParticipant1, uiParticipant2)
assertEquals(expectedVideosFirst, ongoingCallViewModel.state.participants)
val expectedAllMediaFirst = listOf(uiParticipant3, uiParticipant2, uiParticipant1)
assertEquals(expectedAllMediaFirst, ongoingCallViewModel.state.participants)
coVerify(exactly = 1) {
arrangement.observeLastActiveCall(any(), eq(CallingParticipantsOrderType.VIDEOS_FIRST))
arrangement.observeLastActiveCall(any(), eq(CallingParticipantsOrderType.ALL_MEDIA_FIRST))
}

ongoingCallViewModel.setOthersVideosDisabled(true)
advanceUntilIdle()
val expectedAlphabetically = listOf(uiParticipant1, uiParticipant2, uiParticipant3)
assertEquals(expectedAlphabetically, ongoingCallViewModel.state.participants)
val expectedPresentersFirst = listOf(uiParticipant3, uiParticipant1, uiParticipant2)
assertEquals(expectedPresentersFirst, ongoingCallViewModel.state.participants)
coVerify(exactly = 1) {
arrangement.observeLastActiveCall(any(), eq(CallingParticipantsOrderType.ALPHABETICALLY))
arrangement.observeLastActiveCall(any(), eq(CallingParticipantsOrderType.PRESENTERS_FIRST))
}
}

Expand Down
Loading