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 bufferedPercentage: Int
val repository: MediaRepository val repository: MediaRepository
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Long var fastSeekAmountSec: Int
var playBackMode: PlayMode var playBackMode: PlayMode
var playList: MutableList<String> var playList: MutableList<String>
@ -54,6 +54,7 @@ interface NewPlayer {
fun pause() fun pause()
fun fastSeekForward() fun fastSeekForward()
fun fastSeekBackward() fun fastSeekBackward()
fun seekTo(millisecond: Long)
fun addToPlaylist(newItem: String) fun addToPlaylist(newItem: String)
fun addListener(callbackListener: Listener) 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 duartion: Long = internal_player.duration
override val bufferedPercentage: Int = internal_player.bufferedPercentage override val bufferedPercentage: Int = internal_player.bufferedPercentage
override var currentPosition: Long = internal_player.currentPosition 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 playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
override var playList: MutableList<String> = ArrayList<String>() override var playList: MutableList<String> = ArrayList<String>()
@ -115,11 +116,17 @@ class NewPlayerImpl(override val internal_player: Player, override val repositor
} }
override fun fastSeekForward() { 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() { 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) { override fun addToPlaylist(newItem: String) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,23 +22,43 @@ package net.newpipe.newplayer.ui.videoplayer
import android.util.Log import android.util.Log
import android.view.MotionEvent 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize 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.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter 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.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
private const val TAG = "TouchUi" 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 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 @Composable
fun GestureUI( fun GestureUI(
@ -56,6 +79,7 @@ fun GestureUI(
showUi: () -> Unit, showUi: () -> Unit,
uiVissible: Boolean, uiVissible: Boolean,
fullscreen: Boolean, fullscreen: Boolean,
fastSeekSeconds: Int,
switchToFullscreen: () -> Unit, switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit, switchToEmbeddedView: () -> Unit,
embeddedDraggedDownBy: (Float) -> 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) { if (fullscreen) {
Row(modifier = modifier) { Row(modifier = modifier) {
TouchSurface( TouchSurface(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
onRegularTap = defaultOnRegularTap, onRegularTap = defaultOnRegularTap,
onDoubleTab = fastSeekBackward onDoubleTab = doBackwardSeek
) {
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
seconds = fastSeekSeconds,
backwards = true,
modifier = Modifier.align(Alignment.CenterEnd)
) )
}
}
}
TouchSurface( TouchSurface(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
@ -92,9 +155,19 @@ fun GestureUI(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
onRegularTap = defaultOnRegularTap, 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) } else { // (!fullscreen)
val handleDownwardMovement = { movement: TouchedPosition -> val handleDownwardMovement = { movement: TouchedPosition ->
Log.d(TAG, "${movement.x}:${movement.y}") Log.d(TAG, "${movement.x}:${movement.y}")
@ -109,20 +182,51 @@ fun GestureUI(
TouchSurface( TouchSurface(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
onDoubleTab = fastSeekBackward, onDoubleTab = doBackwardSeek,
onRegularTap = defaultOnRegularTap, onRegularTap = defaultOnRegularTap,
onMovement = handleDownwardMovement onMovement = handleDownwardMovement
) {
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
Box(modifier = Modifier.fillMaxSize()) {
FastSeekVisualFeedback(
modifier = Modifier.align(Alignment.Center),
seconds = fastSeekSeconds,
backwards = true
) )
}
}
}
TouchSurface( TouchSurface(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f),
onDoubleTab = fastSeekForward, onDoubleTab = doForwardSeek,
onRegularTap = defaultOnRegularTap, onRegularTap = defaultOnRegularTap,
onMovement = handleDownwardMovement 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 @Composable
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@ -131,7 +235,8 @@ private fun TouchSurface(
color: Color = Color.Transparent, color: Color = Color.Transparent,
onDoubleTab: () -> Unit = {}, onDoubleTab: () -> Unit = {},
onRegularTap: () -> Unit = {}, onRegularTap: () -> Unit = {},
onMovement: (TouchedPosition) -> Unit = {} onMovement: (TouchedPosition) -> Unit = {},
content: @Composable () -> Unit = {}
) { ) {
var moveOccured by remember { var moveOccured by remember {
mutableStateOf(false) mutableStateOf(false)
@ -194,6 +299,169 @@ private fun TouchSurface(
else -> false else -> false
} }
}) { }) {
content()
Surface(color = color, modifier = Modifier.fillMaxSize()) {} 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_toggle_fullscreen">Toggle fullscreen</string>
<string name="widget_description_chapter_selection">Chapter selection</string> <string name="widget_description_chapter_selection">Chapter selection</string>
<string name="widget_descriptoin_playlist_item_selection">Playlist item 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> </resources>