From bd522c8cf718e7d68b6c0d67868aea197a9cce9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Mon, 11 Sep 2023 10:11:00 +0200 Subject: [PATCH 1/4] Progress track to simplify Slider integration --- .../ui/player/controls/PlayerTimeSlider.kt | 70 +++-------- .../pillarbox/player/PillarboxPlayer.kt | 9 ++ .../ch/srgssr/pillarbox/ui/ProgressTracker.kt | 114 ++++++++++++++++++ 3 files changed, 137 insertions(+), 56 deletions(-) create mode 100644 pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index 8f96d6466..8b8d1b94e 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -9,75 +9,39 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.Player -import ch.srgssr.pillarbox.player.canSeek -import ch.srgssr.pillarbox.ui.availableCommandsAsState -import ch.srgssr.pillarbox.ui.currentPositionAsState -import ch.srgssr.pillarbox.ui.durationAsState +import ch.srgssr.pillarbox.ui.ProgressTracker +import ch.srgssr.pillarbox.ui.rememberProgressTracker /** * Player time slider * - * @param player The [StatefulPlayer] to observe. + * @param player The [Player] to observe. * @param modifier The modifier to be applied to the layout. + * @param sliderColors The slider colors to apply. + * @param progressTracker The progress track. * @param interactionSource The Slider interaction source. */ @Composable fun PlayerTimeSlider( player: Player, modifier: Modifier = Modifier, + sliderColors: SliderColors = playerCustomColors(), + progressTracker: ProgressTracker = rememberProgressTracker(player = player), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - TimeSlider( - modifier = modifier, - position = player.currentPositionAsState(), - duration = player.durationAsState(), - enabled = player.availableCommandsAsState().canSeek(), - interactionSource = interactionSource, - onSeek = { positionMs, finished -> - if (finished) { - player.seekTo(positionMs) - } - } - ) -} - -@Composable -private fun TimeSlider( - position: Long, - duration: Long, - modifier: Modifier = Modifier, - enabled: Boolean = false, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - onSeek: ((Long, Boolean) -> Unit)? = null, -) { - val progressPercentage = position / duration.coerceAtLeast(1).toFloat() - var sliderPosition by remember { mutableStateOf(0.0f) } - var isUserSeeking by remember { mutableStateOf(false) } - if (!isUserSeeking) { - sliderPosition = progressPercentage - } + val sliderPosition = progressTracker.progressPercent() Slider( - modifier = modifier, value = sliderPosition, + modifier = modifier, + value = sliderPosition.value, interactionSource = interactionSource, - onValueChange = { - isUserSeeking = true - sliderPosition = it - onSeek?.let { it1 -> it1((sliderPosition * duration).toLong(), false) } - }, - onValueChangeFinished = { - onSeek?.let { it((sliderPosition * duration).toLong(), true) } - isUserSeeking = false - }, - enabled = enabled, - colors = playerCustomColors(), + onValueChange = progressTracker::userSeek, + onValueChangeFinished = progressTracker::userSeekFinish, + enabled = progressTracker.canSeek().value, + colors = sliderColors, ) } @@ -89,9 +53,3 @@ private fun playerCustomColors(): SliderColors = SliderDefaults.colors( activeTrackColor = Color.White, thumbColor = Color.White ) - -@Preview(showBackground = false) -@Composable -fun TimeSliderPreview() { - TimeSlider(position = 34 * 3600 * 1000L, duration = 67 * 3600 * 1000L, enabled = true) -} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index 8aceaad15..897ca6819 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -181,6 +181,15 @@ fun Player.getPlaybackSpeed(): Float { return playbackParameters.speed } +/** + * Current position percent + * + * @return the current position in percent [0,1]. + */ +fun Player.currentPositionPercent(): Float { + return currentPosition / duration.coerceAtLeast(1).toFloat() +} + /** * Return if the playback [speed] is possible at [position]. * Always return true for none live content or if [Player.getCurrentTimeline] is empty. diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt new file mode 100644 index 000000000..c102ab492 --- /dev/null +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023. SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.media3.common.Player +import ch.srgssr.pillarbox.player.availableCommandsAsFlow +import ch.srgssr.pillarbox.player.canSeek +import ch.srgssr.pillarbox.player.currentPositionAsFlow +import ch.srgssr.pillarbox.player.currentPositionPercent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +/** + * Progress tracker + * + * Handle a progress position that is a mix of the player current position and the user seeked position. + * + * @property player The player to track current position. + */ +@Stable +class ProgressTracker internal constructor(private val player: Player) { + private val playerProgressPercent: Flow = player.currentPositionAsFlow().map { player.currentPositionPercent() } + private val userSeekState = MutableStateFlow(UserSeekState.Idle) + private val canSeek = player.availableCommandsAsFlow().map { it.canSeek() } + private val progressPercentFlow: Flow = combine(userSeekState, playerProgressPercent) { seekState, playerProgress -> + when (seekState) { + is UserSeekState.Seeking -> seekState.percent + else -> playerProgress + } + } + + /** + * Progress percent + * + * @return progress percent as State. + */ + @Composable + fun progressPercent(): State = progressPercentFlow.collectAsState(initial = player.currentPositionPercent()) + + /** + * Can seek + * + * @return can seek as State. + */ + @Composable + fun canSeek(): State = canSeek.collectAsState(initial = player.availableCommands.canSeek()) + + /** + * User seek at percent position + * + * @param percent Position in percent [0,1]. + */ + fun userSeek(percent: Float) { + userSeekState.value = UserSeekState.Seeking(percent) + } + + /** + * User has finish seeking. + */ + fun userSeekFinish() { + userSeekState.value.let { + if (it is UserSeekState.Seeking) { + userSeekState.value = UserSeekState.End(it.percent) + } + } + } + + internal suspend fun handleSeek() { + userSeekState.collectLatest { + when (it) { + is UserSeekState.End -> { + player.seekTo((it.percent * player.duration).toLong()) + } + + else -> { + // Nothing + } + } + } + } + + private sealed interface UserSeekState { + data object Idle : UserSeekState + data class Seeking(val percent: Float) : UserSeekState + data class End(val percent: Float) : UserSeekState + } +} + +/** + * Remember progress tracker + * + * @param player The player to observe. + */ +@Composable +fun rememberProgressTracker(player: Player): ProgressTracker { + val progressTracker = remember(player) { + ProgressTracker(player) + } + LaunchedEffect(progressTracker) { + progressTracker.handleSeek() + } + return progressTracker +} From 32e86a84a6f9579f8a787d617312f9a43d28d4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 13 Sep 2023 14:45:24 +0200 Subject: [PATCH 2/4] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Samuel Défago --- .../src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt index c102ab492..efcc279f5 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt @@ -24,9 +24,9 @@ import kotlinx.coroutines.flow.map /** * Progress tracker * - * Handle a progress position that is a mix of the player current position and the user seeked position. + * Handle a progress position that is a mix of the player current position and the user desired seek position. * - * @property player The player to track current position. + * @property player The player whose current position must be tracked. */ @Stable class ProgressTracker internal constructor(private val player: Player) { @@ -66,7 +66,7 @@ class ProgressTracker internal constructor(private val player: Player) { } /** - * User has finish seeking. + * User has finished seeking. */ fun userSeekFinish() { userSeekState.value.let { From d041f491115b618fbeed14cb4e342f165d2fc10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 13 Sep 2023 14:48:45 +0200 Subject: [PATCH 3/4] Rename to userSeekFinished --- .../pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt | 2 +- .../src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index 8b8d1b94e..bc36341bb 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -39,7 +39,7 @@ fun PlayerTimeSlider( value = sliderPosition.value, interactionSource = interactionSource, onValueChange = progressTracker::userSeek, - onValueChangeFinished = progressTracker::userSeekFinish, + onValueChangeFinished = progressTracker::userSeekFinished, enabled = progressTracker.canSeek().value, colors = sliderColors, ) diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt index efcc279f5..ed2bd49e9 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt @@ -68,7 +68,7 @@ class ProgressTracker internal constructor(private val player: Player) { /** * User has finished seeking. */ - fun userSeekFinish() { + fun userSeekFinished() { userSeekState.value.let { if (it is UserSeekState.Seeking) { userSeekState.value = UserSeekState.End(it.percent) From 31b8dd1001c98647c97b2b7395d9f8da846cdb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Wed, 13 Sep 2023 16:04:23 +0200 Subject: [PATCH 4/4] Rename method to currentPositionPercentage To fit media3 naming. --- .../main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt | 2 +- .../src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt index 897ca6819..fa4d74744 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt @@ -186,7 +186,7 @@ fun Player.getPlaybackSpeed(): Float { * * @return the current position in percent [0,1]. */ -fun Player.currentPositionPercent(): Float { +fun Player.currentPositionPercentage(): Float { return currentPosition / duration.coerceAtLeast(1).toFloat() } diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt index ed2bd49e9..508c9caff 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt @@ -14,7 +14,7 @@ import androidx.media3.common.Player import ch.srgssr.pillarbox.player.availableCommandsAsFlow import ch.srgssr.pillarbox.player.canSeek import ch.srgssr.pillarbox.player.currentPositionAsFlow -import ch.srgssr.pillarbox.player.currentPositionPercent +import ch.srgssr.pillarbox.player.currentPositionPercentage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.map */ @Stable class ProgressTracker internal constructor(private val player: Player) { - private val playerProgressPercent: Flow = player.currentPositionAsFlow().map { player.currentPositionPercent() } + private val playerProgressPercent: Flow = player.currentPositionAsFlow().map { player.currentPositionPercentage() } private val userSeekState = MutableStateFlow(UserSeekState.Idle) private val canSeek = player.availableCommandsAsFlow().map { it.canSeek() } private val progressPercentFlow: Flow = combine(userSeekState, playerProgressPercent) { seekState, playerProgress -> @@ -46,7 +46,7 @@ class ProgressTracker internal constructor(private val player: Player) { * @return progress percent as State. */ @Composable - fun progressPercent(): State = progressPercentFlow.collectAsState(initial = player.currentPositionPercent()) + fun progressPercent(): State = progressPercentFlow.collectAsState(initial = player.currentPositionPercentage()) /** * Can seek