Voice message scrubbing improvements (#1847)

- Voice messages can be scrubbed (i.e. seeked to) even when they have not been played yet..
- The progress bar is displayed also when paused.
- Multiple voice messages can keep their state when paused.
- Tries to adhere as much as possible at the detailed "green cursor" behavior in the story (but might not be 100% compliant).

Story: https://github.com/vector-im/element-meta/issues/2113
This commit is contained in:
Marco Romano
2023-11-21 20:48:08 +01:00
committed by GitHub
parent 6b467d95a7
commit eb2836f42b
11 changed files with 381 additions and 208 deletions

View File

@@ -38,6 +38,7 @@ interface MediaPlayer : AutoCloseable {
uri: String,
mediaId: String,
mimeType: String,
startPositionMs: Long = 0,
): State
/**
@@ -69,6 +70,10 @@ interface MediaPlayer : AutoCloseable {
* Whether the player is currently playing.
*/
val isPlaying: Boolean,
/**
* Whether the player has reached the end of the current media.
*/
val isEnded: Boolean,
/**
* The id of the media which is currently playing.
*

View File

@@ -77,6 +77,7 @@ class MediaPlayerImpl @Inject constructor(
_state.update {
it.copy(
isReady = playbackState == Player.STATE_READY,
isEnded = playbackState == Player.STATE_ENDED,
currentPosition = player.currentPosition,
duration = duration,
)
@@ -95,16 +96,22 @@ class MediaPlayerImpl @Inject constructor(
MediaPlayer.State(
isReady = false,
isPlaying = false,
isEnded = false,
mediaId = null,
currentPosition = 0L,
duration = 0L
duration = null,
)
)
override val state: StateFlow<MediaPlayer.State> = _state.asStateFlow()
@OptIn(FlowPreview::class)
override suspend fun setMedia(uri: String, mediaId: String, mimeType: String): MediaPlayer.State {
override suspend fun setMedia(
uri: String,
mediaId: String,
mimeType: String,
startPositionMs: Long,
): 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(
@@ -112,7 +119,8 @@ class MediaPlayerImpl @Inject constructor(
.setUri(uri)
.setMediaId(mediaId)
.setMimeType(mimeType)
.build()
.build(),
startPositionMs,
)
player.prepare()
// Will throw TimeoutCancellationException if the player is not ready after 1 second.
@@ -129,7 +137,7 @@ class MediaPlayerImpl @Inject constructor(
// playing no sound.
// This is a workaround which will reload the media file.
player.getCurrentMediaItem()?.let {
player.setMediaItem(it)
player.setMediaItem(it, 0)
player.prepare()
player.play()
}

View File

@@ -35,7 +35,7 @@ interface SimplePlayer {
val playbackState: Int
val duration: Long
fun clearMediaItems()
fun setMediaItem(mediaItem: MediaItem)
fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long)
fun getCurrentMediaItem(): MediaItem?
fun prepare()
fun play()
@@ -81,7 +81,7 @@ class SimplePlayerImpl(
override fun clearMediaItems() = p.clearMediaItems()
override fun setMediaItem(mediaItem: MediaItem) = p.setMediaItem(mediaItem)
override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) = p.setMediaItem(mediaItem, startPositionMs)
override fun getCurrentMediaItem(): MediaItem? = p.currentMediaItem

View File

@@ -26,31 +26,37 @@ import kotlinx.coroutines.flow.update
/**
* Fake implementation of [MediaPlayer] for testing purposes.
*/
class FakeMediaPlayer : MediaPlayer {
companion object {
private const val FAKE_TOTAL_DURATION_MS = 10_000L
private const val FAKE_PLAYED_DURATION_MS = 1000L
}
class FakeMediaPlayer(
private val fakeTotalDurationMs: Long = 10_000L,
private val fakePlayedDurationMs: Long = 1000L,
) : MediaPlayer {
private val _state = MutableStateFlow(
MediaPlayer.State(
isReady = false,
isPlaying = false,
isEnded = false,
mediaId = null,
currentPosition = 0L,
duration = 0L
duration = null
)
)
override val state: StateFlow<MediaPlayer.State> = _state.asStateFlow()
override suspend fun setMedia(uri: String, mediaId: String, mimeType: String): MediaPlayer.State {
override suspend fun setMedia(
uri: String,
mediaId: String,
mimeType: String,
startPositionMs: Long,
): MediaPlayer.State {
_state.update {
it.copy(
isReady = false,
isPlaying = false,
isEnded = false,
mediaId = mediaId,
currentPosition = 0,
currentPosition = startPositionMs,
duration = null,
)
}
@@ -58,7 +64,7 @@ class FakeMediaPlayer : MediaPlayer {
_state.update {
it.copy(
isReady = true,
duration = FAKE_TOTAL_DURATION_MS,
duration = fakeTotalDurationMs,
)
}
return _state.value
@@ -66,10 +72,20 @@ class FakeMediaPlayer : MediaPlayer {
override fun play() {
_state.update {
it.copy(
isPlaying = true,
currentPosition = it.currentPosition + FAKE_PLAYED_DURATION_MS,
)
val newPosition = it.currentPosition + fakePlayedDurationMs
if (newPosition < fakeTotalDurationMs) {
it.copy(
isPlaying = true,
currentPosition = newPosition,
)
} else {
it.copy(
isReady = false,
isPlaying = false,
isEnded = true,
currentPosition = fakeTotalDurationMs,
)
}
}
}