From 31580e62913ab925c87e9e01e0eb8d46df4c6fa6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 16 May 2024 14:00:24 +0200 Subject: [PATCH] Reworked recordings player --- .../recordings/fragment/RecordingsFragment.kt | 1 + .../main/recordings/model/RecordingModel.kt | 116 ++------------- .../viewmodel/RecordingsListViewModel.kt | 136 +++++++++++++++++- 3 files changed, 147 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt index 6b0110be8..60e621627 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt @@ -73,6 +73,7 @@ class RecordingsFragment : GenericMainFragment() { binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = listViewModel + observeToastEvents(listViewModel) binding.setBackClickListener { goBack() diff --git a/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt index 2c8495d10..028d2f83e 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt @@ -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() - 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) - } } diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt index e4f76a0fd..96286a39b 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt @@ -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>() } + 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 {