Added video calls

This commit is contained in:
Sylvain Berfini 2023-08-11 16:00:13 +02:00
parent 6124cdd806
commit 970652d81f
12 changed files with 326 additions and 7 deletions

View file

@ -177,6 +177,30 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
Log.i("[Context] Starting call $call")
}
fun switchCamera() {
val currentDevice = core.videoDevice
Log.i("[Context] Current camera device is $currentDevice")
for (camera in core.videoDevicesList) {
if (camera != currentDevice && camera != "StaticImage: Static picture") {
Log.i("[Context] New camera device will be $camera")
core.videoDevice = camera
break
}
}
val call = core.currentCall
if (call == null) {
Log.w("[Context] Switching camera while not in call")
return
}
call.update(null)
}
fun showSwitchCameraButton(): Boolean {
return core.videoDevicesList.size > 2 // Count StaticImage camera
}
private fun showCallActivity() {
Log.i("[Context] Starting VoIP activity")
val intent = Intent(context, VoipActivity::class.java)

View file

@ -34,6 +34,8 @@ import org.linphone.databinding.MainActivityBinding
class MainActivity : AppCompatActivity() {
companion object {
private const val CONTACTS_PERMISSION_REQUEST = 0
private const val CAMERA_PERMISSION_REQUEST = 1
private const val RECORD_AUDIO_PERMISSION_REQUEST = 2
}
private lateinit var binding: MainActivityBinding
@ -54,6 +56,8 @@ class MainActivity : AppCompatActivity() {
if (checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
loadContacts()
}
checkSelfPermission(Manifest.permission.CAMERA)
checkSelfPermission(Manifest.permission.RECORD_AUDIO)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.lifecycleOwner = this
@ -68,6 +72,18 @@ class MainActivity : AppCompatActivity() {
CONTACTS_PERMISSION_REQUEST
)
}
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_REQUEST
)
}
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.RECORD_AUDIO),
RECORD_AUDIO_PERMISSION_REQUEST
)
}
}
override fun onRequestPermissionsResult(

View file

@ -22,9 +22,11 @@ package org.linphone.ui.voip.fragment
import android.os.Bundle
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.databinding.VoipActiveCallFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
@ -38,6 +40,33 @@ class ActiveCallFragment : GenericFragment() {
private lateinit var callViewModel: CurrentCallViewModel
// For moving video preview purposes
private var previewX: Float = 0f
private var previewY: Float = 0f
private val previewTouchListener = View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
previewX = view.x - event.rawX
previewY = view.y - event.rawY
true
}
MotionEvent.ACTION_MOVE -> {
view.animate()
.x(event.rawX + previewX)
.y(event.rawY + previewY)
.setDuration(0)
.start()
true
}
else -> {
view.performClick()
false
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -101,4 +130,19 @@ class ActiveCallFragment : GenericFragment() {
binding.chronometer.start()
}
}
override fun onResume() {
super.onResume()
coreContext.postOnCoreThread { core ->
core.nativeVideoWindowId = binding.remoteVideoSurface
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
binding.localPreviewVideoSurface.setOnTouchListener(previewTouchListener)
}
}
override fun onPause() {
super.onPause()
binding.localPreviewVideoSurface.setOnTouchListener(null)
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2010-2021 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.voip.view
import android.content.Context
import android.graphics.Outline
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import org.linphone.R
import org.linphone.mediastream.video.capture.CaptureTextureView
class RoundCornersTextureView : CaptureTextureView {
private var mRadius: Float = 0f
constructor(context: Context) : super(context) {
mAlignTopRight = true
mDisplayMode = DisplayMode.BLACK_BARS
setRoundCorners()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
readAttributes(attrs)
setRoundCorners()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
readAttributes(attrs)
setRoundCorners()
}
private fun readAttributes(attrs: AttributeSet) {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.RoundCornersTextureView,
0,
0
).apply {
try {
mAlignTopRight = getBoolean(R.styleable.RoundCornersTextureView_alignTopRight, true)
val mode = getInteger(
R.styleable.RoundCornersTextureView_displayMode,
DisplayMode.BLACK_BARS.ordinal
)
mDisplayMode = when (mode) {
1 -> DisplayMode.OCCUPY_ALL_SPACE
2 -> DisplayMode.HYBRID
else -> DisplayMode.BLACK_BARS
}
mRadius = getFloat(
R.styleable.RoundCornersTextureView_radius,
context.resources.getDimension(
R.dimen.in_call_round_corners_texture_view_radius
)
)
} finally {
recycle()
}
}
}
private fun setRoundCorners() {
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val rect = if (previewRectF != null &&
actualDisplayMode == DisplayMode.BLACK_BARS &&
mAlignTopRight
) {
Rect(
previewRectF.left.toInt(),
previewRectF.top.toInt(),
previewRectF.right.toInt(),
previewRectF.bottom.toInt()
)
} else {
Rect(
0,
0,
width,
height
)
}
outline.setRoundRect(rect, mRadius)
}
}
clipToOutline = true
}
override fun setAspectRatio(width: Int, height: Int) {
super.setAspectRatio(width, height)
val previewSize = previewVideoSize
if (previewSize.width > 0 && previewSize.height > 0) {
setRoundCorners()
}
}
}

