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