Reworked recordings player

This commit is contained in:
Sylvain Berfini 2024-05-16 14:00:24 +02:00
parent 4f848b182a
commit 31580e6291
3 changed files with 147 additions and 106 deletions

View file

@ -73,6 +73,7 @@ class RecordingsFragment : GenericMainFragment() {
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
observeToastEvents(listViewModel)
binding.setBackClickListener {
goBack()

View file

@ -22,27 +22,21 @@ package org.linphone.ui.main.recordings.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.media.AudioFocusRequestCompat
import java.text.SimpleDateFormat
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Factory
import org.linphone.core.Player
import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log
import org.linphone.utils.AudioUtils
import org.linphone.utils.FileUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class RecordingModel @WorkerThread constructor(val filePath: String, val fileName: String) {
class RecordingModel @WorkerThread constructor(
val filePath: String,
val fileName: String,
private val onPlay: ((model: RecordingModel) -> Unit),
private val onPause: ((model: RecordingModel) -> Unit)
) {
companion object {
private const val TAG = "[Recording Model]"
}
@ -61,20 +55,6 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
val position = MutableLiveData<Int>()
private var audioFocusRequest: AudioFocusRequestCompat? = null
private lateinit var player: Player
private val playerListener = PlayerListener {
Log.i("$TAG End of file reached")
pause()
player.seek(0)
position.postValue(0)
player.close()
}
private val tickerChannel = ticker(1000, 1000)
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
init {
isPlaying.postValue(false)
@ -108,13 +88,10 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
sipUri
}
val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
val audioPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
val audioPlayer = coreContext.core.createLocalPlayer(null, null, null)
if (audioPlayer != null) {
player = audioPlayer
player.open(filePath)
player.addListener(playerListener)
duration = player.duration
audioPlayer.open(filePath)
duration = audioPlayer.duration
formattedDuration = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
} else {
duration = 0
@ -125,32 +102,15 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
@WorkerThread
fun destroy() {
scope.cancel()
tickerChannel.cancel()
if (::player.isInitialized) {
if (player.state != Player.State.Closed) {
player.close()
}
if (audioFocusRequest != null) {
AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
coreContext.context,
audioFocusRequest!!
)
}
player.removeListener(playerListener)
}
}
@UiThread
fun togglePlayPause() {
coreContext.postOnCoreThread {
if (isPlaying.value == true) {
pause()
onPause.invoke(this)
} else {
play()
onPlay.invoke(this)
}
}
}
@ -160,58 +120,4 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
Log.i("$TAG Deleting call recording [$filePath]")
FileUtils.deleteFile(filePath)
}
@WorkerThread
private fun play() {
if (!::player.isInitialized) return
Log.i("$TAG Starting player, acquiring audio focus")
audioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
if (player.state == Player.State.Closed) {
player.open(filePath)
player.seek(0)
}
player.start()
isPlaying.postValue(true)
scope.launch {
withContext(Dispatchers.IO) {
for (tick in tickerChannel) {
withContext(Dispatchers.Main) {
if (player.state == Player.State.Playing) {
updatePosition()
}
}
}
}
}
}
@UiThread
private fun updatePosition() {
val progress = if (player.state == Player.State.Closed) 0 else player.currentPosition
position.value = progress
}
@WorkerThread
private fun pause() {
if (!::player.isInitialized) return
Log.i("$TAG Stopping player, releasing audio focus")
if (audioFocusRequest != null) {
AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
coreContext.context,
audioFocusRequest!!
)
}
coreContext.postOnCoreThread {
player.pause()
}
isPlaying.postValue(false)
}
}

View file

@ -22,10 +22,20 @@ package org.linphone.ui.main.recordings.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.media.AudioFocusRequestCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Player
import org.linphone.core.PlayerListener
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.ui.main.recordings.model.RecordingModel
import org.linphone.utils.AudioUtils
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
@ -46,6 +56,18 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
MutableLiveData<Event<Boolean>>()
}
private var audioFocusRequest: AudioFocusRequestCompat? = null
private var currentlyPlayedRecording: RecordingModel? = null
private var player: Player? = null
private val playerListener = PlayerListener {
Log.i("$TAG End of file reached")
stop(currentlyPlayedRecording)
}
private val tickerChannel = ticker(1000, 1000)
init {
searchBarVisible.value = false
fetchInProgress.value = true
@ -56,7 +78,13 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
}
override fun onCleared() {
if (currentlyPlayedRecording != null) {
stop(currentlyPlayedRecording)
player?.removeListener(playerListener)
player = null
}
recordings.value.orEmpty().forEach(RecordingModel::destroy)
super.onCleared()
}
@ -85,6 +113,101 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
}
}
@WorkerThread
fun onRecordingStartedPlaying(model: RecordingModel) {
val lowMediaVolume = AudioUtils.isMediaVolumeLow(coreContext.context)
if (lowMediaVolume) {
Log.w("$TAG Media volume is low, notifying user as they may not hear voice message")
showRedToastEvent.postValue(
Event(Pair(R.string.toast_low_media_volume, R.drawable.speaker_slash))
)
}
if (player == null) {
initAudioPlayer()
}
if (currentlyPlayedRecording != null && model != currentlyPlayedRecording) {
Log.i("$TAG Recording model has changed, stopping player before starting it")
stop(model)
}
currentlyPlayedRecording = model
play(model)
}
@WorkerThread
fun onRecordingPaused(model: RecordingModel) {
pause(model)
}
@WorkerThread
private fun initAudioPlayer() {
Log.i("$TAG Creating player")
val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
player = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
player?.addListener(playerListener)
}
@WorkerThread
private fun play(model: RecordingModel?) {
model ?: return
Log.i("$TAG Starting player")
if (player?.state == Player.State.Closed) {
player?.open(model.filePath)
player?.seek(0)
}
Log.i("$TAG Acquiring audio focus")
audioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
player?.start()
model.isPlaying.postValue(true)
viewModelScope.launch {
withContext(Dispatchers.IO) {
for (tick in tickerChannel) {
coreContext.postOnCoreThread {
if (player?.state == Player.State.Playing) {
model.position.postValue(player?.currentPosition)
}
}
}
}
}
}
@WorkerThread
private fun pause(model: RecordingModel?) {
model ?: return
Log.i("$TAG Pausing player, releasing audio focus")
if (audioFocusRequest != null) {
AudioUtils.releaseAudioFocusForVoiceRecordingOrPlayback(
coreContext.context,
audioFocusRequest!!
)
}
player?.pause()
model.isPlaying.postValue(false)
}
@WorkerThread
private fun stop(model: RecordingModel?) {
model ?: return
Log.i("$TAG Stopping player")
pause(model)
model.position.postValue(0)
player?.seek(0)
player?.close()
currentlyPlayedRecording = null
}
@WorkerThread
private fun computeList(filter: String) {
recordings.value.orEmpty().forEach(RecordingModel::destroy)
@ -95,7 +218,18 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
val path = file.path
val name = file.name
Log.i("$TAG Found file $path")
list.add(RecordingModel(path, name))
list.add(
RecordingModel(
path,
name,
{ model ->
onRecordingStartedPlaying(model)
},
{ model ->
onRecordingPaused(model)
}
)
)
}
list.sortBy {