Skip to content

Commit

Permalink
feat: scale video in Surface with controls, improve focus handling, a…
Browse files Browse the repository at this point in the history
…dd fullscreen handling
  • Loading branch information
silenium-dev committed Oct 14, 2024
1 parent 89e9090 commit b540abd
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class VideoPlayer(hwdec: Boolean = false) : AutoCloseable {
@InternalMultimediaApi
suspend fun command(vararg command: String) = mpv.commandAsync(command.toList().toTypedArray())

suspend fun toggleFullscreen() = mpv.commandAsync("cycle", "fullscreen")
suspend fun togglePause() = mpv.commandAsync("cycle", "pause")
suspend fun toggleMute() = mpv.commandAsync("cycle", "mute")
suspend fun setVolume(volume: Long) = mpv.commandAsync("set", "volume", volume.toString())
Expand Down Expand Up @@ -69,6 +70,8 @@ class VideoPlayer(hwdec: Boolean = false) : AutoCloseable {
fun onRender(scope: GLDrawScope, state: GLSurfaceState) {
initialize(state)

// TODO: fix render block if screen is disconnected and reconnected

glClearColor(0f, 0f, 0f, 0f)
glClear(GL_COLOR_BUFFER_BIT)
render?.render(scope.fbo)?.getOrThrow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ fun VideoSurfaceControls(
Box(modifier = modifier) {
Box(
modifier = Modifier.matchParentSize()
.handleInputs(player)
.handleInputs(player, focus)
.focusRequester(focus)
.focusable(enabled = true, interactionSource = MutableInteractionSource())
.focusable(enabled = true, interactionSource = remember { MutableInteractionSource() })
)
StateIndicatorIcon(player, Modifier.align(Alignment.Center))
if (loading != false) {
Expand Down Expand Up @@ -109,7 +109,6 @@ fun VideoSurfaceControls(
onClick = {
coroutineScope.launch {
player.togglePause()
println("Paused: ${player.getProperty<Boolean>("pause")}")
}
},
modifier = Modifier.padding(horizontal = 4.dp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,72 @@ package dev.silenium.multimedia.compose.player
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.*
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.input.pointer.pointerInput
import dev.silenium.multimedia.compose.util.LocalFullscreenProvider
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration.Companion.milliseconds

@OptIn(ExperimentalComposeUiApi::class, InternalMultimediaApi::class)
@Composable
fun Modifier.handleInputs(player: VideoPlayer): Modifier {
fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester): Modifier {
val coroutineScope = rememberCoroutineScope()
var setSpeedJob: Job? by remember { mutableStateOf(null) }
var spedUp by remember { mutableStateOf(false) }
return this
.onPointerEvent(PointerEventType.Press) {
if (it.button == PointerButton.Primary) {
setSpeedJob = coroutineScope.launch {
delay(500)
spedUp = true
player.setProperty("speed", 2.0)
}
}
}
.onPointerEvent(PointerEventType.Release) {
if (it.button == PointerButton.Primary) {
setSpeedJob?.cancel()
if (spedUp) {
spedUp = false
setSpeedJob = null
coroutineScope.launch {
player.setProperty("speed", 1.0)
}
} else {
coroutineScope.launch {
player.togglePause()
var lastRelease by remember { mutableStateOf(Instant.DISTANT_PAST) }
val fullscreenProvider = LocalFullscreenProvider.current
return this.pointerInput(player) {
awaitPointerEventScope {
val longPressTimeout = viewConfiguration.longPressTimeoutMillis.milliseconds
while (true) {
val event = awaitPointerEvent()
if (event.button == PointerButton.Primary) {
focusRequester.requestFocus()
if (event.type == PointerEventType.Press) {
setSpeedJob = coroutineScope.launch {
delay(longPressTimeout)
spedUp = true
player.setProperty("speed", 2.0)
}
} else if (event.type == PointerEventType.Release) {
setSpeedJob?.cancel()
if (spedUp) {
spedUp = false
setSpeedJob = null
coroutineScope.launch {
player.setProperty("speed", 1.0)
}
} else {
coroutineScope.launch {
player.togglePause()
}
}
val now = Clock.System.now()
if (lastRelease + longPressTimeout > now) {
fullscreenProvider.toggleFullscreen()
}
lastRelease = Clock.System.now()
}
}
}
}
.onPreviewKeyEvent {
if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
when (it.key) {
Key.Spacebar -> {
println("${it.type}: ${it.key}")
coroutineScope.launch { player.togglePause() }
true
}

else -> false
}.onPreviewKeyEvent {
if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
when (it.key) {
Key.Spacebar -> {
coroutineScope.launch { player.togglePause() }
true
}

else -> false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
package dev.silenium.multimedia.compose.player

import androidx.compose.foundation.layout.BoxWithConstraints
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

@OptIn(InternalMultimediaApi::class)
@Composable
fun VideoSurfaceWithControls(
player: VideoPlayer,
modifier: Modifier = Modifier,
showStats: Boolean = false,
onInitialized: () -> Unit = {},
) {
BoxWithConstraints(modifier = modifier) {
VideoSurface(
player, showStats,
onInitialized = onInitialized,
modifier = Modifier.matchParentSize(),
)
VideoSurfaceControls(player, Modifier.matchParentSize())
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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ fun VolumeSlider(player: VideoPlayer, coroutineScope: CoroutineScope, modifier:
IconButton(
onClick = {
coroutineScope.launch {
player.toggleMute().onFailure {
println("Failed to toggle mute: $it")
}
player.toggleMute()
}
},
modifier = Modifier.padding(horizontal = 4.dp),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.silenium.multimedia.compose.util

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowState

class FullscreenProvider {
var isFullscreen by mutableStateOf(false)
private set
val windowState = WindowState()

@Synchronized
fun toggleFullscreen() {
isFullscreen = !isFullscreen
windowState.placement = if (isFullscreen) WindowPlacement.Fullscreen else WindowPlacement.Floating
}
}

val LocalFullscreenProvider = staticCompositionLocalOf { FullscreenProvider() }
56 changes: 47 additions & 9 deletions src/test/kotlin/dev/silenium/multimedia/compose/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ package dev.silenium.multimedia.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Modifier
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
Expand All @@ -36,15 +40,48 @@ fun App() {
}
val player = rememberVideoPlayer()
var ready by remember { mutableStateOf(false) }
Box(
BoxWithConstraints(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)
) {
VideoSurfaceWithControls(
player, showStats = true, modifier = Modifier.fillMaxSize(),
onInitialized = {
ready = true
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
}
)
Text(
text = "This is a test video player",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onBackground
)
}
var previousPosition by remember { mutableStateOf(0) }
LaunchedEffect(fullscreen) {
if (fullscreen) {
previousPosition = scroll.value
scroll.scrollTo(0)
} else {
scroll.scrollTo(previousPosition)
}
)
}
}
LaunchedEffect(file) {
withContext(Dispatchers.Default) {
Expand All @@ -56,7 +93,8 @@ fun App() {
}

suspend fun main(): Unit = awaitApplication {
Window(onCloseRequest = ::exitApplication) {
val state = LocalFullscreenProvider.current.windowState
Window(state = state, onCloseRequest = ::exitApplication) {
App()
}
}

0 comments on commit b540abd

Please sign in to comment.