Add visibility rules and timeout for call controls for v2.

This commit is contained in:
Alex Hart 2024-08-23 10:20:44 -03:00 committed by Nicholas Tinsley
parent 3f71f90234
commit 5f66e2eb15
5 changed files with 115 additions and 44 deletions

View file

@ -18,8 +18,10 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rxjava3.subscribeAsState import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
@ -36,11 +38,13 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel
import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
import org.thoughtcrime.securesms.components.webrtc.controls.RaiseHandSnackbar
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.VibrateUtil
import org.thoughtcrime.securesms.util.viewModel import org.thoughtcrime.securesms.util.viewModel
import org.whispersystems.signalservice.api.messages.calls.HangupMessage import org.whispersystems.signalservice.api.messages.calls.HangupMessage
@ -77,6 +81,8 @@ class CallActivity : BaseActivity(), CallControlsCallback {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val fullscreenHelper = FullscreenHelper(this)
lifecycleDisposable.bindTo(this) lifecycleDisposable.bindTo(this)
val compositeDisposable = CompositeDisposable() val compositeDisposable = CompositeDisposable()
lifecycleDisposable.add(compositeDisposable) lifecycleDisposable.add(compositeDisposable)
@ -120,10 +126,20 @@ class CallActivity : BaseActivity(), CallControlsCallback {
} }
} }
var areControlsVisible by remember { mutableStateOf(true) }
LaunchedEffect(areControlsVisible) {
if (areControlsVisible) {
fullscreenHelper.showSystemUI()
} else {
fullscreenHelper.hideSystemUI()
}
}
SignalTheme { SignalTheme {
Surface { Surface {
CallScreen( CallScreen(
callRecipient = recipient ?: Recipient.UNKNOWN, callRecipient = recipient,
webRtcCallState = callParticipantsState.callState, webRtcCallState = callParticipantsState.callState,
callScreenState = callScreenState, callScreenState = callScreenState,
callControlsState = callControlsState, callControlsState = callControlsState,
@ -146,8 +162,16 @@ class CallActivity : BaseActivity(), CallControlsCallback {
.alpha(it) .alpha(it)
) )
}, },
raiseHandSnackbar = {
RaiseHandSnackbar.View(
webRtcCallViewModel = webRtcCallViewModel,
showCallInfoListener = { /*TODO*/ },
modifier = it
)
},
onNavigationClick = { finish() }, onNavigationClick = { finish() },
onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked,
onControlsToggled = { areControlsVisible = it }
) )
} }
} }

View file

@ -11,12 +11,19 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -41,10 +48,20 @@ fun CallControls(
) { ) {
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val density = LocalDensity.current
val padBottom = with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }
var bottom by remember {
mutableStateOf(padBottom)
}
if (padBottom != 0.dp) {
bottom = padBottom
}
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = spacedBy(30.dp), verticalArrangement = spacedBy(30.dp),
modifier = modifier.navigationBarsPadding() modifier = modifier.padding(bottom = bottom)
) { ) {
Row( Row(
horizontalArrangement = spacedBy(20.dp) horizontalArrangement = spacedBy(20.dp)

View file

@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -19,12 +20,10 @@ import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -38,10 +37,12 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -50,11 +51,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.signal.core.ui.BottomSheets import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Previews import org.signal.core.ui.Previews
@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import kotlin.math.max import kotlin.math.max
import kotlin.math.round import kotlin.math.round
import kotlin.time.Duration.Companion.seconds
private const val DRAG_HANDLE_HEIGHT = 22 private const val DRAG_HANDLE_HEIGHT = 22
private const val SHEET_TOP_PADDING = 9 private const val SHEET_TOP_PADDING = 9
@ -86,19 +87,26 @@ fun CallScreen(
localParticipant: CallParticipant, localParticipant: CallParticipant,
localRenderState: WebRtcLocalRenderState, localRenderState: WebRtcLocalRenderState,
callInfoView: @Composable (Float) -> Unit, callInfoView: @Composable (Float) -> Unit,
raiseHandSnackbar: @Composable (Modifier) -> Unit,
onNavigationClick: () -> Unit, onNavigationClick: () -> Unit,
onLocalPictureInPictureClicked: () -> Unit onLocalPictureInPictureClicked: () -> Unit,
onControlsToggled: (Boolean) -> Unit
) { ) {
var peekPercentage by remember { var peekPercentage by remember {
mutableFloatStateOf(0f) mutableFloatStateOf(0f)
} }
val skipHiddenState by rememberUpdatedState(newValue = callControlsState.skipHiddenState)
val valueChangeOperation: (SheetValue) -> Boolean = remember {
{
!(it == SheetValue.Hidden && skipHiddenState)
}
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scaffoldState = rememberBottomSheetScaffoldState( val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState( bottomSheetState = rememberStandardBottomSheetState(
confirmValueChange = { confirmValueChange = valueChangeOperation,
!(it == SheetValue.Hidden && callControlsState.skipHiddenState)
},
skipHiddenState = false skipHiddenState = false
) )
) )
@ -169,7 +177,8 @@ fun CallScreen(
overflowParticipants = overflowParticipants, overflowParticipants = overflowParticipants,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
callControlsState = callControlsState, callControlsState = callControlsState,
onPipClick = onLocalPictureInPictureClicked onPipClick = onLocalPictureInPictureClicked,
onControlsToggled = onControlsToggled
) )
} }
@ -187,7 +196,8 @@ fun CallScreen(
overflowParticipants = overflowParticipants, overflowParticipants = overflowParticipants,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
callControlsState = callControlsState, callControlsState = callControlsState,
onPipClick = onLocalPictureInPictureClicked onPipClick = onLocalPictureInPictureClicked,
onControlsToggled = onControlsToggled
) )
} }
} }
@ -203,13 +213,19 @@ fun CallScreen(
} }
if (webRtcCallState.isPassedPreJoin) { if (webRtcCallState.isPassedPreJoin) {
CallScreenTopBar( AnimatedVisibility(
callRecipient = callRecipient, visible = scaffoldState.bottomSheetState.targetValue != SheetValue.Hidden,
callStatus = callScreenState.callStatus, enter = fadeIn(),
onNavigationClick = onNavigationClick, exit = fadeOut()
onCallInfoClick = onCallInfoClick, ) {
modifier = Modifier.padding(bottom = padding) CallScreenTopBar(
) callRecipient = callRecipient,
callStatus = callScreenState.callStatus,
onNavigationClick = onNavigationClick,
onCallInfoClick = onCallInfoClick,
modifier = Modifier.padding(bottom = padding)
)
}
} else { } else {
CallScreenPreJoinOverlay( CallScreenPreJoinOverlay(
callRecipient = callRecipient, callRecipient = callRecipient,
@ -221,6 +237,8 @@ fun CallScreen(
) )
} }
raiseHandSnackbar(Modifier.fillMaxWidth())
AnimatedCallStateUpdate( AnimatedCallStateUpdate(
callControlsChange = callScreenState.callControlsChange, callControlsChange = callScreenState.callControlsChange,
modifier = Modifier modifier = Modifier
@ -246,7 +264,8 @@ private fun BoxScope.Viewport(
overflowParticipants: List<CallParticipant>, overflowParticipants: List<CallParticipant>,
scaffoldState: BottomSheetScaffoldState, scaffoldState: BottomSheetScaffoldState,
callControlsState: CallControlsState, callControlsState: CallControlsState,
onPipClick: () -> Unit onPipClick: () -> Unit,
onControlsToggled: (Boolean) -> Unit
) { ) {
if (webRtcCallState.isPreJoinOrNetworkUnavailable) { if (webRtcCallState.isPreJoinOrNetworkUnavailable) {
LargeLocalVideoRenderer( LargeLocalVideoRenderer(
@ -260,6 +279,15 @@ private fun BoxScope.Viewport(
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState)
LaunchedEffect(hideSheet) {
if (hideSheet) {
delay(5.seconds)
scaffoldState.bottomSheetState.hide()
onControlsToggled(false)
}
}
Row { Row {
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -275,7 +303,9 @@ private fun BoxScope.Viewport(
scope.launch { scope.launch {
if (scaffoldState.bottomSheetState.isVisible) { if (scaffoldState.bottomSheetState.isVisible) {
scaffoldState.bottomSheetState.hide() scaffoldState.bottomSheetState.hide()
onControlsToggled(false)
} else { } else {
onControlsToggled(true)
scaffoldState.bottomSheetState.show() scaffoldState.bottomSheetState.show()
} }
} }
@ -317,17 +347,9 @@ private fun BoxScope.Viewport(
} }
if (webRtcCallState.inOngoingCall && localParticipant.isVideoEnabled && !isLargeGroupCall) { if (webRtcCallState.inOngoingCall && localParticipant.isVideoEnabled && !isLargeGroupCall) {
val padBottom: Dp = if (scaffoldState.bottomSheetState.isVisible) {
0.dp
} else {
val density = LocalDensity.current
with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }
}
SmallMoveableLocalVideoRenderer( SmallMoveableLocalVideoRenderer(
localParticipant = localParticipant, localParticipant = localParticipant,
localRenderState = localRenderState, localRenderState = localRenderState,
extraPadBottom = padBottom,
onClick = onPipClick onClick = onPipClick
) )
} }
@ -365,12 +387,14 @@ private fun TinyLocalVideoRenderer(
val smallSize = remember(isPortrait) { val smallSize = remember(isPortrait) {
if (isPortrait) DpSize(40.dp, 72.dp) else DpSize(72.dp, 40.dp) if (isPortrait) DpSize(40.dp, 72.dp) else DpSize(72.dp, 40.dp)
} }
val largeSize = remember(isPortrait) { val expandedSize = remember(isPortrait) {
if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp) if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp)
} }
val width by animateDpAsState(label = "tiny-width", targetValue = if (localRenderState == WebRtcLocalRenderState.EXPANDED) largeSize.width else smallSize.width) val size = if (localRenderState == WebRtcLocalRenderState.EXPANDED) expandedSize else smallSize
val height by animateDpAsState(label = "tiny-height", targetValue = if (localRenderState == WebRtcLocalRenderState.EXPANDED) largeSize.height else smallSize.height)
val width by animateDpAsState(label = "tiny-width", targetValue = size.width)
val height by animateDpAsState(label = "tiny-height", targetValue = size.height)
LocalParticipantRenderer( LocalParticipantRenderer(
localParticipant = localParticipant, localParticipant = localParticipant,
@ -391,7 +415,6 @@ private fun TinyLocalVideoRenderer(
private fun SmallMoveableLocalVideoRenderer( private fun SmallMoveableLocalVideoRenderer(
localParticipant: CallParticipant, localParticipant: CallParticipant,
localRenderState: WebRtcLocalRenderState, localRenderState: WebRtcLocalRenderState,
extraPadBottom: Dp,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
@ -399,15 +422,14 @@ private fun SmallMoveableLocalVideoRenderer(
val smallSize = remember(isPortrait) { val smallSize = remember(isPortrait) {
if (isPortrait) DpSize(90.dp, 160.dp) else DpSize(160.dp, 90.dp) if (isPortrait) DpSize(90.dp, 160.dp) else DpSize(160.dp, 90.dp)
} }
val largeSize = remember(isPortrait) { val expandedSize = remember(isPortrait) {
if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp) if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp)
} }
val size = if (localRenderState == WebRtcLocalRenderState.SMALL_RECTANGLE) smallSize else largeSize val size = if (localRenderState == WebRtcLocalRenderState.EXPANDED) expandedSize else smallSize
val targetWidth by animateDpAsState(label = "animate-pip-width", targetValue = size.width, animationSpec = tween()) val targetWidth by animateDpAsState(label = "animate-pip-width", targetValue = size.width, animationSpec = tween())
val targetHeight by animateDpAsState(label = "animate-pip-height", targetValue = size.height, animationSpec = tween()) val targetHeight by animateDpAsState(label = "animate-pip-height", targetValue = size.height, animationSpec = tween())
val bottomPadding by animateDpAsState(label = "animate-pip-bottom-pad", targetValue = extraPadBottom, animationSpec = tween())
PictureInPicture( PictureInPicture(
contentSize = DpSize(targetWidth, targetHeight), contentSize = DpSize(targetWidth, targetHeight),
@ -415,7 +437,6 @@ private fun SmallMoveableLocalVideoRenderer(
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(16.dp)
.statusBarsPadding() .statusBarsPadding()
.padding(bottom = bottomPadding)
) { ) {
LocalParticipantRenderer( LocalParticipantRenderer(
localParticipant = localParticipant, localParticipant = localParticipant,
@ -482,9 +503,11 @@ private fun CallScreenPreview() {
callInfoView = { callInfoView = {
Text(text = "Call Info View Preview", modifier = Modifier.alpha(it)) Text(text = "Call Info View Preview", modifier = Modifier.alpha(it))
}, },
raiseHandSnackbar = {},
onNavigationClick = {}, onNavigationClick = {},
onLocalPictureInPictureClicked = {}, onLocalPictureInPictureClicked = {},
overflowParticipants = (1..5).map { CallParticipant() } overflowParticipants = emptyList(),
onControlsToggled = {}
) )
} }
} }

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable
@ -47,13 +48,19 @@ fun LocalParticipantRenderer(
) )
} }
val localRecipient = if (LocalInspectionMode.current) {
Recipient()
} else {
localParticipant.recipient
}
val model = remember { val model = remember {
ProfileContactPhoto(Recipient.self()) ProfileContactPhoto(localRecipient)
} }
val context = LocalContext.current val context = LocalContext.current
val fallback = remember { val fallback = remember {
FallbackAvatarDrawable(context, Recipient.self().getFallbackAvatar()) FallbackAvatarDrawable(context, localRecipient.getFallbackAvatar())
} }
GlideImage( GlideImage(

View file

@ -91,9 +91,9 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
seen.add(Recipient.self()); seen.add(Recipient.self());
for (GroupCall.RemoteDeviceState device : remoteDeviceStates) { for (GroupCall.RemoteDeviceState device : remoteDeviceStates) {
Recipient recipient = Recipient.externalPush(ACI.from(device.getUserId())); Recipient recipient = Recipient.externalPush(ACI.from(device.getUserId()));
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId()); CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
CallParticipant callParticipant = participants.get(callParticipantId); CallParticipant callParticipant = participants.get(callParticipantId);
BroadcastVideoSink videoSink; BroadcastVideoSink videoSink;
VideoTrack videoTrack = device.getVideoTrack(); VideoTrack videoTrack = device.getVideoTrack();