new: allow user to seek a position in the record

This commit is contained in:
2026-06-19 13:53:10 +02:00
parent 511e3b5b9a
commit 7720a71825
7 changed files with 48 additions and 3 deletions
@@ -49,6 +49,10 @@ class AndroidAudioPlayer(
?.milliseconds ?.milliseconds
?: 0.milliseconds ?: 0.milliseconds
override fun seekTo(position: Duration) {
player.seekTo(position.inWholeMilliseconds)
}
override fun load(uri: String) { override fun load(uri: String) {
val mediaItem = MediaItem.fromUri(uri) val mediaItem = MediaItem.fromUri(uri)
player.setMediaItem(mediaItem) player.setMediaItem(mediaItem)
@@ -10,6 +10,8 @@ interface AudioPlayer {
val duration: Duration val duration: Duration
fun seekTo(position: Duration)
fun play() fun play()
fun pause() fun pause()
@@ -13,7 +13,9 @@ 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
import platform.AVFoundation.seekToTime
import platform.CoreMedia.CMTimeGetSeconds import platform.CoreMedia.CMTimeGetSeconds
import platform.CoreMedia.CMTimeMakeWithSeconds
import platform.Foundation.NSNotificationCenter import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSOperationQueue import platform.Foundation.NSOperationQueue
import platform.Foundation.NSURL import platform.Foundation.NSURL
@@ -45,6 +47,12 @@ class IosAudioPlayer : AudioPlayer {
return seconds?.seconds ?: 0.milliseconds return seconds?.seconds ?: 0.milliseconds
} }
@OptIn(ExperimentalForeignApi::class)
override fun seekTo(position: Duration) {
val seconds = position.inWholeSeconds
player?.seekToTime(CMTimeMakeWithSeconds(seconds.toDouble(), preferredTimescale = 1000))
}
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
override fun load(uri: String) { override fun load(uri: String) {
release() release()
@@ -33,6 +33,10 @@ class JvmAudioPlayer : AudioPlayer {
override val duration: Duration override val duration: Duration
get() = (clip?.microsecondLength ?: 0L).microseconds get() = (clip?.microsecondLength ?: 0L).microseconds
override fun seekTo(position: Duration) {
clip?.microsecondPosition = position.inWholeMicroseconds
}
override fun load(uri: String) { override fun load(uri: String) {
release() release()
@@ -20,6 +20,10 @@ class WebAudioPlayer : AudioPlayer {
override val duration: Duration override val duration: Duration
get() = (audio?.duration?.takeIf { it.isFinite() } ?: 0.0).seconds get() = (audio?.duration?.takeIf { it.isFinite() } ?: 0.0).seconds
override fun seekTo(position: Duration) {
audio?.currentTime = position.inWholeMicroseconds.toDouble()
}
override fun load(uri: String) { override fun load(uri: String) {
release() release()
@@ -18,14 +18,17 @@ 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.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@@ -58,6 +61,7 @@ fun PlayerScreen(
recordTitle = recordTitle, recordTitle = recordTitle,
uiState = uiState, uiState = uiState,
onMainPlayActionButtonClicked = viewModel::performMainPlayAction, onMainPlayActionButtonClicked = viewModel::performMainPlayAction,
onSeek = viewModel::seekTo,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
) )
} }
@@ -68,6 +72,7 @@ fun PlayerScreen(
recordTitle: String, recordTitle: String,
uiState: PlayerUiState, uiState: PlayerUiState,
onMainPlayActionButtonClicked: () -> Unit, onMainPlayActionButtonClicked: () -> Unit,
onSeek: (progress: Float) -> Unit,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
) { ) {
Scaffold( Scaffold(
@@ -103,8 +108,18 @@ fun PlayerScreen(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
LinearProgressIndicator( var seekProgress by remember { mutableStateOf<Float?>(null) }
progress = { uiState.progress }, Slider(
value = seekProgress ?: uiState.progress,
onValueChange = { seekProgress = it },
onValueChangeFinished = {
seekProgress?.let { fraction ->
if (uiState.durationMs > 0) {
onSeek(fraction)
}
}
seekProgress = null
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@@ -145,6 +160,7 @@ private fun PlayerScreenPlayingPreview() {
recordTitle = "Demat", recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000), uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000),
onMainPlayActionButtonClicked = {}, onMainPlayActionButtonClicked = {},
onSeek = {},
onBackClicked = {}, onBackClicked = {},
) )
} }
@@ -158,6 +174,7 @@ private fun PlayerScreenPausedPreview() {
recordTitle = "Demat", recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000), uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000),
onMainPlayActionButtonClicked = {}, onMainPlayActionButtonClicked = {},
onSeek = {},
onBackClicked = {}, onBackClicked = {},
) )
} }
@@ -171,6 +188,7 @@ private fun PlayerScreenEndedPreview() {
recordTitle = "Demat", recordTitle = "Demat",
uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000), uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000),
onMainPlayActionButtonClicked = {}, onMainPlayActionButtonClicked = {},
onSeek = {},
onBackClicked = {}, onBackClicked = {},
) )
} }
@@ -81,6 +81,11 @@ class PlayerViewModel(
} }
} }
fun seekTo(progress: Float) {
val position = (progress.toDouble() * uiState.value.durationMs).milliseconds
audioPlayer.seekTo(position)
}
fun stop() { fun stop() {
audioPlayer.stop() audioPlayer.stop()
} }