View file

@ -27,6 +27,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.MediaDirection
import org.linphone.core.MediaEncryption
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
@ -50,14 +51,19 @@ class CurrentCallViewModel() : ViewModel() {
val isMicrophoneMuted = MutableLiveData<Boolean>()
val fullScreenMode = MutableLiveData<Boolean>()
// To synchronize chronometers in UI
val callDuration = MutableLiveData<Int>()
// ZRTP related
val isRemoteDeviceTrusted = MutableLiveData<Boolean>()
val showZrtpSasDialogEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()
}
val callDuration = MutableLiveData<Int>()
// Extras actions
val isActionsMenuExpanded = MutableLiveData<Boolean>()
@ -88,11 +94,20 @@ class CurrentCallViewModel() : ViewModel() {
override fun onEncryptionChanged(call: Call, on: Boolean, authenticationToken: String?) {
updateEncryption()
}
override fun onStateChanged(call: Call, state: Call.State?, message: String) {
if (LinphoneUtils.isCallOutgoing(call.state)) {
isVideoEnabled.postValue(call.params.isVideoEnabled)
} else {
isVideoEnabled.postValue(call.currentParams.isVideoEnabled)
}
}
}
init {
isVideoEnabled.value = false
isMicrophoneMuted.value = false
fullScreenMode.value = false
isActionsMenuExpanded.value = false
extraActionsMenuTranslateY.value = extraActionsMenuHeight
@ -119,6 +134,14 @@ class CurrentCallViewModel() : ViewModel() {
}
}
fun answer() {
// UI thread
coreContext.postOnCoreThread {
Log.i("$TAG Answering call [$call]")
call.accept()
}
}
fun hangUp() {
// UI thread
coreContext.postOnCoreThread {
@ -154,7 +177,38 @@ class CurrentCallViewModel() : ViewModel() {
// UI thread
// TODO: check video permission
// TODO
coreContext.postOnCoreThread { core ->
if (::call.isInitialized) {
val params = core.createCallParams(call)
if (call.conference != null) {
if (params?.isVideoEnabled == false) {
params.isVideoEnabled = true
params.videoDirection = MediaDirection.SendRecv
} else {
if (params?.videoDirection == MediaDirection.SendRecv || params?.videoDirection == MediaDirection.SendOnly) {
params.videoDirection = MediaDirection.RecvOnly
} else {
params?.videoDirection = MediaDirection.SendRecv
}
}
} else {
params?.isVideoEnabled = params?.isVideoEnabled == false
Log.i(
"$TAG Updating call with video enabled set to ${params?.isVideoEnabled}"
)
}
call.update(params)
}
}
}
fun switchCamera() {
coreContext.switchCamera()
}
fun toggleFullScreen() {
if (fullScreenMode.value == false && isVideoEnabled.value == false) return
fullScreenMode.value = fullScreenMode.value != true
}
fun toggleExpandActionsMenu() {

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/green_online"/>
</shape>

View file

@ -65,13 +65,14 @@
app:layout_constraintBottom_toBottomOf="@id/call_direction_label"/>
<ImageView
android:onClick="@{() -> viewModel.switchCamera()}"
android:id="@+id/switch_camera"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="10dp"
android:adjustViewBounds="true"
android:src="@drawable/switch_camera"
android:enabled="@{viewModel.isVideoEnabled()}"
android:visibility="@{viewModel.isVideoEnabled() ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/call_direction_label"
app:layout_constraintBottom_toBottomOf="@id/call_direction_label"
app:layout_constraintEnd_toEndOf="parent"
@ -148,6 +149,31 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<org.linphone.ui.voip.view.RoundCornersTextureView
android:id="@+id/remote_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:visibility="@{viewModel.isVideoEnabled ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="@id/background"
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toTopOf="@id/background" />
<org.linphone.ui.voip.view.RoundCornersTextureView
android:id="@+id/local_preview_video_surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="40dp"
android:visibility="@{viewModel.isVideoEnabled ? View.VISIBLE : View.GONE}"
app:alignTopRight="true"
app:displayMode="black_bars"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintEnd_toEndOf="@id/background"
app:layout_constraintHeight_max="200dp"
app:layout_constraintWidth_max="200dp" />
<include
android:id="@+id/bottom_bar"
layout="@layout/voip_call_extra_actions"

View file

@ -44,19 +44,20 @@
android:paddingBottom="15dp"
android:src="@drawable/hang_up"
android:background="@drawable/shape_hang_up_button_background"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:onClick="@{() -> viewModel.toggleVideo()}"
android:id="@+id/toggle_video"
android:enabled="@{viewModel.isVideoEnabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:padding="15dp"
android:src="@drawable/in_call_camera_button"
android:src="@{viewModel.isVideoEnabled() ? @drawable/camera_enabled : @drawable/camera_disabled, default=@drawable/camera_enabled}"
android:background="@drawable/in_call_button_background"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/toggle_mute_mic" />
@ -74,6 +75,7 @@
app:layout_constraintEnd_toStartOf="@id/change_audio_output" />
<ImageView
android:onClick="@{() -> viewModel.changeAudioOutputDevice()}"
android:id="@+id/change_audio_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -29,9 +29,25 @@
android:paddingBottom="15dp"
android:src="@drawable/hang_up"
android:background="@drawable/shape_hang_up_button_background"
app:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:onClick="@{() -> viewModel.answer()}"
android:id="@+id/answer_call"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="30dp"
android:padding="15dp"
android:src="@drawable/calls"
android:background="@drawable/shape_answer_button_background"
app:tint="@color/white"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="@id/hang_up"
app:layout_constraintBottom_toBottomOf="@id/hang_up"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -40,13 +40,14 @@
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:onClick="@{() -> viewModel.switchCamera()}"
android:id="@+id/switch_camera"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="10dp"
android:adjustViewBounds="true"
android:src="@drawable/switch_camera"
android:enabled="@{viewModel.isVideoEnabled()}"
android:visibility="@{viewModel.isVideoEnabled() ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/call_direction_label"
app:layout_constraintBottom_toBottomOf="@id/call_direction_label"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundCornersTextureView">
<attr name="alignTopRight" format="boolean" />
<attr name="displayMode" format="enum">
<enum name="black_bars" value="0"/>
<enum name="occupy_all_space" value="1"/>
<enum name="hybrid" value="2"/>
</attr>
<attr name="radius" format="float" />
</declare-styleable>
</resources>

View file

@ -14,6 +14,7 @@
<dimen name="in_call_main_actions_menu_height">116dp</dimen>
<dimen name="in_call_extra_actions_menu_height">237dp</dimen>
<dimen name="in_call_all_actions_menu_height">353dp</dimen> <!-- sum of above two -->
<dimen name="in_call_round_corners_texture_view_radius">20dp</dimen>
<dimen name="toast_max_width">360dp</dimen>
</resources>