make fastseek work

This commit is contained in:
Christian Schabesberger 2024-08-06 17:06:24 +02:00
parent ae3ef47a3f
commit 990a4aaa12
10 changed files with 312 additions and 19 deletions

View File

@ -45,7 +45,7 @@ interface NewPlayer {
val bufferedPercentage: Int
val repository: MediaRepository
var currentPosition: Long
var fastSeekAmountSec: Long
var fastSeekAmountSec: Int
var playBackMode: PlayMode
var playList: MutableList<String>
@ -54,6 +54,7 @@ interface NewPlayer {
fun pause()
fun fastSeekForward()
fun fastSeekBackward()
fun seekTo(millisecond: Long)
fun addToPlaylist(newItem: String)
fun addListener(callbackListener: Listener)
@ -88,7 +89,7 @@ class NewPlayerImpl(override val internal_player: Player, override val repositor
override val duartion: Long = internal_player.duration
override val bufferedPercentage: Int = internal_player.bufferedPercentage
override var currentPosition: Long = internal_player.currentPosition
override var fastSeekAmountSec: Long = 100
override var fastSeekAmountSec: Int = 10
override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
override var playList: MutableList<String> = ArrayList<String>()
@ -115,11 +116,17 @@ class NewPlayerImpl(override val internal_player: Player, override val repositor
}
override fun fastSeekForward() {
Log.d(TAG, "not implemented fast seek forward")
val currentPosition = internal_player.currentPosition
internal_player.seekTo(currentPosition + fastSeekAmountSec * 1000)
}
override fun fastSeekBackward() {
Log.d(TAG, "not implemented fast seek backward")
val currentPosition = internal_player.currentPosition
internal_player.seekTo(currentPosition - fastSeekAmountSec * 1000)
}
override fun seekTo(millisecond: Long) {
internal_player.seekTo(millisecond)
}
override fun addToPlaylist(newItem: String) {

View File

@ -37,7 +37,8 @@ data class VideoPlayerUIState(
val bufferedPercentage: Float,
val isLoading: Boolean,
val durationInMs: Long,
val playbackPositionInMs: Long
val playbackPositionInMs: Long,
val fastseekSeconds: Int,
) : Parcelable {
companion object {
val DEFAULT = VideoPlayerUIState(
@ -52,7 +53,8 @@ data class VideoPlayerUIState(
bufferedPercentage = 0f,
isLoading = true,
durationInMs = 0,
playbackPositionInMs = 0
playbackPositionInMs = 0,
fastseekSeconds = 10
)
}
}

View File

@ -65,6 +65,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
set(value) {
field = value
installExoPlayer()
mutableUiState.update { it.copy(fastseekSeconds = field?.fastSeekAmountSec ?: 10) }
}
override val uiState = mutableUiState.asStateFlow()
@ -259,7 +260,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
resetHideUiDelayedJob()
val seekerPosition = mutableUiState.value.seekerPosition
val seekPositionInMs = (player?.duration?.toFloat() ?: 0F) * seekerPosition
player?.seekTo(seekPositionInMs.toLong())
newPlayer?.seekTo(seekPositionInMs.toLong())
Log.i(TAG, "Seek to Ms: $seekPositionInMs")
}
@ -268,6 +269,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
override fun fastSeekForward() {
mutableUiState.update { it.copy(fastseekSeconds = newPlayer?.fastSeekAmountSec ?: 10) }
newPlayer?.fastSeekForward()
if (mutableUiState.value.uiVisible) {
resetHideUiDelayedJob()
@ -275,6 +277,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
override fun fastSeekBackward() {
mutableUiState.update { it.copy(fastseekSeconds = newPlayer?.fastSeekAmountSec ?: 10) }
newPlayer?.fastSeekBackward()
if (mutableUiState.value.uiVisible) {
resetHideUiDelayedJob()

View File

@ -62,6 +62,7 @@ fun VideoPlayerControllerUI(
durationInMs: Long,
playbackPositionInMs: Long,
bufferedPercentage: Float,
fastSeekSeconds: Int,
play: () -> Unit,
pause: () -> Unit,
prevStream: () -> Unit,
@ -101,7 +102,8 @@ fun VideoPlayerControllerUI(
switchToEmbeddedView = switchToEmbeddedView,
embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeekForward = fastSeekForward,
fastSeekBackward = fastSeekBackward
fastSeekBackward = fastSeekBackward,
fastSeekSeconds = fastSeekSeconds
)
}
@ -136,7 +138,8 @@ fun VideoPlayerControllerUI(
switchToEmbeddedView = switchToEmbeddedView,
embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeekForward = fastSeekForward,
fastSeekBackward = fastSeekBackward
fastSeekBackward = fastSeekBackward,
fastSeekSeconds = fastSeekSeconds
)
Box(modifier = Modifier.fillMaxSize()) {
@ -219,6 +222,7 @@ fun VideoPlayerControllerUIPreviewEmbedded() {
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
bufferedPercentage = 0.4f,
fastSeekSeconds = 10,
play = {},
pause = {},
prevStream = {},
@ -249,6 +253,7 @@ fun VideoPlayerControllerUIPreviewLandscape() {
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
bufferedPercentage = 0.4f,
fastSeekSeconds = 10,
play = {},
pause = {},
prevStream = {},
@ -280,6 +285,7 @@ fun VideoPlayerControllerUIPreviewPortrait() {
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
bufferedPercentage = 0.4f,
fastSeekSeconds = 10,
play = {},
pause = {},
prevStream = {},

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@ -30,6 +31,7 @@ fun VideoPlayerLoadingPlaceholder(aspectRatio: Float = 3F / 1F) {
.height(64.dp)
.align((Alignment.Center)))
}
}
}

View File

@ -152,6 +152,7 @@ fun VideoPlayerUI(
durationInMs = uiState.durationInMs,
playbackPositionInMs = uiState.playbackPositionInMs,
bufferedPercentage = uiState.bufferedPercentage,
fastSeekSeconds = uiState.fastseekSeconds,
play = viewModel::play,
pause = viewModel::pause,
prevStream = viewModel::prevStream,

View File

@ -83,6 +83,7 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
durationInMs = 9*60*1000,
playbackPositionInMs = 6*60*1000,
bufferedPercentage = 0.4f,
fastSeekSeconds = 10,
play = {},
pause = {},
prevStream = {},

View File

@ -22,23 +22,43 @@ package net.newpipe.newplayer.ui.videoplayer
import android.util.Log
import android.view.MotionEvent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
private const val TAG = "TouchUi"
@ -47,7 +67,10 @@ private data class TouchedPosition(val x: Float, val y: Float) {
}
const val DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS: Long = 200
const val SEEK_ANIMATION_DURATION_IN_MS = 400
const val SEEK_ANIMATION_VISSIBLE_IN_MS = 500L
const val SEEK_ANIMATION_FADE_IN = 200
const val SEEK_ANIMATION_FADE_OUT = 500
@Composable
fun GestureUI(
@ -56,6 +79,7 @@ fun GestureUI(
showUi: () -> Unit,
uiVissible: Boolean,
fullscreen: Boolean,
fastSeekSeconds: Int,
switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit,
embeddedDraggedDownBy: (Float) -> Unit,
@ -70,14 +94,53 @@ fun GestureUI(
}
}
var showFastSeekBack by remember {
mutableStateOf(false)
}
var showFastSeekForward by remember {
mutableStateOf(false)
}
val composeScope = rememberCoroutineScope()
val doForwardSeek = {
showFastSeekForward = true
composeScope.launch {
delay(SEEK_ANIMATION_VISSIBLE_IN_MS)
showFastSeekForward = false
}
fastSeekForward()
}
val doBackwardSeek = {
showFastSeekBack = true
composeScope.launch {
delay(SEEK_ANIMATION_VISSIBLE_IN_MS)
showFastSeekBack = false
}
fastSeekBackward()
}
if (fullscreen) {
Row(modifier = modifier) {
TouchSurface(
modifier = Modifier
.weight(1f),
onRegularTap = defaultOnRegularTap,
onDoubleTab = fastSeekBackward
onDoubleTab = doBackwardSeek
) {
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
seconds = fastSeekSeconds,
backwards = true,
modifier = Modifier.align(Alignment.CenterEnd)
)
}
}
}
TouchSurface(
modifier = Modifier
.weight(1f),
@ -92,9 +155,19 @@ fun GestureUI(
modifier = Modifier
.weight(1f),
onRegularTap = defaultOnRegularTap,
onDoubleTab = fastSeekForward
onDoubleTab = doForwardSeek
) {
FadedAnimationForSeekFeedback(visible = showFastSeekForward) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
modifier = Modifier.align(Alignment.CenterStart),
seconds = fastSeekSeconds,
backwards = false
)
}
}
}
}
} else { // (!fullscreen)
val handleDownwardMovement = { movement: TouchedPosition ->
Log.d(TAG, "${movement.x}:${movement.y}")
@ -109,20 +182,51 @@ fun GestureUI(
TouchSurface(
modifier = Modifier
.weight(1f),
onDoubleTab = fastSeekBackward,
onDoubleTab = doBackwardSeek,
onRegularTap = defaultOnRegularTap,
onMovement = handleDownwardMovement
) {
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
modifier = Modifier.align(Alignment.Center),
seconds = fastSeekSeconds,
backwards = true
)
}
}
}
TouchSurface(
modifier = Modifier
.weight(1f),
onDoubleTab = fastSeekForward,
onDoubleTab = doForwardSeek,
onRegularTap = defaultOnRegularTap,
onMovement = handleDownwardMovement
) {
FadedAnimationForSeekFeedback(visible = showFastSeekForward) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
modifier = Modifier.align(Alignment.Center),
seconds = fastSeekSeconds,
backwards = false
)
}
}
}
}
}
}
@Composable
fun FadedAnimationForSeekFeedback(visible: Boolean, content: @Composable () -> Unit) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(SEEK_ANIMATION_FADE_IN)),
exit = fadeOut(animationSpec = tween(SEEK_ANIMATION_FADE_OUT))
) {
content()
}
}
@Composable
@OptIn(ExperimentalComposeUiApi::class)
@ -131,7 +235,8 @@ private fun TouchSurface(
color: Color = Color.Transparent,
onDoubleTab: () -> Unit = {},
onRegularTap: () -> Unit = {},
onMovement: (TouchedPosition) -> Unit = {}
onMovement: (TouchedPosition) -> Unit = {},
content: @Composable () -> Unit = {}
) {
var moveOccured by remember {
mutableStateOf(false)
@ -194,6 +299,169 @@ private fun TouchSurface(
else -> false
}
}) {
content()
Surface(color = color, modifier = Modifier.fillMaxSize()) {}
}
}
@Composable
fun FastSeekVisualFeedback(modifier: Modifier = Modifier, seconds: Int, backwards: Boolean) {
val contentDescription = String.format(
if (backwards) {
"Fast seeking backward by %d seconds."
//stringResource(id = R.string.fast_seeking_backward)
} else {
"Fast seeking forward by %d seconds."
//stringResource(id = R.string.fast_seeking_forward)
}, seconds
)
val infiniteTransition = rememberInfiniteTransition()
val animatedColor1 by infiniteTransition.animateColor(
initialValue = Color.White,
targetValue = Color.Transparent,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
Color.White.copy(alpha = 1f) at 0 with LinearEasing
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
},
repeatMode = RepeatMode.Restart
), label = "Arrow1 animation"
)
val animatedColor2 by infiniteTransition.animateColor(
initialValue = Color.White,
targetValue = Color.Transparent,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
Color.White.copy(alpha = 1f / 3f) at 0 with LinearEasing
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
Color.White.copy(alpha = 1f) at SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
},
repeatMode = RepeatMode.Restart
), label = "Arrow2 animation"
)
val animatedColor3 by infiniteTransition.animateColor(
initialValue = Color.White,
targetValue = Color.Transparent,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
Color.White.copy(alpha = 2f / 3f) at 0 with LinearEasing
Color.White.copy(alpha = 0f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
Color.White.copy(alpha = 1f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
},
repeatMode = RepeatMode.Restart
), label = "Arrow3 animation"
)
//val secondsString = stringResource(id = R.string.seconds)
val secondsString = "Seconds"
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Row {
SeekerIcon(
backwards = backwards,
description = contentDescription,
color = if (backwards) animatedColor3 else animatedColor1
)
SeekerIcon(
backwards = backwards,
description = contentDescription,
color = animatedColor2
)
SeekerIcon(
backwards = backwards,
description = contentDescription,
color = if (backwards) animatedColor1 else animatedColor3
)
}
Text(text = "$seconds $secondsString")
}
}
@Composable
fun SeekerIcon(backwards: Boolean, description: String, color: Color) {
Icon(
modifier = if (backwards) {
Modifier.scale(-1f, 1f)
} else {
Modifier
},
tint = color,
painter = painterResource(id = R.drawable.ic_play_seek_triangle),
contentDescription = description
)
}
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
fun FullscreenGestureUIPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
GestureUI(
modifier = Modifier,
hideUi = { },
showUi = { },
uiVissible = false,
fullscreen = true,
fastSeekSeconds = 10,
switchToFullscreen = { println("switch to fullscreen") },
switchToEmbeddedView = { println("switch to embedded") },
embeddedDraggedDownBy = { println("embedded dragged down") },
fastSeekBackward = { println("fast seek backward") },
fastSeekForward = { println("fast seek forward") })
}
}
}
@Preview(device = "spec:width=600px,height=400px,dpi=440,orientation=landscape")
@Composable
fun EmbeddedGestureUIPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
GestureUI(
modifier = Modifier,
hideUi = { },
showUi = { },
uiVissible = false,
fullscreen = false,
fastSeekSeconds = 10,
switchToFullscreen = { println("switch to fullscreen") },
switchToEmbeddedView = { println("switch to embedded") },
embeddedDraggedDownBy = { println("embedded dragged down") },
fastSeekBackward = { println("fast seek backward") },
fastSeekForward = { println("fast seek forward") })
}
}
}
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
fun FastSeekVisualFeedbackPreviewBackwards() {
VideoPlayerTheme {
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
FastSeekVisualFeedback(seconds = 10, backwards = true)
}
}
}
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
@Composable
fun FastSeekVisualFeedbackPreview() {
VideoPlayerTheme {
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
FastSeekVisualFeedback(seconds = 10, backwards = false)
}
}
}

View File

@ -35,4 +35,7 @@
<string name="widget_description_toggle_fullscreen">Toggle fullscreen</string>
<string name="widget_description_chapter_selection">Chapter selection</string>
<string name="widget_descriptoin_playlist_item_selection">Playlist item selection</string>
<string name="fast_seeking_backward">Fast seeking backward by %d seconds.</string>
<string name="fast_seeking_forward">Fast seeking forward by %d seconds.</string>
<string name="seconds">Seconds</string>
</resources>