new: display playback progress with a progress bar
This commit is contained in:
+10
-1
@@ -1,6 +1,7 @@
|
||||
package fr.ajaury.gwenedeg.player
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
@@ -38,7 +39,15 @@ class AndroidAudioPlayer(
|
||||
)
|
||||
}
|
||||
|
||||
override fun currentPosition(): Duration = player.currentPosition.coerceAtLeast(0).milliseconds
|
||||
override val currentPosition: Duration
|
||||
get() = player.currentPosition.coerceAtLeast(0).milliseconds
|
||||
|
||||
override val duration: Duration
|
||||
get() = player.duration
|
||||
.takeIf { it != C.TIME_UNSET }
|
||||
?.coerceAtLeast(0)
|
||||
?.milliseconds
|
||||
?: 0.milliseconds
|
||||
|
||||
override fun load(uri: String) {
|
||||
val mediaItem = MediaItem.fromUri(uri)
|
||||
|
||||
@@ -6,7 +6,9 @@ import kotlin.time.Duration
|
||||
interface AudioPlayer {
|
||||
val playbackState: StateFlow<PlaybackState>
|
||||
|
||||
fun currentPosition(): Duration
|
||||
val currentPosition: Duration
|
||||
|
||||
val duration: Duration
|
||||
|
||||
fun play()
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import platform.AVFoundation.AVPlayer
|
||||
import platform.AVFoundation.AVPlayerItem
|
||||
import platform.AVFoundation.AVPlayerItemDidPlayToEndTimeNotification
|
||||
import platform.AVFoundation.currentItem
|
||||
import platform.AVFoundation.currentTime
|
||||
import platform.AVFoundation.duration
|
||||
import platform.AVFoundation.pause
|
||||
import platform.AVFoundation.play
|
||||
import platform.AVFoundation.replaceCurrentItemWithPlayerItem
|
||||
@@ -28,11 +30,20 @@ class IosAudioPlayer : AudioPlayer {
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun currentPosition(): Duration {
|
||||
val player = player ?: return 0.milliseconds
|
||||
val seconds = CMTimeGetSeconds(player.currentTime()).takeIf { !it.isNaN() }
|
||||
return seconds?.seconds ?: 0.milliseconds
|
||||
}
|
||||
override val currentPosition: Duration
|
||||
get() {
|
||||
val player = player ?: return 0.milliseconds
|
||||
val seconds = CMTimeGetSeconds(player.currentTime()).takeIf { !it.isNaN() }
|
||||
return seconds?.seconds ?: 0.milliseconds
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override val duration: Duration
|
||||
get() {
|
||||
val item = player?.currentItem ?: return 0.milliseconds
|
||||
val seconds = CMTimeGetSeconds(item.duration).takeIf { it.isFinite() }
|
||||
return seconds?.seconds ?: 0.milliseconds
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
override fun load(uri: String) {
|
||||
|
||||
@@ -27,7 +27,11 @@ class JvmAudioPlayer : AudioPlayer {
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
override fun currentPosition(): Duration = (clip?.microsecondPosition ?: 0L).microseconds
|
||||
override val currentPosition: Duration
|
||||
get() = (clip?.microsecondPosition ?: 0L).microseconds
|
||||
|
||||
override val duration: Duration
|
||||
get() = (clip?.microsecondLength ?: 0L).microseconds
|
||||
|
||||
override fun load(uri: String) {
|
||||
release()
|
||||
|
||||
@@ -14,7 +14,11 @@ class WebAudioPlayer : AudioPlayer {
|
||||
private val _playbackState = MutableStateFlow(PlaybackState.IDLE)
|
||||
override val playbackState: StateFlow<PlaybackState> = _playbackState.asStateFlow()
|
||||
|
||||
override fun currentPosition(): Duration = (audio?.currentTime ?: 0.0).seconds
|
||||
override val currentPosition: Duration
|
||||
get() = (audio?.currentTime ?: 0.0).seconds
|
||||
|
||||
override val duration: Duration
|
||||
get() = (audio?.duration?.takeIf { it.isFinite() } ?: 0.0).seconds
|
||||
|
||||
override fun load(uri: String) {
|
||||
release()
|
||||
|
||||
@@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -16,6 +18,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@@ -85,7 +88,8 @@ fun PlayerScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
@@ -97,9 +101,11 @@ fun PlayerScreen(
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${uiState.positionMs} ms",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { uiState.progress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Button(
|
||||
@@ -137,7 +143,7 @@ private fun PlayerScreenPlayingPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
recordTitle = "Demat",
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234),
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
@@ -150,7 +156,7 @@ private fun PlayerScreenPausedPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
recordTitle = "Demat",
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234),
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
@@ -163,7 +169,7 @@ private fun PlayerScreenEndedPreview() {
|
||||
GwenedegTheme {
|
||||
PlayerScreen(
|
||||
recordTitle = "Demat",
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234),
|
||||
uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000),
|
||||
onMainPlayActionButtonClicked = {},
|
||||
onBackClicked = {},
|
||||
)
|
||||
|
||||
+5
-1
@@ -5,4 +5,8 @@ import fr.ajaury.gwenedeg.player.PlaybackState
|
||||
data class PlayerUiState(
|
||||
val playbackState: PlaybackState = PlaybackState.IDLE,
|
||||
val positionMs: Long = 0L,
|
||||
)
|
||||
val durationMs: Long = 0L,
|
||||
) {
|
||||
val progress: Float
|
||||
get() = if (durationMs > 0) (positionMs.toFloat() / durationMs).coerceIn(0f, 1f) else 0f
|
||||
}
|
||||
|
||||
+4
-3
@@ -35,12 +35,12 @@ class PlayerViewModel(
|
||||
if (state == PlaybackState.PLAYING) {
|
||||
flow {
|
||||
while (true) {
|
||||
emit(audioPlayer.currentPosition())
|
||||
emit(audioPlayer.currentPosition)
|
||||
delay(POSITION_POLL_INTERVAL)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flowOf(audioPlayer.currentPosition())
|
||||
flowOf(audioPlayer.currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class PlayerViewModel(
|
||||
PlayerUiState(
|
||||
playbackState = playbackState,
|
||||
positionMs = currentPositionMs.inWholeMilliseconds,
|
||||
durationMs = audioPlayer.duration.inWholeMilliseconds,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
@@ -91,6 +92,6 @@ class PlayerViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val POSITION_POLL_INTERVAL = 200.milliseconds
|
||||
private val POSITION_POLL_INTERVAL = 50.milliseconds
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user