From de5dc34cb7155f50bdad44c48d8aba85642fa89a Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 18 May 2026 15:16:23 +0300 Subject: [PATCH] fix(desktop): fix macOS VLC bundled discovery and video rendering MacOsVlcDiscoverer.setPluginPath() called LibC.INSTANCE.setenv() via JNA, but macOS 13+ uses versioned symbols (setenv$3b99ba0d) that JNA can't resolve, breaking all video playback without system VLC. Changes: - Replace LibC.setenv with direct JNA Function.getFunction("c","setenv") call that bypasses the problematic interface binding - Store discoveredPluginPath for --plugin-path factory arg fallback - Add -Dvlc.plugin.path JVM property as ultimate fallback - Delete stale VLC plugin cache on macOS before factory creation - Pass --plugin-path to audio factory too when env var fails - Add jdk.unsupported module to jlink (VLCJ ByteBufferFactory needs sun.misc.Unsafe for video frame buffer allocation) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../desktop/service/media/VlcjPlayerPool.kt | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt index 18e08c306d..50ea042e58 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VlcjPlayerPool.kt @@ -55,6 +55,9 @@ object VlcjPlayerPool { private val idleThumbPlayers = ConcurrentLinkedQueue() private const val MAX_THUMB_POOL_SIZE = 2 + // Cached plugin path for audio factory creation (set during init) + private var cachedPluginPath: String? = null + // Audio player pool (shared factory with --no-video) private var audioFactory: MediaPlayerFactory? = null private val allAudioPlayers = mutableListOf() @@ -76,12 +79,13 @@ object VlcjPlayerPool { return try { // Try bundled VLC first, then fall through to system VLC + val macOsDiscoverer = MacOsVlcDiscoverer() val discovery = try { val nd = NativeDiscovery( BundledVlcDiscoverer(), - MacOsVlcDiscoverer(), + macOsDiscoverer, ) val found = nd.discover() if (found) { @@ -99,7 +103,34 @@ object VlcjPlayerPool { val systemDiscovery = NativeDiscovery().discover() println("VLC: system discovery ${if (systemDiscovery) "succeeded" else "failed"}") } - val f = MediaPlayerFactory("--no-xlib") + + // Delete stale VLC plugin cache on macOS to avoid spam warnings + if ("mac" in System.getProperty("os.name").lowercase()) { + try { + val cacheDir = java.io.File(System.getProperty("user.home"), "Library/Caches/org.videolan.vlc") + cacheDir.listFiles()?.filter { it.name.startsWith("plugins") }?.forEach { it.delete() } + } catch (_: Throwable) { + // Best-effort cache cleanup + } + } + + // Build factory args — add --plugin-path fallback if env var wasn't set + val factoryArgs = mutableListOf("--no-xlib") + if (!macOsDiscoverer.envVarSet) { + val pluginPath = + macOsDiscoverer.discoveredPluginPath + ?: System.getProperty("vlc.plugin.path") + ?: VlcResourceResolver.findVlcDir()?.let { "${it.absolutePath}/plugins" } + if (pluginPath != null) { + factoryArgs += "--plugin-path=$pluginPath" + println("VLC: using --plugin-path fallback: $pluginPath") + } + } + + cachedPluginPath = macOsDiscoverer.discoveredPluginPath + ?: System.getProperty("vlc.plugin.path") + + val f = MediaPlayerFactory(*factoryArgs.toTypedArray()) factory = f available.set(true) println("VLC: MediaPlayerFactory created successfully") @@ -184,7 +215,9 @@ object VlcjPlayerPool { val af = audioFactory ?: try { - MediaPlayerFactory("--no-video", "--no-xlib").also { audioFactory = it } + val audioArgs = mutableListOf("--no-video", "--no-xlib") + cachedPluginPath?.let { audioArgs += "--plugin-path=$it" } + MediaPlayerFactory(*audioArgs.toTypedArray()).also { audioFactory = it } } catch (_: Throwable) { return null }