From 281c7dc68e207f02ad168f7e19eb6fa7133d9d01 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 7 Mar 2025 23:44:46 +0100 Subject: [PATCH] Support a video on the desktop --- app/build.gradle.kts | 3 + .../kotlin/com/daniebeler/pfpixelix/Main.kt | 49 +++++----- .../pfpixelix/utils/VideoPlayer.jvm.kt | 92 +++++++++++++++---- gradle/libs.versions.toml | 2 + 4 files changed, 105 insertions(+), 41 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a61c8b50..ccf9fc55 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,9 @@ kotlin { implementation(libs.ktor.client.okhttp) implementation(libs.appdirs) implementation(libs.slf4j.simple) + implementation(libs.vlcj) + implementation("net.java.dev.jna:jna:5.15.0") + implementation("net.java.dev.jna:jna-platform:5.15.0") } } diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt index f950880e..ffc09aca 100644 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt @@ -15,31 +15,36 @@ import com.daniebeler.pfpixelix.utils.configureLogger import java.awt.Desktop import java.awt.Dimension -fun main() = application { - val appComponent = AppComponent.Companion.create( - object : KmpContext() {}, - DesktopFileService(), - DesktopAppIconManager() - ) +fun main() { + //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-swing-interoperability.html + System.setProperty("compose.swing.render.on.graphics", "true") + System.setProperty("compose.interop.blending", "true") + application { + val appComponent = AppComponent.Companion.create( + object : KmpContext() {}, + DesktopFileService(), + DesktopAppIconManager() + ) - configureJavaLogger() + configureJavaLogger() - SingletonImageLoader.setSafe { - appComponent.provideImageLoader() - } + SingletonImageLoader.setSafe { + appComponent.provideImageLoader() + } - Desktop.getDesktop().setOpenURIHandler { url -> - appComponent.systemUrlHandler.onRedirect( - url.uri.toString() - ) - } + Desktop.getDesktop().setOpenURIHandler { url -> + appComponent.systemUrlHandler.onRedirect( + url.uri.toString() + ) + } - Window( - title = "Pixelix", - state = rememberWindowState(width = 600.dp, height = 1000.dp), - onCloseRequest = ::exitApplication, - ) { - window.minimumSize = Dimension(400, 600) - App(appComponent) { exitApplication() } + Window( + title = "Pixelix", + state = rememberWindowState(width = 600.dp, height = 1000.dp), + onCloseRequest = ::exitApplication, + ) { + window.minimumSize = Dimension(400, 600) + App(appComponent) { exitApplication() } + } } } \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt index 9b194c1f..9dcf6b90 100644 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt @@ -1,38 +1,92 @@ package com.daniebeler.pfpixelix.utils -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.graphics.Color import kotlinx.coroutines.CoroutineScope -import org.jetbrains.compose.resources.stringResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.video_player_not_supported +import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery +import uk.co.caprica.vlcj.factory.discovery.strategy.OsxNativeDiscoveryStrategy +import uk.co.caprica.vlcj.player.base.MediaPlayer +import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter +import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent +import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent +import uk.co.caprica.vlcj.player.component.InputEvents +import java.awt.Component +import java.util.Locale actual class VideoPlayer actual constructor( context: KmpContext, private val coroutineScope: CoroutineScope ) { + private val mpComponent = initializeMediaPlayerComponent() + private val player = mpComponent.mediaPlayer() + actual var progress: ((current: Long, duration: Long) -> Unit)? = null actual var hasAudio: ((Boolean) -> Unit)? = null + private val listener = object : MediaPlayerEventAdapter() { + override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) { + hasAudio?.invoke(player.audio().trackCount() > 0) + } + + override fun positionChanged(mediaPlayer: MediaPlayer?, newPosition: Float) { + val status = player.status() + progress?.invoke((status.length() * status.position()).toLong(), status.length()) + } + } + + init { + player.events().addMediaPlayerEventListener(listener) + } + @Composable actual fun view(modifier: Modifier) { - Box( - modifier.background(MaterialTheme.colors.secondaryVariant), - contentAlignment = Alignment.Center - ) { - Text(stringResource(Res.string.video_player_not_supported)) - } + SwingPanel( + factory = { mpComponent }, + background = Color.Transparent, + modifier = modifier + ) + } + + actual fun prepare(url: String) { + player.media().prepare(url) + } + + actual fun play() { + player.controls().play() + } + + actual fun pause() { + player.controls().pause() + } + + actual fun release() { + player.events().removeMediaPlayerEventListener(listener) + player.release() + } + + actual fun audio(enable: Boolean) { + player.audio().isMute = !enable + } + + private fun Component.mediaPlayer() = when (this) { + is CallbackMediaPlayerComponent -> mediaPlayer() + is EmbeddedMediaPlayerComponent -> mediaPlayer() + else -> error("mediaPlayer() can only be called on vlcj player components") } - actual fun prepare(url: String) {} + private fun initializeMediaPlayerComponent(): Component { + NativeDiscovery(OsxNativeDiscoveryStrategy()).discover() + return if (isMacOS()) { + CallbackMediaPlayerComponent(null, null, InputEvents.NONE, true, null) + } else { + EmbeddedMediaPlayerComponent(null, null, null, InputEvents.NONE, null) + } + } - actual fun play() {} - actual fun pause() {} - actual fun release() {} - actual fun audio(enable: Boolean) {} + private fun isMacOS(): Boolean { + val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) + return "mac" in os || "darwin" in os + } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b00c825b..264d76fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ workRuntimeKtx = "2.10.0" #desktop appdirs = "1.1.1" slf4jSimple = "2.0.17" +vlcj = "4.10.1" [libraries] @@ -105,6 +106,7 @@ filekit-compose = { module = "io.github.vinceglb:filekit-compose", version.ref = krop = { module = "com.attafitamim.krop:ui", version.ref = "krop" } appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "appdirs" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" } +vlcj = { module = "uk.co.caprica:vlcj", version.ref = "vlcj" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }