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.lifecycleOwner = viewLifecycleOwner
|
||||||
binding.viewModel = listViewModel
|
binding.viewModel = listViewModel
|
||||||
|
observeToastEvents(listViewModel)
|
||||||
|
|
||||||
binding.setBackClickListener {
|
binding.setBackClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,21 @@ package org.linphone.ui.main.recordings.model
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.media.AudioFocusRequestCompat
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
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.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.core.Factory
|
import org.linphone.core.Factory
|
||||||
import org.linphone.core.Player
|
|
||||||
import org.linphone.core.PlayerListener
|
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.AudioUtils
|
|
||||||
import org.linphone.utils.FileUtils
|
import org.linphone.utils.FileUtils
|
||||||
import org.linphone.utils.LinphoneUtils
|
import org.linphone.utils.LinphoneUtils
|
||||||
import org.linphone.utils.TimestampUtils
|
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 {
|
companion object {
|
||||||
private const val TAG = "[Recording Model]"
|
private const val TAG = "[Recording Model]"
|
||||||
}
|
}
|
||||||
|
|
@ -61,20 +55,6 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
|
||||||
|
|
||||||
val position = MutableLiveData<Int>()
|
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 {
|
init {
|
||||||
isPlaying.postValue(false)
|
isPlaying.postValue(false)
|
||||||
|
|
||||||
|
|
@ -108,13 +88,10 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
|
||||||
sipUri
|
sipUri
|
||||||
}
|
}
|
||||||
|
|
||||||
val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
|
val audioPlayer = coreContext.core.createLocalPlayer(null, null, null)
|
||||||
val audioPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
|
|
||||||
if (audioPlayer != null) {
|
if (audioPlayer != null) {
|
||||||
player = audioPlayer
|
audioPlayer.open(filePath)
|
||||||
player.open(filePath)
|
duration = audioPlayer.duration
|
||||||
player.addListener(playerListener)
|
|
||||||
duration = player.duration
|
|
||||||
formattedDuration = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
formattedDuration = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
||||||
} else {
|
} else {
|
||||||
duration = 0
|
duration = 0
|
||||||
|
|
@ -125,32 +102,15 @@ class RecordingModel @WorkerThread constructor(val filePath: String, val fileNam
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun destroy() {
|
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
|
@UiThread
|
||||||
fun togglePlayPause() {
|
fun togglePlayPause() {
|
||||||
coreContext.postOnCoreThread {
|
coreContext.postOnCoreThread {
|
||||||
if (isPlaying.value == true) {
|
if (isPlaying.value == true) {
|
||||||
pause()
|
onPause.invoke(this)
|
||||||
} else {
|
} 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]")
|
Log.i("$TAG Deleting call recording [$filePath]")
|
||||||
FileUtils.deleteFile(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.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.MutableLiveData
|
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.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.core.tools.Log
|
||||||
import org.linphone.ui.GenericViewModel
|
import org.linphone.ui.GenericViewModel
|
||||||
import org.linphone.ui.main.recordings.model.RecordingModel
|
import org.linphone.ui.main.recordings.model.RecordingModel
|
||||||
|
import org.linphone.utils.AudioUtils
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
import org.linphone.utils.FileUtils
|
import org.linphone.utils.FileUtils
|
||||||
|
|
||||||
|
|
@ -46,6 +56,18 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
|
||||||
MutableLiveData<Event<Boolean>>()
|
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 {
|
init {
|
||||||
searchBarVisible.value = false
|
searchBarVisible.value = false
|
||||||
fetchInProgress.value = true
|
fetchInProgress.value = true
|
||||||
|
|
@ -56,7 +78,13 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
if (currentlyPlayedRecording != null) {
|
||||||
|
stop(currentlyPlayedRecording)
|
||||||
|
player?.removeListener(playerListener)
|
||||||
|
player = null
|
||||||
|
}
|
||||||
recordings.value.orEmpty().forEach(RecordingModel::destroy)
|
recordings.value.orEmpty().forEach(RecordingModel::destroy)
|
||||||
|
|
||||||
super.onCleared()
|
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
|
@WorkerThread
|
||||||
private fun computeList(filter: String) {
|
private fun computeList(filter: String) {
|
||||||
recordings.value.orEmpty().forEach(RecordingModel::destroy)
|
recordings.value.orEmpty().forEach(RecordingModel::destroy)
|
||||||
|
|
@ -95,7 +218,18 @@ class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
|
||||||
val path = file.path
|
val path = file.path
|
||||||
val name = file.name
|
val name = file.name
|
||||||
Log.i("$TAG Found file $path")
|
Log.i("$TAG Found file $path")
|
||||||
list.add(RecordingModel(path, name))
|
list.add(
|
||||||
|
RecordingModel(
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
{ model ->
|
||||||
|
onRecordingStartedPlaying(model)
|
||||||
|
},
|
||||||
|
{ model ->
|
||||||
|
onRecordingPaused(model)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
list.sortBy {
|
list.sortBy {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue