Skip to content

Commit

Permalink
Progress track to simplify Slider integration (#240)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Défago <[email protected]>
  • Loading branch information
StaehliJ and defagos authored Sep 13, 2023
1 parent ddcd42d commit 93ad115
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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::userSeekFinished,
enabled = progressTracker.canSeek().value,
colors = sliderColors,
)
}

Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ fun Player.getPlaybackSpeed(): Float {
return playbackParameters.speed
}

/**
* Current position percent
*
* @return the current position in percent [0,1].
*/
fun Player.currentPositionPercentage(): 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.
Expand Down
114 changes: 114 additions & 0 deletions pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/ProgressTracker.kt
Original file line number Diff line number Diff line change
@@ -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.currentPositionPercentage
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 desired seek position.
*
* @property player The player whose current position must be tracked.
*/
@Stable
class ProgressTracker internal constructor(private val player: Player) {
private val playerProgressPercent: Flow<Float> = player.currentPositionAsFlow().map { player.currentPositionPercentage() }
private val userSeekState = MutableStateFlow<UserSeekState>(UserSeekState.Idle)
private val canSeek = player.availableCommandsAsFlow().map { it.canSeek() }
private val progressPercentFlow: Flow<Float> = 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<Float> = progressPercentFlow.collectAsState(initial = player.currentPositionPercentage())

/**
* Can seek
*
* @return can seek as State.
*/
@Composable
fun canSeek(): State<Boolean> = 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 finished seeking.
*/
fun userSeekFinished() {
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
}

0 comments on commit 93ad115

Please sign in to comment.