Use JavaFX Gradle plugin for proper module path setup

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.
This commit is contained in:
Kyle Bolen
2026-07-30 06:25:38 +00:00
parent 3c2ec07bdb
commit bbb3bb1aae
3 changed files with 9 additions and 73 deletions

View File

@@ -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"
// JavaFX media — configured via the javafx plugin block below
}
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 {
version = "21.0.3"
modules = listOf("javafx.media", "javafx.swing", "javafx.graphics", "javafx.controls", "javafx.base")
}
compose.desktop {

View File

@@ -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" }

View File

@@ -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<String>
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}")
}
}