Voice message MediaPlayer: wait until player is ready (#1772)

Change to `MediaPlayer` API to allow waiting for the player to be in a ready state.
This is needed in order to perform some tasks (e.g. read the media duration, seek) after changing the media file.
This commit is contained in:
Marco Romano 2023-11-09 15:34:38 +01:00 committed by GitHub
parent b83d8733e2
commit 878417f557
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 110 additions and 28 deletions

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.mediaplayer.impl
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import com.squareup.anvil.annotations.ContributesBinding
@ -24,14 +25,18 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/**
* Default implementation of [MediaPlayer] backed by a [SimplePlayer].
@ -47,7 +52,7 @@ class MediaPlayerImpl @Inject constructor(
_state.update {
it.copy(
currentPosition = player.currentPosition,
duration = player.duration.coerceAtLeast(0),
duration = duration,
isPlaying = isPlaying,
)
}
@ -62,11 +67,21 @@ class MediaPlayerImpl @Inject constructor(
_state.update {
it.copy(
currentPosition = player.currentPosition,
duration = player.duration.coerceAtLeast(0),
duration = duration,
mediaId = mediaItem?.mediaId,
)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
_state.update {
it.copy(
isReady = playbackState == Player.STATE_READY,
currentPosition = player.currentPosition,
duration = duration,
)
}
}
}
init {
@ -76,11 +91,21 @@ class MediaPlayerImpl @Inject constructor(
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private var job: Job? = null
private val _state = MutableStateFlow(MediaPlayer.State(false, null, 0L, 0L))
private val _state = MutableStateFlow(
MediaPlayer.State(
isReady = false,
isPlaying = false,
mediaId = null,
currentPosition = 0L,
duration = 0L
)
)
override val state: StateFlow<MediaPlayer.State> = _state.asStateFlow()
override fun acquireControlAndPlay(uri: String, mediaId: String, mimeType: String) {
@OptIn(FlowPreview::class)
override suspend fun setMedia(uri: String, mediaId: String, mimeType: String): MediaPlayer.State {
player.pause() // Must pause here otherwise if the player was playing it would keep on playing the new media item.
player.clearMediaItems()
player.setMediaItem(
MediaItem.Builder()
@ -90,7 +115,8 @@ class MediaPlayerImpl @Inject constructor(
.build()
)
player.prepare()
player.play()
// Will throw TimeoutCancellationException if the player is not ready after 1 second.
return state.timeout(1.seconds).first { it.isReady }
}
override fun play() {
@ -136,4 +162,9 @@ class MediaPlayerImpl @Inject constructor(
}
}
}
private val duration: Long?
get() = player.duration.let {
if (it == C.TIME_UNSET) null else it
}
}

View file

@ -45,6 +45,7 @@ interface SimplePlayer {
interface Listener {
fun onIsPlayingChanged(isPlaying: Boolean)
fun onMediaItemTransition(mediaItem: MediaItem?)
fun onPlaybackStateChanged(playbackState: Int)
}
}
@ -67,6 +68,7 @@ class SimplePlayerImpl(
p.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) = listener.onIsPlayingChanged(isPlaying)
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) = listener.onMediaItemTransition(mediaItem)
override fun onPlaybackStateChanged(playbackState: Int) = listener.onPlaybackStateChanged(playbackState)
})
}