Using new APIs to be able to make asymetrical video calls

This commit is contained in:
Sylvain Berfini 2024-02-23 11:56:30 +01:00
parent d6ea531cea
commit 9c1b9b2939
19 changed files with 104 additions and 49 deletions

View file

@ -19,6 +19,9 @@ upload_bw=0
[video]
size=vga
automatically_accept=1
automatically_initiate=0
automatically_accept_direction=2 #receive only
[app]
tunnel=disabled

View file

@ -36,8 +36,6 @@ android_disable_audio_focus_requests=1
displaytype=MSAndroidTextureDisplay
auto_resize_preview_to_keep_ratio=1
max_conference_size=vga
automatically_accept=1
automatically_initiate=0
[misc]
enable_basic_to_client_group_chat_room_migration=0

View file

@ -249,6 +249,16 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
Log.i("$TAG Configuring Core")
core.videoCodecPriorityPolicy = CodecPriorityPolicy.Auto
val oldVersion = corePreferences.linphoneConfigurationVersion
val newVersion = "6.0.0"
if (oldVersion == "5.2") {
Log.i("$TAG Migrating configuration from [$oldVersion] to [$newVersion]")
val policy = core.videoActivationPolicy.clone()
policy.automaticallyAccept = true
policy.automaticallyAcceptDirection = MediaDirection.RecvOnly
core.videoActivationPolicy = policy
}
updateFriendListsSubscriptionDependingOnDefaultAccount()
computeUserAgent()
@ -262,7 +272,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audioManager.registerAudioDeviceCallback(audioDeviceCallback, coreThread)
corePreferences.linphoneConfigurationVersion = "6.0"
corePreferences.linphoneConfigurationVersion = newVersion
Log.i("$TAG Report Core created and started")
}
@ -354,6 +364,30 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
return found != null
}
@WorkerThread
fun startAudioCall(
address: Address,
forceZRTP: Boolean = false,
localAddress: Address? = null
) {
val params = core.createCallParams(null)
params?.isVideoEnabled = true
params?.videoDirection = MediaDirection.Inactive
startCall(address, params, forceZRTP, localAddress)
}
@WorkerThread
fun startVideoCall(
address: Address,
forceZRTP: Boolean = false,
localAddress: Address? = null
) {
val params = core.createCallParams(null)
params?.isVideoEnabled = true
params?.videoDirection = MediaDirection.SendRecv
startCall(address, params, forceZRTP, localAddress)
}
@WorkerThread
fun startCall(
address: Address,

View file

@ -335,6 +335,22 @@ class ActiveCallFragment : GenericCallFragment() {
}
}
callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
coreContext.core.nativePreviewWindowId = if (sending) {
binding.localPreviewVideoSurface
} else {
null
}
}
callViewModel.isReceivingVideo.observe(viewLifecycleOwner) { receiving ->
coreContext.core.nativeVideoWindowId = if (receiving) {
binding.remoteVideoSurface
} else {
null
}
}
callViewModel.chatRoomCreationErrorEvent.observe(viewLifecycleOwner) {
it.consume { error ->
(requireActivity() as CallActivity).showRedToast(
@ -368,8 +384,6 @@ class ActiveCallFragment : GenericCallFragment() {
super.onResume()
coreContext.postOnCoreThread { core ->
core.nativeVideoWindowId = binding.remoteVideoSurface
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
binding.localPreviewVideoSurface.setOnTouchListener(previewTouchListener)
// Need to be done manually

View file

@ -39,7 +39,7 @@ class NewCallFragment : AbstractNewTransferCallFragment() {
@WorkerThread
override fun action(address: Address) {
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startCall(address)
coreContext.startAudioCall(address)
coreContext.postOnMainThread {
findNavController().popBackStack()

View file

@ -76,6 +76,10 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
val isVideoEnabled = MutableLiveData<Boolean>()
val isSendingVideo = MutableLiveData<Boolean>()
val isReceivingVideo = MutableLiveData<Boolean>()
val showSwitchCamera = MutableLiveData<Boolean>()
val isOutgoing = MutableLiveData<Boolean>()
@ -230,6 +234,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
Log.i("$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]")
if (LinphoneUtils.isCallOutgoing(call.state)) {
isVideoEnabled.postValue(call.params.isVideoEnabled)
updateVideoDirection(call.currentParams.videoDirection)
} else if (LinphoneUtils.isCallEnding(call.state)) {
// If current call is being terminated but there is at least one other call, switch
val core = call.core
@ -270,6 +275,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
isVideoEnabled.postValue(videoEnabled)
updateVideoDirection(call.currentParams.videoDirection)
// Toggle full screen OFF when remote disables video
if (!videoEnabled && fullScreenMode.value == true) {
@ -606,10 +612,15 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
params?.videoDirection = MediaDirection.SendRecv
}
}
} else {
params?.isVideoEnabled = params?.isVideoEnabled == false
} else if (params != null) {
params.isVideoEnabled = true
params.videoDirection = when (currentCall.currentParams.videoDirection) {
MediaDirection.RecvOnly -> MediaDirection.SendRecv
MediaDirection.SendRecv, MediaDirection.SendOnly -> MediaDirection.RecvOnly
else -> MediaDirection.SendOnly
}
Log.i(
"$TAG Updating call with video enabled set to ${params?.isVideoEnabled}"
"$TAG Updating call with video enabled and media direction set to ${params.videoDirection}"
)
}
currentCall.update(params)
@ -967,6 +978,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
} else {
isVideoEnabled.postValue(call.currentParams.isVideoEnabled)
}
updateVideoDirection(call.currentParams.videoDirection)
if (ActivityCompat.checkSelfPermission(
coreContext.context,
@ -1056,6 +1068,16 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
@WorkerThread
private fun updateVideoDirection(direction: MediaDirection) {
isSendingVideo.postValue(
direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly
)
isReceivingVideo.postValue(
direction == MediaDirection.SendRecv || direction == MediaDirection.RecvOnly
)
}
@AnyThread
private fun updateCallQualityIcon() {
viewModelScope.launch {

View file

@ -622,7 +622,7 @@ class MainActivity : GenericActivity() {
)
Log.i("$TAG Interpreted SIP URI is [${address?.asStringUriOnly()}]")
if (address != null) {
coreContext.startCall(address)
coreContext.startAudioCall(address)
}
}
}

View file

@ -217,7 +217,7 @@ class ConversationModel @WorkerThread constructor(
coreContext.postOnCoreThread {
val address = chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startCall(address)
coreContext.startAudioCall(address)
}
}

View file

@ -557,7 +557,7 @@ class MessageModel @WorkerThread constructor(
Log.i("$TAG Clicked on SIP URI: $text")
val address = coreContext.core.interpretUrl(text, false)
if (address != null) {
coreContext.startCall(address)
coreContext.startAudioCall(address)
} else {
Log.w("$TAG Failed to parse [$text] as SIP URI")
}

View file

@ -207,7 +207,7 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startCall(conferenceAddress)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
// TODO: notify error to user
@ -284,9 +284,7 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi
val address = firstParticipant?.address
if (address != null) {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
val params = core.createCallParams(null)
params?.isVideoEnabled = false
coreContext.startCall(address, params)
coreContext.startAudioCall(address)
} else {
Log.e("$TAG Failed to find participant to call!")
}

View file

@ -283,7 +283,7 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startCall(conferenceAddress)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
// TODO: notify error to user
@ -394,9 +394,7 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
val address = firstParticipant?.address
if (address != null) {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
val params = core.createCallParams(null)
params?.isVideoEnabled = false
coreContext.startCall(address, params)
coreContext.startAudioCall(address)
}
}
}

View file

@ -140,15 +140,11 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
when (expectedAction) {
START_AUDIO_CALL -> {
Log.i("$TAG Audio calling SIP address [${address.asStringUriOnly()}]")
val params = core.createCallParams(null)
params?.isVideoEnabled = false
coreContext.startCall(address, params)
coreContext.startAudioCall(address)
}
START_VIDEO_CALL -> {
Log.i("$TAG Video calling SIP address [${address.asStringUriOnly()}]")
val params = core.createCallParams(null)
params?.isVideoEnabled = true
coreContext.startCall(address, params)
coreContext.startVideoCall(address)
}
START_CONVERSATION -> {
Log.i(
@ -405,7 +401,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
"$TAG Only 1 SIP address found for contact [${friend.name}], starting audio call directly"
)
val address = friend.addresses.first()
coreContext.startCall(address)
coreContext.startAudioCall(address)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, LinphoneUtils.applyInternationalPrefix())
@ -413,7 +409,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], starting audio call directly"
)
coreContext.startCall(address)
coreContext.startAudioCall(address)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}
@ -442,9 +438,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
"$TAG Only 1 SIP address found for contact [${friend.name}], starting video call directly"
)
val address = friend.addresses.first()
val params = core.createCallParams(null)
params?.isVideoEnabled = true
coreContext.startCall(address, params)
coreContext.startVideoCall(address)
} else if (addressesCount == 0 && numbersCount == 1 && enablePhoneNumbers) {
val number = friend.phoneNumbers.first()
val address = core.interpretUrl(number, LinphoneUtils.applyInternationalPrefix())
@ -452,9 +446,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
Log.i(
"$TAG Only 1 phone number found for contact [${friend.name}], starting video call directly"
)
val params = core.createCallParams(null)
params?.isVideoEnabled = true
coreContext.startCall(address, params)
coreContext.startVideoCall(address)
} else {
Log.e("$TAG Failed to interpret phone number [$number] as SIP address")
}

View file

@ -185,7 +185,7 @@ class HistoryListFragment : AbstractTopBarFragment() {
)
} else {
Log.i("$TAG Starting call to [${model.address.asStringUriOnly()}]")
coreContext.startCall(model.address)
coreContext.startAudioCall(model.address)
}
}
}

