From 3a7971d10d6057a2b3bf76d8b32735acd3cc27e7 Mon Sep 17 00:00:00 2001
From: Mahdi Hosseinzadeh <29678011+mahozad@users.noreply.github.com>
Date: Thu, 20 Apr 2023 16:01:02 +0330
Subject: [PATCH] Improve the experimental video player (#2906)
* Refactor video player`isMacOS` function
* Refactor video player `mediaPayer` extension function
* Re-implement the experimental video player
* Use icons instead of labels and buttons for video player controls
* Center video player controls
* Add a TODO to video player to make the slider logarithmic
* Add `rememberVideoPlayerState` function for video player
To simplify state management for the client
* Extend a comment with more info
* Improve a KDoc
* Specify video player minimum size for the window
* Use `VideoPlayerState` instead of explicit parameters for video player
* Reorder a statement in video player
* Use the implicit `it` instead of named lambda parameter
* Use `roundToInt` instead of `toInt` for converting a `Float` to percentage
* Update some documentation
* Remove a redundant trailing comma
* Add more comments about video player fullscreen option
* Add some functions to VideoPlayerState
And use them in the code
* Add `ms` unit label to video player timestamp
* Remove a redundant import statement
* Simplify video player `produceProgress` function
* Extract video player URL as a constant
And add some docs about local files
* Add some comments
About placing controls on the video player which is NOT currently possible.
* Add video player minor code improvements
* Ensure video player volume icon and slider are aligned
* Make video player control icons smaller
* Add some comments for video player Progress class
* Improve video player speed input
* Extract a remember function call to an outer function
* Convert a lambda to method reference
* Update the video player component
* Convert video player progress from return value to state property
---
.../compose/videoplayer/demo/Main.kt | 145 +++++++++++++++--
.../jvmMain/resources/enter-fullscreen.svg | 1 +
.../src/jvmMain/resources/exit-fullscreen.svg | 1 +
.../demo/src/jvmMain/resources/pause.svg | 1 +
.../demo/src/jvmMain/resources/play.svg | 1 +
.../demo/src/jvmMain/resources/speed.svg | 1 +
.../demo/src/jvmMain/resources/volume.svg | 1 +
.../compose/videoplayer/VideoPlayer.kt | 118 +++++++++++++-
.../compose/videoplayer/DesktopVideoPlayer.kt | 146 +++++++++++++-----
9 files changed, 365 insertions(+), 50 deletions(-)
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/enter-fullscreen.svg
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/exit-fullscreen.svg
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/pause.svg
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/play.svg
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/speed.svg
create mode 100644 experimental/components/VideoPlayer/demo/src/jvmMain/resources/volume.svg
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt b/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt
index 7cdec64a32..e7ec6322cd 100644
--- a/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/kotlin/org/jetbrains/compose/videoplayer/demo/Main.kt
@@ -1,22 +1,145 @@
package org.jetbrains.compose.videoplayer.demo
-import androidx.compose.material.MaterialTheme
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.singleWindowApplication
import org.jetbrains.compose.videoplayer.VideoPlayer
+import org.jetbrains.compose.videoplayer.rememberVideoPlayerState
+import java.awt.Dimension
+
+/**
+ * To play a local file, use a URL notation like this:
+ * ```kotlin
+ * const val VIDEO_URL = "file:///C:/Users/John/Desktop/example.mp4"
+ * ```
+ * Relative paths like this may also work (relative to subproject directory aka `demo/`):
+ * ```kotlin
+ * val VIDEO_URL = """file:///${Path("videos/example.mp4")}"""
+ * ```
+ * To package a video with the app distributable,
+ * see [this tutorial](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Native_distributions_and_local_execution#adding-files-to-packaged-application)
+ * and then use a URL syntax like this:
+ * ```kotlin
+ * val VIDEO_URL = """file:///${Path(System.getProperty("compose.application.resources.dir")) / "example.mp4"}"""
+ * ```
+ */
+const val VIDEO_URL = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
fun main() {
- singleWindowApplication(
- title = "Video Player",
- state = WindowState(width = 800.dp, height = 800.dp)
- ) {
+ singleWindowApplication(title = "Video Player") {
+ // See https://github.com/JetBrains/compose-multiplatform/issues/2285
+ window.minimumSize = Dimension(700, 560)
MaterialTheme {
- VideoPlayer(
- url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
- width = 640,
- height = 480
- )
+ App()
+ }
+ }
+}
+
+@Composable
+fun App() {
+ val state = rememberVideoPlayerState()
+ /*
+ * Could not use a [Box] to overlay the controls on top of the video.
+ * See https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/Swing_Integration
+ * Related issues:
+ * https://github.com/JetBrains/compose-multiplatform/issues/1521
+ * https://github.com/JetBrains/compose-multiplatform/issues/2926
+ */
+ Column {
+ VideoPlayer(
+ url = VIDEO_URL,
+ state = state,
+ onFinish = state::stopPlayback,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(400.dp)
+ )
+ Slider(
+ value = state.progress.value.fraction,
+ onValueChange = { state.seek = it },
+ modifier = Modifier.fillMaxWidth()
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Timestamp: ${state.progress.value.timeMillis} ms", modifier = Modifier.width(180.dp))
+ IconButton(onClick = state::toggleResume) {
+ Icon(
+ painter = painterResource("${if (state.isResumed) "pause" else "play"}.svg"),
+ contentDescription = "Play/Pause",
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ IconButton(onClick = state::toggleFullscreen) {
+ Icon(
+ painter = painterResource("${if (state.isFullscreen) "exit" else "enter"}-fullscreen.svg"),
+ contentDescription = "Toggle fullscreen",
+ modifier = Modifier.size(32.dp)
+ )
+ }
+ Speed(
+ initialValue = state.speed,
+ modifier = Modifier.width(104.dp)
+ ) {
+ state.speed = it ?: state.speed
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ painter = painterResource("volume.svg"),
+ contentDescription = "Volume",
+ modifier = Modifier.size(32.dp)
+ )
+ // TODO: Make the slider change volume in logarithmic manner
+ // See https://www.dr-lex.be/info-stuff/volumecontrols.html
+ // and https://ux.stackexchange.com/q/79672/117386
+ // and https://dcordero.me/posts/logarithmic_volume_control.html
+ Slider(
+ value = state.volume,
+ onValueChange = { state.volume = it },
+ modifier = Modifier.width(100.dp)
+ )
+ }
}
}
}
+
+/**
+ * See [this Stack Overflow post](https://stackoverflow.com/a/67765652).
+ */
+@Composable
+fun Speed(
+ initialValue: Float,
+ modifier: Modifier = Modifier,
+ onChange: (Float?) -> Unit
+) {
+ var input by remember { mutableStateOf(initialValue.toString()) }
+ OutlinedTextField(
+ value = input,
+ modifier = modifier,
+ singleLine = true,
+ leadingIcon = {
+ Icon(
+ painter = painterResource("speed.svg"),
+ contentDescription = "Speed",
+ modifier = Modifier.size(28.dp)
+ )
+ },
+ onValueChange = {
+ input = if (it.isEmpty()) {
+ it
+ } else if (it.toFloatOrNull() == null) {
+ input // Old value
+ } else {
+ it // New value
+ }
+ onChange(input.toFloatOrNull())
+ }
+ )
+}
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/enter-fullscreen.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/enter-fullscreen.svg
new file mode 100644
index 0000000000..088bd892ff
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/enter-fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/exit-fullscreen.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/exit-fullscreen.svg
new file mode 100644
index 0000000000..2e9533f6d3
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/exit-fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/pause.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/pause.svg
new file mode 100644
index 0000000000..bbc21ec85e
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/pause.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/play.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/play.svg
new file mode 100644
index 0000000000..7a4d37dbeb
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/play.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/speed.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/speed.svg
new file mode 100644
index 0000000000..b3986a4c54
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/speed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/demo/src/jvmMain/resources/volume.svg b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/volume.svg
new file mode 100644
index 0000000000..4ac55f74fd
--- /dev/null
+++ b/experimental/components/VideoPlayer/demo/src/jvmMain/resources/volume.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt b/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt
index 68007a12b2..d9858f81df 100644
--- a/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt
+++ b/experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt
@@ -1,10 +1,120 @@
package org.jetbrains.compose.videoplayer
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+
+data class Progress(
+ val fraction: Float,
+ // TODO: Use kotlin.time.Duration when Kotlin version is updated.
+ // See https://github.com/Kotlin/api-guidelines/issues/6
+ val timeMillis: Long
+)
@Composable
-fun VideoPlayer(url: String, width: Int, height: Int) {
- VideoPlayerImpl(url, width, height)
+fun VideoPlayer(
+ url: String,
+ state: VideoPlayerState,
+ modifier: Modifier = Modifier,
+ onFinish: (() -> Unit)? = null
+) = VideoPlayerImpl(
+ url = url,
+ isResumed = state.isResumed,
+ volume = state.volume,
+ speed = state.speed,
+ seek = state.seek,
+ isFullscreen = state.isFullscreen,
+ progressState = state._progress,
+ modifier = modifier,
+ onFinish = onFinish
+)
+
+internal expect fun VideoPlayerImpl(
+ url: String,
+ isResumed: Boolean,
+ volume: Float,
+ speed: Float,
+ seek: Float,
+ isFullscreen: Boolean,
+ progressState: MutableState