improve/fix contents for picture in picture mode

depending on amount of participants, voiceOnly call and enabled/disabled own video, the contents of PIP windows are updated.

This will be further improved when speaker-view is implemented.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-05-13 11:27:55 +02:00
parent eaed93087b
commit 6a048fde08
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
4 changed files with 70 additions and 58 deletions

View File

@ -938,35 +938,17 @@ class CallActivity : CallBaseActivity() {
binding!!.pipSelfVideoRenderer.release() binding!!.pipSelfVideoRenderer.release()
} }
private fun initSelfVideoViewForPipMode() {
if (!isVoiceOnlyCall) {
binding!!.pipSelfVideoRenderer.visibility = View.VISIBLE
try {
binding!!.pipSelfVideoRenderer.init(rootEglBase!!.eglBaseContext, null)
} catch (e: IllegalStateException) {
Log.d(TAG, "pipGroupVideoRenderer already initialized", e)
}
binding!!.pipSelfVideoRenderer.setZOrderMediaOverlay(true)
// disabled because it causes some devices to crash
binding!!.pipSelfVideoRenderer.setEnableHardwareScaler(false)
binding!!.pipSelfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
localVideoTrack?.addSink(binding?.pipSelfVideoRenderer)
} else {
binding!!.pipSelfVideoRenderer.visibility = View.GONE
}
}
private fun initGrid() { private fun initGrid() {
Log.d(TAG, "initGrid") Log.d(TAG, "initGrid")
binding!!.composeParticipantGrid.visibility = View.VISIBLE
binding!!.composeParticipantGrid.setContent { binding!!.composeParticipantGrid.setContent {
MaterialTheme { MaterialTheme {
val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value } val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value }
ParticipantGrid( ParticipantGrid(
participantUiStates = participantUiStates, participantUiStates = participantUiStates,
eglBase = rootEglBase!!, eglBase = rootEglBase!!,
isVoiceOnlyCall = isVoiceOnlyCall isVoiceOnlyCall = isVoiceOnlyCall,
isInPipMode = isInPipMode
) { ) {
animateCallControls(true, 0) animateCallControls(true, 0)
} }
@ -3206,33 +3188,38 @@ class CallActivity : CallBaseActivity() {
override fun updateUiForPipMode() { override fun updateUiForPipMode() {
Log.d(TAG, "updateUiForPipMode") Log.d(TAG, "updateUiForPipMode")
val params = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.setMargins(0, 0, 0, 0)
binding!!.composeParticipantGrid.layoutParams = params
binding!!.callControls.visibility = View.GONE binding!!.callControls.visibility = View.GONE
binding!!.callInfosLinearLayout.visibility = View.GONE binding!!.callInfosLinearLayout.visibility = View.GONE
binding!!.selfVideoViewWrapper.visibility = View.GONE binding!!.selfVideoViewWrapper.visibility = View.GONE
binding!!.callStates.callStateRelativeLayout.visibility = View.GONE binding!!.callStates.callStateRelativeLayout.visibility = View.GONE
binding!!.pipCallConversationNameTextView.text = conversationName binding!!.pipCallConversationNameTextView.text = conversationName
if (isVoiceOnlyCall) { binding!!.selfVideoRenderer.clearImage()
if (participantItems.size > 1) { binding!!.selfVideoRenderer.release()
if (participantItems.size == 1) {
binding!!.pipOverlay.visibility = View.GONE
} else {
binding!!.composeParticipantGrid.visibility = View.GONE
if (localVideoTrack?.enabled() == true) {
binding!!.pipOverlay.visibility = View.VISIBLE
binding!!.pipSelfVideoRenderer.visibility = View.VISIBLE
try {
binding!!.pipSelfVideoRenderer.init(rootEglBase!!.eglBaseContext, null)
} catch (e: IllegalStateException) {
Log.d(TAG, "pipGroupVideoRenderer already initialized", e)
}
binding!!.pipSelfVideoRenderer.setZOrderMediaOverlay(true)
// disabled because it causes some devices to crash
binding!!.pipSelfVideoRenderer.setEnableHardwareScaler(false)
binding!!.pipSelfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
localVideoTrack?.addSink(binding?.pipSelfVideoRenderer)
} else {
binding!!.pipOverlay.visibility = View.VISIBLE binding!!.pipOverlay.visibility = View.VISIBLE
binding!!.pipSelfVideoRenderer.visibility = View.GONE binding!!.pipSelfVideoRenderer.visibility = View.GONE
} else {
binding!!.pipOverlay.visibility = View.GONE
}
} else {
binding!!.selfVideoRenderer.clearImage()
binding!!.selfVideoRenderer.release()
if (participantItems.size > 1) {
binding!!.pipOverlay.visibility = View.VISIBLE
initSelfVideoViewForPipMode()
} else {
binding!!.pipOverlay.visibility = View.GONE
} }
} }
} }
@ -3240,6 +3227,7 @@ class CallActivity : CallBaseActivity() {
override fun updateUiForNormalMode() { override fun updateUiForNormalMode() {
Log.d(TAG, "updateUiForNormalMode") Log.d(TAG, "updateUiForNormalMode")
binding!!.pipOverlay.visibility = View.GONE binding!!.pipOverlay.visibility = View.GONE
binding!!.composeParticipantGrid.visibility = View.VISIBLE
if (isVoiceOnlyCall) { if (isVoiceOnlyCall) {
binding!!.callControls.visibility = View.VISIBLE binding!!.callControls.visibility = View.VISIBLE

View File

@ -10,7 +10,6 @@ package com.nextcloud.talk.call.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -19,7 +18,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.nextcloud.talk.adapters.ParticipantUiState import com.nextcloud.talk.adapters.ParticipantUiState
@ -34,7 +32,6 @@ fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Mod
Box( Box(
modifier = modifier modifier = modifier
.size(150.dp)
.clip(CircleShape), .clip(CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {

View File

@ -29,12 +29,14 @@ import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase import org.webrtc.EglBase
import kotlin.math.ceil import kotlin.math.ceil
@Suppress("LongParameterList")
@Composable @Composable
fun ParticipantGrid( fun ParticipantGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
eglBase: EglBase?, eglBase: EglBase?,
participantUiStates: List<ParticipantUiState>, participantUiStates: List<ParticipantUiState>,
isVoiceOnlyCall: Boolean, isVoiceOnlyCall: Boolean,
isInPipMode: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
@ -58,7 +60,7 @@ fun ParticipantGrid(
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt() val rows = ceil(participantUiStates.size / columns.toFloat()).toInt()
val heightForNonGridComponents = if (isVoiceOnlyCall) { val heightForNonGridComponents = if (isVoiceOnlyCall && !isInPipMode) {
// this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls // this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls
// Once everything is migrated to jetpack, this workaround should be obsolete or solved in a better way // Once everything is migrated to jetpack, this workaround should be obsolete or solved in a better way
240.dp 240.dp
@ -97,6 +99,7 @@ fun ParticipantGrid(
.height(itemHeight) .height(itemHeight)
.fillMaxWidth(), .fillMaxWidth(),
eglBase = eglBase, eglBase = eglBase,
isInPipMode = isInPipMode,
isVoiceOnlyCall = isVoiceOnlyCall isVoiceOnlyCall = isVoiceOnlyCall
) )
} }
@ -109,7 +112,8 @@ fun ParticipantGridPreview() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -119,7 +123,8 @@ fun TwoParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -129,7 +134,8 @@ fun ThreeParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -139,7 +145,8 @@ fun FourParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -149,7 +156,8 @@ fun FiveParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(5), participantUiStates = getTestParticipants(5),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -159,7 +167,8 @@ fun SevenParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -169,7 +178,8 @@ fun FiftyParticipants() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -183,7 +193,8 @@ fun OneParticipantLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -197,7 +208,8 @@ fun TwoParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -211,7 +223,8 @@ fun ThreeParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -225,7 +238,8 @@ fun FourParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -239,7 +253,8 @@ fun SevenParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }
@ -253,7 +268,8 @@ fun FiftyParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participantUiStates = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false,
isInPipMode = false
) {} ) {}
} }

View File

@ -36,10 +36,12 @@ import org.webrtc.EglBase
const val NICK_OFFSET = 4f const val NICK_OFFSET = 4f
const val NICK_BLUR_RADIUS = 4f const val NICK_BLUR_RADIUS = 4f
@Suppress("Detekt.LongMethod")
@Composable @Composable
fun ParticipantTile( fun ParticipantTile(
participantUiState: ParticipantUiState, participantUiState: ParticipantUiState,
eglBase: EglBase?, eglBase: EglBase?,
isInPipMode: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isVoiceOnlyCall: Boolean isVoiceOnlyCall: Boolean
) { ) {
@ -53,9 +55,17 @@ fun ParticipantTile(
if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) { if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) {
WebRTCVideoView(participantUiState, eglBase) WebRTCVideoView(participantUiState, eglBase)
} else { } else {
val avatarSize = if (isInPipMode) {
100.dp
} else {
150.dp
}
AvatarWithFallback( AvatarWithFallback(
participant = participantUiState, participant = participantUiState,
modifier = Modifier.align(Alignment.Center) modifier = Modifier
.align(Alignment.Center)
.size(avatarSize)
) )
} }
@ -125,6 +135,7 @@ fun ParticipantTilePreview() {
.fillMaxWidth() .fillMaxWidth()
.height(300.dp), .height(300.dp),
eglBase = null, eglBase = null,
isInPipMode = false,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) )
} }