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.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.TextureView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.linphone.R 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.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.databinding.GenericListHeaderBinding import org.linphone.databinding.GenericListHeaderBinding
import org.linphone.databinding.RecordingListCellBinding import org.linphone.databinding.RecordingListCellBinding
import org.linphone.utils.HeaderAdapter import org.linphone.utils.*
import org.linphone.utils.LifecycleListAdapter
import org.linphone.utils.LifecycleViewHolder
import org.linphone.utils.TimestampUtils
class RecordingsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter<RecordingViewModel, RecordingsListAdapter.ViewHolder>( class RecordingsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter<RecordingViewModel, RecordingsListAdapter.ViewHolder>(
RecordingDiffCallback() RecordingDiffCallback()
), HeaderAdapter { ), 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: RecordingListCellBinding = DataBindingUtil.inflate( val binding: RecordingListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context), 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() executePendingBindings()
} }
} }

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.recordings.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -41,6 +42,9 @@ class RecordingsFragment : MasterFragment() {
private lateinit var viewModel: RecordingsViewModel private lateinit var viewModel: RecordingsViewModel
private lateinit var adapter: RecordingsListAdapter private lateinit var adapter: RecordingsListAdapter
private var videoX: Float = 0f
private var videoY: Float = 0f
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -83,6 +87,31 @@ class RecordingsFragment : MasterFragment() {
binding.setBackClickListener { findNavController().popBackStack() } binding.setBackClickListener { findNavController().popBackStack() }
binding.setEditClickListener { listSelectionViewModel.isEditionEnabled.value = true } 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 { override fun getItemCount(): Int {

View file

@ -19,6 +19,8 @@
*/ */
package org.linphone.activities.main.recordings.viewmodels package org.linphone.activities.main.recordings.viewmodels
import android.graphics.SurfaceTexture
import android.view.TextureView
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -35,6 +37,7 @@ import org.linphone.core.AudioDevice
import org.linphone.core.Player import org.linphone.core.Player
import org.linphone.core.PlayerListener import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingViewModel> { 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 name: String
lateinit var date: Date lateinit var date: Date
val duration: Int val duration = MutableLiveData<Int>()
get() { val formattedDuration = MutableLiveData<String>()
if (isClosed()) player.open(path) val formattedDate = MutableLiveData<String>()
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 position = MutableLiveData<Int>() val position = MutableLiveData<Int>()
val formattedPosition = MutableLiveData<String>() val formattedPosition = MutableLiveData<String>()
val isPlaying = MutableLiveData<Boolean>() val isPlaying = MutableLiveData<Boolean>()
val isVideoRecordingPlayingEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private val tickerChannel = ticker(1000, 1000) private val tickerChannel = ticker(1000, 1000)
private lateinit var player: Player private lateinit var player: Player
@ -82,23 +79,7 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
position.value = 0 position.value = 0
formattedPosition.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(0) formattedPosition.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(0)
// Use speaker sound card to play recordings, otherwise use earpiece initPlayer()
// 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)
} }
override fun onCleared() { override fun onCleared() {
@ -130,6 +111,8 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
} }
} }
} }
isVideoRecordingPlayingEvent.value = Event(player.isVideoAvailable)
} }
fun pause() { 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() { private fun updatePosition() {
val progress = if (isClosed()) 0 else player.currentPosition val progress = if (isClosed()) 0 else player.currentPosition
position.postValue(progress) position.postValue(progress)
@ -157,6 +196,8 @@ class RecordingViewModel(val path: String) : ViewModel(), Comparable<RecordingVi
pause() pause()
player.seek(0) player.seek(0)
updatePosition() updatePosition()
player.close()
isVideoRecordingPlayingEvent.value = Event(false)
} }
private fun isClosed(): Boolean { private fun isClosed(): Boolean {

View file

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

View file

@ -10,6 +10,9 @@
<variable <variable
name="editClickListener" name="editClickListener"
type="android.view.View.OnClickListener"/> type="android.view.View.OnClickListener"/>
<variable
name="videoTouchListener"
type="android.view.View.OnTouchListener" />
<variable <variable
name="viewModel" name="viewModel"
type="org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel" /> type="org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel" />
@ -85,6 +88,15 @@
android:gravity="center" android:gravity="center"
android:text="@string/recordings_empty_list" /> 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> </RelativeLayout>
</layout> </layout>