Show video preview in in-call conversation screen

This commit is contained in:
Sylvain Berfini 2026-03-02 11:14:34 +01:00
parent 7fe554be3c
commit 1c4f73a9b8
6 changed files with 116 additions and 0 deletions

View file

@ -34,6 +34,7 @@ Group changes to describe their impact on the project, as follows:
- one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app
- Added a vu meter for recording & playback volumes (must be enabled in developer settings)
- Added support for HDMI audio devices
- Added video preview during in-call conversation
### Changed
- No longer follow TelecomManager audio endpoint during calls, using our own routing policy

View file

@ -19,12 +19,20 @@
*/
package org.linphone.ui.call.fragment
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnLayout
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.call.view.RoundCornersTextureView
import org.linphone.ui.call.viewmodel.CurrentCallViewModel
import org.linphone.ui.fileviewer.FileViewerActivity
import org.linphone.ui.fileviewer.MediaViewerActivity
import org.linphone.ui.main.chat.fragment.ConversationFragment
@ -34,9 +42,48 @@ class ConversationFragment : ConversationFragment() {
private const val TAG = "[In-call Conversation Fragment]"
}
private lateinit var callViewModel: CurrentCallViewModel
private lateinit var localPreviewVideoSurface: RoundCornersTextureView
private var videoPreviewX: Float = 0f
private var videoPreviewY: Float = 0f
// For moving video preview purposes
private val videoPreviewTouchListener = View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
videoPreviewX = view.x - event.rawX
videoPreviewY = view.y - event.rawY
true
}
MotionEvent.ACTION_UP -> {
videoPreviewX = view.x
videoPreviewY = view.y
true
}
MotionEvent.ACTION_MOVE -> {
view.animate()
.x(event.rawX + videoPreviewX)
.y(event.rawY + videoPreviewY)
.setDuration(0)
.start()
true
}
else -> {
view.performClick()
false
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callViewModel = requireActivity().run {
ViewModelProvider(this)[CurrentCallViewModel::class.java]
}
Log.i("$TAG Creating an in-call ConversationFragment")
sendMessageViewModel.isCallConversation.value = true
viewModel.isCallConversation.value = true
@ -70,5 +117,50 @@ class ConversationFragment : ConversationFragment() {
}
}
}
val layout = layoutInflater.inflate(R.layout.call_video_local_preview_surface, binding.constraintLayout, false)
binding.constraintLayout.addView(layout)
localPreviewVideoSurface = layout.findViewById<RoundCornersTextureView>(R.id.local_preview_video_surface)
callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending ->
coreContext.postOnCoreThread { core ->
core.nativePreviewWindowId = if (sending) {
Log.i("$TAG We are sending video, setting capture preview surface")
localPreviewVideoSurface
} else {
Log.i("$TAG We are not sending video, clearing capture preview surface")
null
}
}
}
}
override fun onResume() {
super.onResume()
(binding.root as? ViewGroup)?.doOnLayout {
setupVideoPreview(localPreviewVideoSurface)
}
}
override fun onPause() {
super.onPause()
cleanVideoPreview(localPreviewVideoSurface)
}
@SuppressLint("ClickableViewAccessibility")
private fun setupVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) {
if (requireActivity().isInPictureInPictureMode) {
Log.i("$TAG Activity is in PiP mode, do not move video preview")
return
}
localPreviewVideoSurface.setOnTouchListener(videoPreviewTouchListener)
}
@SuppressLint("ClickableViewAccessibility")
private fun cleanVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) {
localPreviewVideoSurface.setOnTouchListener(null)
}
}

View file

@ -100,6 +100,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
app:alignTopRight="true"
app:displayMode="black_bars"
roundCornersRadius="@dimen/call_round_corners_texture_view_radius"

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.linphone.ui.call.view.RoundCornersTextureView
android:id="@+id/local_preview_video_surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:alignTopRight="true"
app:displayMode="black_bars"
roundCornersRadius="@dimen/call_round_corners_texture_view_radius"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="@dimen/call_video_preview_max_size"
app:layout_constraintWidth_max="@dimen/call_video_preview_max_size" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -98,6 +98,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
app:alignTopRight="true"
app:displayMode="black_bars"
roundCornersRadius="@dimen/call_round_corners_texture_view_radius"

View file

@ -53,6 +53,7 @@
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_background_contrast_in_dark_mode">