Skip to content

Commit b540abd

Browse files
committed
feat: scale video in Surface with controls, improve focus handling, add fullscreen handling
1 parent 89e9090 commit b540abd

File tree

7 files changed

+149
-56
lines changed

7 files changed

+149
-56
lines changed

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayer.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class VideoPlayer(hwdec: Boolean = false) : AutoCloseable {
3737
@InternalMultimediaApi
3838
suspend fun command(vararg command: String) = mpv.commandAsync(command.toList().toTypedArray())
3939

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

73+
// TODO: fix render block if screen is disconnected and reconnected
74+
7275
glClearColor(0f, 0f, 0f, 0f)
7376
glClear(GL_COLOR_BUFFER_BIT)
7477
render?.render(scope.fbo)?.getOrThrow()

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoPlayerControls.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ fun VideoSurfaceControls(
4848
Box(modifier = modifier) {
4949
Box(
5050
modifier = Modifier.matchParentSize()
51-
.handleInputs(player)
51+
.handleInputs(player, focus)
5252
.focusRequester(focus)
53-
.focusable(enabled = true, interactionSource = MutableInteractionSource())
53+
.focusable(enabled = true, interactionSource = remember { MutableInteractionSource() })
5454
)
5555
StateIndicatorIcon(player, Modifier.align(Alignment.Center))
5656
if (loading != false) {
@@ -109,7 +109,6 @@ fun VideoSurfaceControls(
109109
onClick = {
110110
coroutineScope.launch {
111111
player.togglePause()
112-
println("Paused: ${player.getProperty<Boolean>("pause")}")
113112
}
114113
},
115114
modifier = Modifier.padding(horizontal = 4.dp),

src/main/kotlin/dev/silenium/multimedia/compose/player/VideoSurfaceEvents.kt

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,72 @@ package dev.silenium.multimedia.compose.player
33
import androidx.compose.runtime.*
44
import androidx.compose.ui.ExperimentalComposeUiApi
55
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.focus.FocusRequester
67
import androidx.compose.ui.input.key.*
78
import androidx.compose.ui.input.pointer.PointerButton
89
import androidx.compose.ui.input.pointer.PointerEventType
9-
import androidx.compose.ui.input.pointer.onPointerEvent
10+
import androidx.compose.ui.input.pointer.pointerInput
11+
import dev.silenium.multimedia.compose.util.LocalFullscreenProvider
1012
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
1113
import kotlinx.coroutines.Job
1214
import kotlinx.coroutines.delay
1315
import kotlinx.coroutines.launch
16+
import kotlinx.datetime.Clock
17+
import kotlinx.datetime.Instant
18+
import kotlin.time.Duration.Companion.milliseconds
1419

1520
@OptIn(ExperimentalComposeUiApi::class, InternalMultimediaApi::class)
1621
@Composable
17-
fun Modifier.handleInputs(player: VideoPlayer): Modifier {
22+
fun Modifier.handleInputs(player: VideoPlayer, focusRequester: FocusRequester): Modifier {
1823
val coroutineScope = rememberCoroutineScope()
1924
var setSpeedJob: Job? by remember { mutableStateOf(null) }
2025
var spedUp by remember { mutableStateOf(false) }
21-
return this
22-
.onPointerEvent(PointerEventType.Press) {
23-
if (it.button == PointerButton.Primary) {
24-
setSpeedJob = coroutineScope.launch {
25-
delay(500)
26-
spedUp = true
27-
player.setProperty("speed", 2.0)
28-
}
29-
}
30-
}
31-
.onPointerEvent(PointerEventType.Release) {
32-
if (it.button == PointerButton.Primary) {
33-
setSpeedJob?.cancel()
34-
if (spedUp) {
35-
spedUp = false
36-
setSpeedJob = null
37-
coroutineScope.launch {
38-
player.setProperty("speed", 1.0)
39-
}
40-
} else {
41-
coroutineScope.launch {
42-
player.togglePause()
26+
var lastRelease by remember { mutableStateOf(Instant.DISTANT_PAST) }
27+
val fullscreenProvider = LocalFullscreenProvider.current
28+
return this.pointerInput(player) {
29+
awaitPointerEventScope {
30+
val longPressTimeout = viewConfiguration.longPressTimeoutMillis.milliseconds
31+
while (true) {
32+
val event = awaitPointerEvent()
33+
if (event.button == PointerButton.Primary) {
34+
focusRequester.requestFocus()
35+
if (event.type == PointerEventType.Press) {
36+
setSpeedJob = coroutineScope.launch {
37+
delay(longPressTimeout)
38+
spedUp = true
39+
player.setProperty("speed", 2.0)
40+
}
41+
} else if (event.type == PointerEventType.Release) {
42+
setSpeedJob?.cancel()
43+
if (spedUp) {
44+
spedUp = false
45+
setSpeedJob = null
46+
coroutineScope.launch {
47+
player.setProperty("speed", 1.0)
48+
}
49+
} else {
50+
coroutineScope.launch {
51+
player.togglePause()
52+
}
53+
}
54+
val now = Clock.System.now()
55+
if (lastRelease + longPressTimeout > now) {
56+
fullscreenProvider.toggleFullscreen()
57+
}
58+
lastRelease = Clock.System.now()
4359
}
4460
}
4561
}
4662
}
47-
.onPreviewKeyEvent {
48-
if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
49-
when (it.key) {
50-
Key.Spacebar -> {
51-
println("${it.type}: ${it.key}")
52-
coroutineScope.launch { player.togglePause() }
53-
true
54-
}
55-
56-
else -> false
63+
}.onPreviewKeyEvent {
64+
if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
65+
when (it.key) {
66+
Key.Spacebar -> {
67+
coroutineScope.launch { player.togglePause() }
68+
true
5769
}
70+
71+
else -> false
5872
}
73+
}
5974
}
Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
package dev.silenium.multimedia.compose.player
22

33
import androidx.compose.foundation.layout.BoxWithConstraints
4+
import androidx.compose.foundation.layout.requiredSizeIn
45
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Alignment
57
import androidx.compose.ui.Modifier
8+
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
69

10+
@OptIn(InternalMultimediaApi::class)
711
@Composable
812
fun VideoSurfaceWithControls(
913
player: VideoPlayer,
1014
modifier: Modifier = Modifier,
1115
showStats: Boolean = false,
1216
onInitialized: () -> Unit = {},
1317
) {
14-
BoxWithConstraints(modifier = modifier) {
15-
VideoSurface(
16-
player, showStats,
17-
onInitialized = onInitialized,
18-
modifier = Modifier.matchParentSize(),
19-
)
20-
VideoSurfaceControls(player, Modifier.matchParentSize())
18+
BoxWithConstraints(modifier) {
19+
BoxWithConstraints(
20+
modifier = Modifier.requiredSizeIn(
21+
this@BoxWithConstraints.minWidth,
22+
this@BoxWithConstraints.minHeight,
23+
this@BoxWithConstraints.maxWidth,
24+
this@BoxWithConstraints.maxHeight,
25+
)
26+
) {
27+
VideoSurface(
28+
player, showStats,
29+
onInitialized = onInitialized,
30+
modifier = Modifier.align(Alignment.Center).requiredSizeIn(
31+
minWidth = minWidth,
32+
minHeight = minHeight,
33+
maxWidth = maxWidth,
34+
maxHeight = maxHeight,
35+
),
36+
)
37+
VideoSurfaceControls(player, Modifier.matchParentSize())
38+
}
2139
}
2240
}

src/main/kotlin/dev/silenium/multimedia/compose/player/VolumeSlider.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ fun VolumeSlider(player: VideoPlayer, coroutineScope: CoroutineScope, modifier:
4343
IconButton(
4444
onClick = {
4545
coroutineScope.launch {
46-
player.toggleMute().onFailure {
47-
println("Failed to toggle mute: $it")
48-
}
46+
player.toggleMute()
4947
}
5048
},
5149
modifier = Modifier.padding(horizontal = 4.dp),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package dev.silenium.multimedia.compose.util
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
6+
import androidx.compose.runtime.staticCompositionLocalOf
7+
import androidx.compose.ui.window.WindowPlacement
8+
import androidx.compose.ui.window.WindowState
9+
10+
class FullscreenProvider {
11+
var isFullscreen by mutableStateOf(false)
12+
private set
13+
val windowState = WindowState()
14+
15+
@Synchronized
16+
fun toggleFullscreen() {
17+
isFullscreen = !isFullscreen
18+
windowState.placement = if (isFullscreen) WindowPlacement.Fullscreen else WindowPlacement.Floating
19+
}
20+
}
21+
22+
val LocalFullscreenProvider = staticCompositionLocalOf { FullscreenProvider() }

src/test/kotlin/dev/silenium/multimedia/compose/Main.kt

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@ package dev.silenium.multimedia.compose
22

33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.isSystemInDarkTheme
5-
import androidx.compose.foundation.layout.Box
6-
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.rememberScrollState
7+
import androidx.compose.foundation.verticalScroll
78
import androidx.compose.material.MaterialTheme
9+
import androidx.compose.material.Text
810
import androidx.compose.material.darkColors
911
import androidx.compose.material.lightColors
1012
import androidx.compose.runtime.*
1113
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.unit.dp
1215
import androidx.compose.ui.window.Window
1316
import androidx.compose.ui.window.awaitApplication
1417
import dev.silenium.multimedia.compose.player.VideoSurfaceWithControls
1518
import dev.silenium.multimedia.compose.player.rememberVideoPlayer
19+
import dev.silenium.multimedia.compose.util.LocalFullscreenProvider
1620
import dev.silenium.multimedia.core.annotation.InternalMultimediaApi
1721
import kotlinx.coroutines.Dispatchers
1822
import kotlinx.coroutines.delay
@@ -36,15 +40,48 @@ fun App() {
3640
}
3741
val player = rememberVideoPlayer()
3842
var ready by remember { mutableStateOf(false) }
39-
Box(
43+
BoxWithConstraints(
4044
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)
4145
) {
42-
VideoSurfaceWithControls(
43-
player, showStats = true, modifier = Modifier.fillMaxSize(),
44-
onInitialized = {
45-
ready = true
46+
val fullscreen = LocalFullscreenProvider.current.isFullscreen
47+
val scroll = rememberScrollState()
48+
Column(
49+
modifier = Modifier.verticalScroll(scroll, !LocalFullscreenProvider.current.isFullscreen).fillMaxSize()
50+
) {
51+
VideoSurfaceWithControls(
52+
player = player,
53+
modifier = Modifier.let {
54+
when {
55+
fullscreen -> it.size(this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight)
56+
else -> it.requiredSizeIn(
57+
this@BoxWithConstraints.minWidth,
58+
this@BoxWithConstraints.minHeight,
59+
this@BoxWithConstraints.maxWidth,
60+
this@BoxWithConstraints.maxHeight,
61+
)
62+
}
63+
},
64+
showStats = true,
65+
onInitialized = {
66+
ready = true
67+
}
68+
)
69+
Text(
70+
text = "This is a test video player",
71+
modifier = Modifier.padding(16.dp),
72+
style = MaterialTheme.typography.h6,
73+
color = MaterialTheme.colors.onBackground
74+
)
75+
}
76+
var previousPosition by remember { mutableStateOf(0) }
77+
LaunchedEffect(fullscreen) {
78+
if (fullscreen) {
79+
previousPosition = scroll.value
80+
scroll.scrollTo(0)
81+
} else {
82+
scroll.scrollTo(previousPosition)
4683
}
47-
)
84+
}
4885
}
4986
LaunchedEffect(file) {
5087
withContext(Dispatchers.Default) {
@@ -56,7 +93,8 @@ fun App() {
5693
}
5794

5895
suspend fun main(): Unit = awaitApplication {
59-
Window(onCloseRequest = ::exitApplication) {
96+
val state = LocalFullscreenProvider.current.windowState
97+
Window(state = state, onCloseRequest = ::exitApplication) {
6098
App()
6199
}
62100
}

0 commit comments

Comments
 (0)