Started to add video to conference participants + fixed call ended fragment timer

This commit is contained in:
Sylvain Berfini 2023-10-27 14:08:23 +02:00
parent 2eb8b496cd
commit aa52f3d2b5
9 changed files with 177 additions and 15 deletions

View file

@ -44,6 +44,7 @@ import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.CallActivityBinding
import org.linphone.ui.call.fragment.ActiveCallFragmentDirections
import org.linphone.ui.call.fragment.ActiveConferenceCallFragmentDirections
import org.linphone.ui.call.fragment.AudioDevicesMenuDialogFragment
import org.linphone.ui.call.fragment.IncomingCallFragmentDirections
import org.linphone.ui.call.fragment.OutgoingCallFragmentDirections
@ -187,17 +188,50 @@ class CallActivity : AppCompatActivity() {
}
callsViewModel.goToActiveCallEvent.observe(this) {
it.consume {
it.consume { singleCall ->
val navController = findNavController(R.id.call_nav_container)
val action = when (navController.currentDestination?.id) {
R.id.outgoingCallFragment -> {
OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveCallFragment()
if (singleCall) {
Log.i("$TAG Going from outgoing call fragment to call fragment")
OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from outgoing call fragment to conference call fragment"
)
OutgoingCallFragmentDirections.actionOutgoingCallFragmentToActiveConferenceCallFragment()
}
}
R.id.incomingCallFragment -> {
IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveCallFragment()
if (singleCall) {
Log.i("$TAG Going from incoming call fragment to call fragment")
IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from incoming call fragment to conference call fragment"
)
IncomingCallFragmentDirections.actionIncomingCallFragmentToActiveConferenceCallFragment()
}
}
R.id.activeConferenceCallFragment -> {
if (singleCall) {
Log.i("$TAG Going from conference call fragment to call fragment")
ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment()
} else {
Log.i(
"$TAG Going from conference call fragment to conference call fragment"
)
ActiveConferenceCallFragmentDirections.actionGlobalActiveConferenceCallFragment()
}
}
else -> {
ActiveCallFragmentDirections.actionGlobalActiveCallFragment()
if (singleCall) {
Log.i("$TAG Going from call fragment to call fragment")
ActiveCallFragmentDirections.actionGlobalActiveCallFragment()
} else {
Log.i("$TAG Going from call fragment to conference call fragment")
ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment()
}
}
}
navController.navigate(action)

View file

@ -253,9 +253,12 @@ class ActiveCallFragment : GenericCallFragment() {
callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Going to conference fragment")
val action = ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment()
findNavController().navigate(action)
if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
Log.i("$TAG Going to conference fragment")
val action =
ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment()
findNavController().navigate(action)
}
}
}

View file

@ -19,11 +19,15 @@
*/
package org.linphone.ui.call.model
import android.view.TextureView
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.MediaDirection
import org.linphone.core.ParticipantDevice
import org.linphone.core.ParticipantDeviceListenerStub
import org.linphone.core.StreamType
import org.linphone.core.tools.Log
class ConferenceParticipantDeviceModel @WorkerThread constructor(
@ -40,7 +44,14 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor(
val isSpeaking = MutableLiveData<Boolean>()
val isVideoAvailable = MutableLiveData<Boolean>()
val isSendingVideo = MutableLiveData<Boolean>()
private lateinit var textureView: TextureView
private val deviceListener = object : ParticipantDeviceListenerStub() {
@WorkerThread
override fun onStateChanged(
participantDevice: ParticipantDevice,
state: ParticipantDevice.State?
@ -50,6 +61,7 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor(
)
}
@WorkerThread
override fun onIsMuted(participantDevice: ParticipantDevice, muted: Boolean) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] is ${if (participantDevice.isMuted) "muted" else "no longer muted"}"
@ -57,6 +69,7 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor(
isMuted.postValue(participantDevice.isMuted)
}
@WorkerThread
override fun onIsSpeakingChanged(
participantDevice: ParticipantDevice,
speaking: Boolean
@ -66,6 +79,41 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor(
)
isSpeaking.postValue(participantDevice.isSpeaking)
}
@WorkerThread
override fun onStreamAvailabilityChanged(
participantDevice: ParticipantDevice,
available: Boolean,
streamType: StreamType?
) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] availability changed to ${if (available) "available" else "not available"}"
)
if (streamType == StreamType.Video) {
val available = participantDevice.getStreamAvailability(StreamType.Video)
isVideoAvailable.postValue(available)
if (available) {
updateWindowId(textureView)
}
}
}
@WorkerThread
override fun onStreamCapabilityChanged(
participantDevice: ParticipantDevice,
direction: MediaDirection?,
streamType: StreamType?
) {
Log.i(
"$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] capability changed to [$direction]"
)
if (streamType == StreamType.Video) {
val videoCapability = participantDevice.getStreamCapability(StreamType.Video)
isSendingVideo.postValue(
videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly
)
}
}
}
init {
@ -76,10 +124,35 @@ class ConferenceParticipantDeviceModel @WorkerThread constructor(
Log.i(
"$TAG Participant [${device.address.asStringUriOnly()}] is in state [${device.state}]"
)
isVideoAvailable.postValue(device.getStreamAvailability(StreamType.Video))
val videoCapability = device.getStreamCapability(StreamType.Video)
isSendingVideo.postValue(
videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly
)
}
@WorkerThread
fun destroy() {
device.removeListener(deviceListener)
}
@UiThread
fun setTextureView(view: TextureView) {
Log.i(
"$TAG TextureView for participant [${device.address.asStringUriOnly()}] available from UI [$view]"
)
textureView = view
coreContext.postOnCoreThread {
updateWindowId(textureView)
}
}
@WorkerThread
private fun updateWindowId(windowId: Any?) {
Log.i(
"$$TAG Setting participant [${device.address.asStringUriOnly()}] window ID [$windowId]"
)
device.nativeVideoWindowId = windowId
}
}

View file

