mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Added video calls
This commit is contained in:
parent
6124cdd806
commit
970652d81f
12 changed files with 326 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
12
app/src/main/res/values/attrs.xml
Normal file
12
app/src/main/res/values/attrs.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Reference in a new issue