View file

@ -150,7 +150,7 @@ class StartCallFragment : GenericAddressPickerFragment() {
@WorkerThread
override fun onSingleAddressSelected(address: Address, friend: Friend) {
coreContext.startCall(address)
coreContext.startAudioCall(address)
}
override fun onPause() {

View file

@ -140,18 +140,14 @@ class ContactHistoryViewModel @UiThread constructor() : ViewModel() {
@UiThread
fun startAudioCall() {
coreContext.postOnCoreThread { core ->
val params = core.createCallParams(null)
params?.isVideoEnabled = false
coreContext.startCall(address, params)
coreContext.startAudioCall(address)
}
}
@UiThread
fun startVideoCall() {
coreContext.postOnCoreThread { core ->
val params = core.createCallParams(null)
params?.isVideoEnabled = true
coreContext.startCall(address, params)
coreContext.startVideoCall(address)
}
}

View file

@ -83,7 +83,7 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
Log.i(
"$TAG Conference info created, address is ${conferenceAddress.asStringUriOnly()}"
)
coreContext.startCall(conferenceAddress)
coreContext.startVideoCall(conferenceAddress)
} else {
Log.e("$TAG Conference info URI is null!")
// TODO: notify error to user
@ -122,7 +122,7 @@ class StartCallViewModel @UiThread constructor() : AddressSelectionViewModel() {
)
if (address != null) {
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startCall(address)
coreContext.startAudioCall(address)
} else {
Log.e("$TAG Failed to parse [$suggestion] as SIP address")
}

View file

@ -257,7 +257,7 @@ abstract class AddressSelectionViewModel @UiThread constructor() : DefaultAccoun
}
val model = ContactOrSuggestionModel(address) {
coreContext.startCall(address)
coreContext.startAudioCall(address)
}
suggestionsList.add(model)

View file

@ -52,7 +52,7 @@
android:padding="@dimen/call_button_icon_padding"
android:enabled="@{!viewModel.isPaused &amp;&amp; !viewModel.isPausedByRemote}"
android:visibility="@{viewModel.hideVideo ? View.GONE : View.VISIBLE}"
android:src="@{viewModel.isVideoEnabled ? @drawable/video_camera : @drawable/video_camera_slash, default=@drawable/video_camera}"
android:src="@{viewModel.isSendingVideo ? @drawable/video_camera : @drawable/video_camera_slash, default=@drawable/video_camera}"
android:background="@drawable/in_call_button_background_red"
app:tint="@color/in_call_button_tint_color"
app:layout_constraintHorizontal_bias="1"

View file

@ -105,7 +105,7 @@
android:layout_marginBottom="@{viewModel.fullScreenMode || viewModel.pipMode || viewModel.halfOpenedFolded ? @dimen/zero : @dimen/call_main_actions_menu_margin, default=@dimen/call_main_actions_menu_margin}"
android:layout_marginTop="@{viewModel.fullScreenMode || viewModel.pipMode || viewModel.halfOpenedFolded ? @dimen/zero : @dimen/call_remote_video_top_margin, default=@dimen/call_remote_video_top_margin}"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:visibility="@{viewModel.isVideoEnabled &amp;&amp; !(viewModel.isPaused || viewModel.isPausedByRemote) ? View.VISIBLE : View.GONE}"
android:visibility="@{viewModel.isReceivingVideo &amp;&amp; !(viewModel.isPaused || viewModel.isPausedByRemote) ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"
app:layout_constraintEnd_toEndOf="parent"
@ -192,7 +192,7 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:src="@drawable/camera_rotate"
android:visibility="@{!viewModel.fullScreenMode &amp;&amp; !viewModel.pipMode &amp;&amp; viewModel.isVideoEnabled &amp;&amp; viewModel.showSwitchCamera ? View.VISIBLE : View.GONE}"
android:visibility="@{!viewModel.fullScreenMode &amp;&amp; !viewModel.pipMode &amp;&amp; viewModel.isSendingVideo &amp;&amp; viewModel.showSwitchCamera ? View.VISIBLE : View.GONE}"
app:tint="@color/white"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
@ -262,7 +262,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="20dp"
android:visibility="@{viewModel.isVideoEnabled &amp;&amp; !(viewModel.isPaused || viewModel.isPausedByRemote) ? View.VISIBLE : View.GONE}"
android:visibility="@{viewModel.isSendingVideo &amp;&amp; !(viewModel.isPaused || viewModel.isPausedByRemote) ? View.VISIBLE : View.GONE}"
app:alignTopRight="true"
app:displayMode="black_bars"
app:layout_constraintBottom_toBottomOf="@id/remote_video_surface"