@ -114,7 +114,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
)
when (call.state) {
Call.State.Connected -> {
goToActiveCallEvent.postValue(Event(true))
goToActiveCallEvent.postValue(Event(call.conference == null))
}
else -> {
}
@ -132,8 +132,15 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
Log.i("$TAG Asking activity to show incoming call fragment")
showIncomingCallEvent.postValue(Event(true))
} else {
Log.i("$TAG Asking activity to show active call fragment")
goToActiveCallEvent.postValue(Event(true))
if (newCurrentCall.conference == null) {
Log.i("$TAG Asking activity to show active call fragment")
goToActiveCallEvent.postValue(Event(true))
} else {
Log.i(
"$TAG Asking activity to show active conference call fragment"
)
goToActiveCallEvent.postValue(Event(false))
}
}
}
}
@ -160,7 +167,7 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
when (currentCall.state) {
Call.State.Connected, Call.State.StreamsRunning, Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote, Call.State.UpdatedByRemote, Call.State.Updating -> {
goToActiveCallEvent.postValue(Event(true))
goToActiveCallEvent.postValue(Event(currentCall.conference == null))
}
Call.State.OutgoingInit, Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingEarlyMedia -> {
showOutgoingCallEvent.postValue(Event(true))

View file

@ -210,9 +210,11 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
Log.e(
"$TAG Failed to get a valid call to display, go to ended call fragment"
)
updateCallDuration()
goToEndedCallEvent.postValue(Event(true))
}
} else {
updateCallDuration()
Log.i("$TAG Call is ending, go to ended call fragment")
// Show that call was ended for a few seconds, then leave
// TODO FIXME: do not show it when call is being ended due to user terminating the call

View file

@ -29,6 +29,7 @@ import org.linphone.core.ConferenceInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.MediaDirection
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.Event
@ -135,6 +136,10 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
fun join() {
coreContext.postOnCoreThread { core ->
if (::conferenceInfo.isInitialized) {
Log.i("$TAG Stopping video preview")
core.nativePreviewWindowId = null
core.isVideoPreviewEnabled = false
val conferenceUri = conferenceInfo.uri
if (conferenceUri == null) {
Log.e("$TAG Conference Info doesn't have a conference SIP URI to call!")
@ -144,7 +149,8 @@ class MeetingWaitingRoomViewModel @UiThread constructor() : ViewModel() {
val params = core.createCallParams(null)
params ?: return@postOnCoreThread
params.isVideoEnabled = isVideoEnabled.value == true
params.isVideoEnabled = true
params.videoDirection = if (isVideoEnabled.value == true) MediaDirection.SendRecv else MediaDirection.RecvOnly
params.isMicEnabled = isMicrophoneMuted.value == false
params.account = core.defaultAccount
coreContext.startCall(conferenceUri, params)

View file

@ -25,6 +25,7 @@ import android.graphics.PorterDuff
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
@ -62,6 +63,7 @@ import org.linphone.contacts.AvatarGenerator
import org.linphone.core.ChatRoom
import org.linphone.core.ConsolidatedPresence
import org.linphone.core.tools.Log
import org.linphone.ui.call.model.ConferenceParticipantDeviceModel
/**
* This file contains all the data binding necessary for the app
@ -376,6 +378,15 @@ private suspend fun loadContactPictureWithCoil(
}
}
@UiThread
@BindingAdapter("participantTextureView")
fun setParticipantTextureView(
textureView: TextureView,
model: ConferenceParticipantDeviceModel
) {
model.setTextureView(textureView)
}
@UiThread
@BindingAdapter("onValueChanged")
fun AppCompatEditText.editTextSetting(lambda: () -> Unit) {

View file

@ -50,9 +50,10 @@
android:id="@+id/participant_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="5dp"
app:alignTopRight="false"
app:displayMode="hybrid"
participantTextureView="@{model}"
android:visibility="@{model.isSendingVideo ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -66,7 +67,7 @@
android:layout_marginEnd="10dp"
android:padding="2dp"
android:src="@drawable/microphone_slash"
android:background="@drawable/circle_white_button_background"
android:background="@drawable/shape_circle_white_background"
android:visibility="@{model.isMuted ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View file

@ -16,6 +16,12 @@
app:popUpTo="@id/outgoingCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true"/>
<action
android:id="@+id/action_outgoingCallFragment_to_activeConferenceCallFragment"
app:destination="@id/activeConferenceCallFragment"
app:popUpTo="@id/outgoingCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true" />
</fragment>
<action android:id="@+id/action_global_outgoingCallFragment"
@ -35,6 +41,12 @@
app:popUpTo="@id/incomingCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true" />
<action
android:id="@+id/action_incomingCallFragment_to_activeConferenceCallFragment"
app:destination="@id/activeConferenceCallFragment"
app:popUpTo="@id/outgoingCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true" />
</fragment>
<action android:id="@+id/action_global_incomingCallFragment"
@ -114,6 +126,19 @@
android:id="@+id/activeConferenceCallFragment"
android:name="org.linphone.ui.call.fragment.ActiveConferenceCallFragment"
android:label="ActiveConferenceCallFragment"
tools:layout="@layout/call_active_conference_fragment"/>
tools:layout="@layout/call_active_conference_fragment">
<action
android:id="@+id/action_activeConferenceCallFragment_to_activeCallFragment"
app:destination="@id/activeCallFragment"
app:popUpTo="@id/activeConferenceCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true" />
</fragment>
<action android:id="@+id/action_global_activeConferenceCallFragment"
app:destination="@id/activeConferenceCallFragment"
app:popUpTo="@id/activeConferenceCallFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true"/>
</navigation>