From bbb3bb1aaed4d2c596212e97675e503e7f9f8f67 Mon Sep 17 00:00:00 2001 From: Kyle Bolen Date: Thu, 30 Jul 2026 06:25:38 +0000 Subject: [PATCH] Use JavaFX Gradle plugin for proper module path setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace manual classifier-based jar deps with the openjfx javafx-plugin which correctly sets up the JavaFX module path, native lib paths, and JVM args needed for media codecs (AVFoundation on macOS). This fixes 'media type not supported' which was caused by JavaFX being on classpath instead of module path — native .dylib codecs only load when JavaFX runs as a named module. --- build.gradle.kts | 22 +++---- gradle/libs.versions.toml | 2 + .../photophetch/ui/LivePhotoPlayer.kt | 58 ------------------- 3 files changed, 9 insertions(+), 73 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 23b9a49..3f9e62d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.javafx) } group = "com.bolenpad.photophetch" @@ -32,21 +33,12 @@ dependencies { implementation(libs.slf4j.api) implementation(libs.logback.classic) - // JavaFX media for Live Photo video playback in preview dialog - val javafxVersion = "21.0.3" - // Detect platform: mac-aarch64 for Apple Silicon, mac for Intel - val osArch = System.getProperty("os.arch") ?: "x86_64" - val javafxPlatform = when { - System.getProperty("os.name").contains("Mac", ignoreCase = true) && osArch.contains("aarch64") -> "mac-aarch64" - System.getProperty("os.name").contains("Mac", ignoreCase = true) -> "mac" - System.getProperty("os.name").contains("Win", ignoreCase = true) -> "win" - else -> "linux" - } - implementation("org.openjfx:javafx-media:$javafxVersion:$javafxPlatform") - implementation("org.openjfx:javafx-swing:$javafxVersion:$javafxPlatform") - implementation("org.openjfx:javafx-graphics:$javafxVersion:$javafxPlatform") - implementation("org.openjfx:javafx-base:$javafxVersion:$javafxPlatform") - implementation("org.openjfx:javafx-controls:$javafxVersion:$javafxPlatform") + // JavaFX media — configured via the javafx plugin block below +} + +javafx { + version = "21.0.3" + modules = listOf("javafx.media", "javafx.swing", "javafx.graphics", "javafx.controls", "javafx.base") } compose.desktop { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b3895b..d88051e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ coroutines = "1.8.0" serialization = "1.6.3" slf4j = "2.0.12" logback = "1.5.3" +javafx = "21.0.3" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } @@ -18,3 +19,4 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose" } +javafx = { id = "org.openjfx.javafxplugin", version = "0.1.0" } diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt index 44a0811..eee4ad7 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt @@ -44,9 +44,6 @@ fun LivePhotoPlayer( log.info("LivePhotoPlayer: loading ${file.name} (${file.length() / 1024}KB)") - // Extract JavaFX native media libs from jar if needed - extractJavafxNativeLibs() - val media = javafx.scene.media.Media(file.toURI().toString()) val player = javafx.scene.media.MediaPlayer(media) @@ -94,58 +91,3 @@ fun isJavaFxMediaAvailable(): Boolean { false } } - -/** - * Extract JavaFX native libraries from the classpath jars to a temp directory - * and add that directory to java.library.path. - * - * This is needed when OpenJFX is loaded from classpath (not module path) — - * the native .dylib/.so files inside the jar need to be extracted for the - * media codecs (AVFoundation on macOS) to load correctly. - */ -private var nativeLibsExtracted = false - -private fun extractJavafxNativeLibs() { - if (nativeLibsExtracted) return - nativeLibsExtracted = true - - try { - val tmpDir = File(System.getProperty("java.io.tmpdir"), "photophetch-javafx-natives") - tmpDir.mkdirs() - - // Find the javafx-media jar on the classpath - val classLoader = Thread.currentThread().contextClassLoader - val mediaClass = classLoader.loadClass("javafx.scene.media.MediaPlayer") - val codeSource = mediaClass.protectionDomain?.codeSource?.location - - if (codeSource != null) { - val jarUrl = codeSource.toURI() - val jarFile = java.util.jar.JarFile(File(jarUrl)) - - // Extract .dylib files (macOS native media codecs) - jarFile.entries().asSequence() - .filter { it.name.endsWith(".dylib") || it.name.endsWith(".so") } - .forEach { entry -> - val outFile = File(tmpDir, File(entry.name).name) - if (!outFile.exists()) { - jarFile.getInputStream(entry).use { input -> - outFile.outputStream().use { output -> input.copyTo(output) } - } - log.info("Extracted native lib: ${outFile.name}") - } - } - jarFile.close() - } - - // Add tmpDir to java.library.path via reflection (standard trick) - val usrPathsField = ClassLoader::class.java.getDeclaredField("usr_paths") - usrPathsField.isAccessible = true - val paths = usrPathsField.get(null) as Array - if (tmpDir.absolutePath !in paths) { - usrPathsField.set(null, paths + tmpDir.absolutePath) - log.info("Added ${tmpDir.absolutePath} to java.library.path") - } - } catch (e: Exception) { - log.warn("extractJavafxNativeLibs failed (may still work): ${e.message}") - } -}