diff --git a/build.gradle.kts b/build.gradle.kts index 0398f70..23b9a49 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,11 +53,6 @@ compose.desktop { application { mainClass = "com.bolenpad.photophetch.MainKt" - jvmArgs( - "--add-modules", "javafx.media,javafx.swing,javafx.graphics", - "--add-opens", "javafx.graphics/com.sun.javafx.application=ALL-UNNAMED", - ) - nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "PhotoPhetch" diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt index 3ae242a..44a0811 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/LivePhotoPlayer.kt @@ -44,6 +44,9 @@ 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) @@ -64,11 +67,10 @@ fun LivePhotoPlayer( val root = javafx.scene.layout.StackPane(mediaView) root.style = "-fx-background-color: black;" - jfxPanel.scene = javafx.scene.Scene(root) } catch (e: Exception) { - log.warn("LivePhotoPlayer: JavaFX Platform.runLater failed: ${e.message}") + log.warn("LivePhotoPlayer: Platform.runLater failed: ${e.message}") } } } catch (e: Exception) { @@ -92,3 +94,58 @@ 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}") + } +}