Skip to content

Commit

Permalink
feat: support disabling focus on click for the player controls and pa…
Browse files Browse the repository at this point in the history
…ssing your own focus requester
  • Loading branch information
silenium-dev committed Oct 16, 2024
1 parent ab99907 commit 60bb347
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>("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(
Expand All @@ -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
Expand Down Expand Up @@ -151,6 +153,8 @@ fun VideoSurfaceControls(
}
}
LaunchedEffect(focus) {
focus.requestFocus()
if (focusRequester == null) { // only focus if we created the focus requester
focus.requestFocus()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
145 changes: 100 additions & 45 deletions src/test/kotlin/dev/silenium/multimedia/compose/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ 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
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
Expand All @@ -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<String>("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)
}
}
}
Expand Down

0 comments on commit 60bb347

Please sign in to comment.