diff --git a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt index 04c50a0..baaa5a8 100644 --- a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt +++ b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt @@ -39,19 +39,15 @@ import kotlin.time.Duration.Companion.seconds fun VideoSurfaceControls( player: VideoPlayer, modifier: Modifier = Modifier, + focusRequester: FocusRequester? = null, + focusOnClick: Boolean = false, ) { val coroutineScope = rememberCoroutineScope() val paused by deferredFlowStateOf(player::paused) val loading by player.property("seeking") val backgroundColor = MaterialTheme.colors.surface - val focus = remember { FocusRequester() } + val focus = remember(focusRequester) { focusRequester ?: FocusRequester() } Box(modifier = modifier) { - Box( - modifier = Modifier.matchParentSize() - .handleInputs(player, focus) - .focusRequester(focus) - .focusable(enabled = true, interactionSource = remember { MutableInteractionSource() }) - ) StateIndicatorIcon(player, Modifier.align(Alignment.Center)) if (loading != false) { CircularProgressIndicator( @@ -60,6 +56,12 @@ fun VideoSurfaceControls( strokeCap = StrokeCap.Round, ) } + Box( + modifier = Modifier.matchParentSize() + .handleInputs(player, focus.takeIf { focusOnClick }) + .focusRequester(focus) + .focusable(enabled = true, interactionSource = remember { MutableInteractionSource() }) + ) BoxWithConstraints( modifier = Modifier @@ -151,6 +153,8 @@ fun VideoSurfaceControls( } } LaunchedEffect(focus) { - focus.requestFocus() + if (focusRequester == null) { // only focus if we created the focus requester + focus.requestFocus() + } } } diff --git a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt index 6c5472b..bf10d46 100644 --- a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt +++ b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt @@ -19,7 +19,7 @@ import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalComposeUiApi::class, InternalMultimediaApi::class) @Composable -fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester): Modifier { +fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester? = null): Modifier { val coroutineScope = rememberCoroutineScope() var setSpeedJob: Job? by remember { mutableStateOf(null) } var spedUp by remember { mutableStateOf(false) } @@ -31,7 +31,7 @@ fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester): while (true) { val event = awaitPointerEvent() if (event.button == PointerButton.Primary) { - focusRequester.requestFocus() + focusRequester?.requestFocus() if (event.type == PointerEventType.Press) { setSpeedJob = coroutineScope.launch { delay(longPressTimeout) diff --git a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceWithControls.kt b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceWithControls.kt index ff43300..a637e59 100644 --- a/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceWithControls.kt +++ b/src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceWithControls.kt @@ -5,36 +5,25 @@ import androidx.compose.foundation.layout.requiredSizeIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import dev.silenium.multimedia.core.annotation.InternalMultimediaApi +import androidx.compose.ui.focus.FocusRequester -@OptIn(InternalMultimediaApi::class) @Composable fun VideoSurfaceWithControls( player: VideoPlayer, modifier: Modifier = Modifier, showStats: Boolean = false, - onInitialized: () -> Unit = {}, + controlFocusRequester: FocusRequester? = null, ) { BoxWithConstraints(modifier) { - BoxWithConstraints( - modifier = Modifier.requiredSizeIn( - this@BoxWithConstraints.minWidth, - this@BoxWithConstraints.minHeight, - this@BoxWithConstraints.maxWidth, - this@BoxWithConstraints.maxHeight, - ) - ) { - VideoSurface( - player, showStats, - onInitialized = onInitialized, - modifier = Modifier.align(Alignment.Center).requiredSizeIn( - minWidth = minWidth, - minHeight = minHeight, - maxWidth = maxWidth, - maxHeight = maxHeight, - ), - ) - VideoSurfaceControls(player, Modifier.matchParentSize()) - } + VideoSurface( + player, showStats, + modifier = Modifier.align(Alignment.Center).requiredSizeIn( + minWidth = minWidth, + minHeight = minHeight, + maxWidth = maxWidth, + maxHeight = maxHeight, + ), + ) + VideoSurfaceControls(player, Modifier.matchParentSize(), controlFocusRequester) } } diff --git a/src/test/kotlin/dev/silenium/multimedia/compose/Main.kt b/src/test/kotlin/dev/silenium/multimedia/compose/Main.kt index b5faec9..e20ec19 100644 --- a/src/test/kotlin/dev/silenium/multimedia/compose/Main.kt +++ b/src/test/kotlin/dev/silenium/multimedia/compose/Main.kt @@ -3,14 +3,16 @@ package dev.silenium.multimedia.compose import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.awaitApplication @@ -18,14 +20,12 @@ import dev.silenium.multimedia.compose.player.VideoSurfaceWithControls import dev.silenium.multimedia.compose.player.rememberVideoPlayer import dev.silenium.multimedia.compose.util.LocalFullscreenProvider import dev.silenium.multimedia.core.annotation.InternalMultimediaApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import java.nio.file.Files import kotlin.io.path.absolutePathString import kotlin.io.path.outputStream import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds @OptIn(InternalMultimediaApi::class) @Composable @@ -38,55 +38,110 @@ fun App() { } videoFile.apply { toFile().deleteOnExit() } } - val player = rememberVideoPlayer() var ready by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val player = rememberVideoPlayer( + onInitialized = { + ready = true + }, + ) + DisposableEffect(Unit) { + onDispose { + ready = false + } + } + LaunchedEffect(file) { + withContext(Dispatchers.Default) { + while (!ready && isActive) delay(10.milliseconds) + player.command("loadfile", file.absolutePathString()) + } + } + val fullscreen = LocalFullscreenProvider.current.isFullscreen + val lazyState = rememberLazyListState() BoxWithConstraints( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background) + modifier = Modifier.background(MaterialTheme.colors.background).fillMaxSize() ) { - val fullscreen = LocalFullscreenProvider.current.isFullscreen - val scroll = rememberScrollState() - Column( - modifier = Modifier.verticalScroll(scroll, !LocalFullscreenProvider.current.isFullscreen).fillMaxSize() - ) { - VideoSurfaceWithControls( - player = player, - modifier = Modifier.let { - when { - fullscreen -> it.size(this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight) - else -> it.requiredSizeIn( - this@BoxWithConstraints.minWidth, - this@BoxWithConstraints.minHeight, - this@BoxWithConstraints.maxWidth, - this@BoxWithConstraints.maxHeight, - ) - } - }, - showStats = true, - onInitialized = { - ready = true - } + var visible by remember { mutableStateOf(true) } + var wasPaused by remember { mutableStateOf("no") } + LaunchedEffect(Unit) { + while (isActive) { + delay(2.seconds) + visible = !visible + } + } + val modifier = when { + fullscreen -> Modifier.size( + this@BoxWithConstraints.maxWidth, + this@BoxWithConstraints.maxHeight ) - Text( - text = "This is a test video player", - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.h6, - color = MaterialTheme.colors.onBackground + + else -> Modifier.requiredSizeIn( + this@BoxWithConstraints.minWidth, + this@BoxWithConstraints.minHeight, + this@BoxWithConstraints.maxWidth, + this@BoxWithConstraints.maxHeight, ) } - var previousPosition by remember { mutableStateOf(0) } - LaunchedEffect(fullscreen) { - if (fullscreen) { - previousPosition = scroll.value - scroll.scrollTo(0) + LazyColumn( + modifier = modifier, + state = lazyState, + userScrollEnabled = !fullscreen, + ) { + if (visible) { + item(key = "video", contentType = "video") { + VideoSurfaceWithControls( + player = player, + modifier = Modifier.fillParentMaxSize().animateItem(), + showStats = true, + controlFocusRequester = remember { FocusRequester() }, + ) + DisposableEffect(Unit) { + coroutineScope.launch { + println("Setting pause to $wasPaused") + player.setProperty("pause", wasPaused) + } + + onDispose { + coroutineScope.launch { + wasPaused = player.getProperty("pause").getOrNull() ?: "no" + player.setProperty("pause", "yes") + } + } + } + } } else { - scroll.scrollTo(previousPosition) + item(key = "video", contentType = "empty") { + Box( + modifier = Modifier.fillParentMaxSize().animateItem(), + ) { + Text( + text = "Video player is not visible", + modifier = Modifier.padding(16.dp).align(Alignment.Center), + style = MaterialTheme.typography.h6, + color = MaterialTheme.colors.onBackground + ) + } + } + } + item(key = "text", contentType = "text") { + Text( + text = "This is a test video player", + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.h6, + color = MaterialTheme.colors.onBackground + ) } } } - LaunchedEffect(file) { - withContext(Dispatchers.Default) { - while (!ready && isActive) delay(10.milliseconds) - player.command("loadfile", file.absolutePathString()) + var previousIndex by remember { mutableStateOf(0) } + var previousOffset by remember { mutableStateOf(0) } + LaunchedEffect(fullscreen) { + if (fullscreen) { + previousIndex = lazyState.firstVisibleItemIndex + previousOffset = lazyState.firstVisibleItemScrollOffset + lazyState.scrollToItem(0) + } else { + lazyState.scrollToItem(previousIndex, previousOffset) } } }