Display video in recordings

This commit is contained in:
Sylvain Berfini 2020-07-27 12:45:34 +02:00
parent 09ebb7e63b
commit 2f4b8ae760
5 changed files with 138 additions and 34 deletions

View file

@ -21,9 +21,11 @@ package org.linphone.activities.main.recordings.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.TextureView
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import org.linphone.R
@ -31,14 +33,22 @@ import org.linphone.activities.main.recordings.viewmodels.RecordingViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.databinding.GenericListHeaderBinding
import org.linphone.databinding.RecordingListCellBinding
import org.linphone.utils.HeaderAdapter
import org.linphone.utils.LifecycleListAdapter
import org.linphone.utils.LifecycleViewHolder
import org.linphone.utils.TimestampUtils
import org.linphone.utils.*
class RecordingsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter<RecordingViewModel, RecordingsListAdapter.ViewHolder>(
RecordingDiffCallback()
), HeaderAdapter {
val isVideoRecordingPlayingEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private lateinit var videoSurface: TextureView
fun setVideoTextureView(textureView: TextureView) {
videoSurface = textureView
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: RecordingListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
@ -72,6 +82,15 @@ class RecordingsListAdapter(val selectionViewModel: ListTopBarViewModel) : Lifec
}
}
recording.isVideoRecordingPlayingEvent.observe(this@ViewHolder, Observer {
it.consume { value ->
if (value) {
recording.setTextureView(videoSurface)
}
isVideoRecordingPlayingEvent.value = Event(value)
}
})
executePendingBindings()
}
}

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.recordings.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
@ -41,6 +42,9 @@ class RecordingsFragment : MasterFragment() {
private lateinit var viewModel: RecordingsViewModel
private lateinit var adapter: RecordingsListAdapter
private var videoX: Float = 0f
private var videoY: Float = 0f
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -83,6 +87,31 @@ class RecordingsFragment : MasterFragment() {
binding.setBackClickListener { findNavController().popBackStack() }
binding.setEditClickListener { listSelectionViewModel.isEditionEnabled.value = true }
binding.setVideoTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
videoX = v.x - event.rawX
videoY = v.y - event.rawY
}
MotionEvent.ACTION_MOVE -> {
v.animate().x(event.rawX + videoX).y(event.rawY + videoY).setDuration(0).start()
}
else -> {
v.performClick()
false
}
}
true
}
adapter.isVideoRecordingPlayingEvent.observe(viewLifecycleOwner, Observer {
it.consume { value ->
viewModel.isVideoVisible.value = value
}
})
adapter.setVideoTextureView(binding.recordingVideoSurface)
}
override fun getItemCount(): Int {

View file

@ -19,6 +19,8 @@
*/
package org.linphone.activities.main.recordings.viewmodels
import android.graphics.SurfaceTexture
import android.view.TextureView
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -35,6 +37,7 @@ import org.linphone.core.AudioDevice
import org.linphone.core.Player
import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingViewModel> {
@ -46,23 +49,17 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
lateinit var name: String
lateinit var date: Date
val duration: Int
get() {
if (isClosed()) player.open(path)
return player.duration
}
val formattedDuration: String
get() = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // is already in milliseconds
val formattedDate: String
get() = DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
val duration = MutableLiveData<Int>()
val formattedDuration = MutableLiveData<String>()
val formattedDate = MutableLiveData<String>()
val position = MutableLiveData<Int>()
val formattedPosition = MutableLiveData<String>()
val isPlaying = MutableLiveData<Boolean>()
val isVideoRecordingPlayingEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private val tickerChannel = ticker(1000, 1000)
private lateinit var player: Player
@ -82,23 +79,7 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
position.value = 0
formattedPosition.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(0)
// Use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
if (device.type == AudioDevice.Type.Speaker) {
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
}
}
}
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) player = localPlayer
else Log.e("[Recording VM] Couldn't create local player!")
player.addListener(listener)
initPlayer()
}
override fun onCleared() {
@ -130,6 +111,8 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
}
}
}
isVideoRecordingPlayingEvent.value = Event(player.isVideoAvailable)
}
fun pause() {
@ -147,6 +130,62 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
}
}
fun setTextureView(textureView: TextureView) {
Log.i("[Recording VM] Is TextureView available? ${textureView.isAvailable}")
if (textureView.isAvailable) {
player.setWindowId(textureView.surfaceTexture)
} else {
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture?,
width: Int,
height: Int
) { }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) { }
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
return true
}
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture?,
width: Int,
height: Int
) {
Log.i("[Recording VM] Surface texture should be available now")
player.setWindowId(textureView.surfaceTexture)
}
}
}
}
private fun initPlayer() {
// Use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
if (device.type == AudioDevice.Type.Speaker) {
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
}
}
}
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) player = localPlayer
else Log.e("[Recording VM] Couldn't create local player!")
player.addListener(listener)
player.open(path)
duration.value = player.duration
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(player.duration) // is already in milliseconds
formattedDate.value = DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
}
private fun updatePosition() {
val progress = if (isClosed()) 0 else player.currentPosition
position.postValue(progress)
@ -157,6 +196,8 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
pause()
player.seek(0)
updatePosition()
player.close()
isVideoRecordingPlayingEvent.value = Event(false)
}
private fun isClosed(): Boolean {

View file

@ -28,8 +28,11 @@ import org.linphone.utils.FileUtils
class RecordingsViewModel : ViewModel() {
val recordingsList = MutableLiveData<ArrayList<RecordingViewModel>>()
val isVideoVisible = MutableLiveData<Boolean>()
init {
getRecordings()
isVideoVisible.value = false
}
fun deleteRecordings(list: ArrayList<RecordingViewModel>) {

View file

@ -10,6 +10,9 @@
<variable
name="editClickListener"
type="android.view.View.OnClickListener"/>
<variable
name="videoTouchListener"
type="android.view.View.OnTouchListener" />
<variable
name="viewModel"
type="org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel" />
@ -85,6 +88,15 @@
android:gravity="center"
android:text="@string/recordings_empty_list" />
<TextureView
android:id="@+id/recording_video_surface"
android:onTouch="@{videoTouchListener}"
android:visibility="@{viewModel.isVideoVisible ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</layout>