mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 03:18:06 +00:00
Reworked recordings player
This commit is contained in:
parent
4f848b182a
commit
31580e6291
3 changed files with 147 additions and 106 deletions
|
|
@ -73,6 +73,7 @@ class RecordingsFragment : GenericMainFragment() {
|
|||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = listViewModel
|
||||
observeToastEvents(listViewModel)
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue