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:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user