Audited all 143 plan files across the 10 plans/ folders. Each plan now carries a Status header (shipped | in-progress | queued | abandoned) backed by codebase evidence, and every folder has a README.md index grouping plans by status. Shipped plans were moved into a per-folder plans/archive/ (via git mv, history preserved) so each plans/ folder surfaces only live work: shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4 docs/plans/ is the frozen legacy folder; its plans were stamped and indexed in place (48 of 52 archived) but it remains closed to new plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
8.8 KiB
title, type, status, date, deepened, origin
| title | type | status | date | deepened | origin |
|---|---|---|---|---|---|
| fix: macOS bundled VLC discovery broken by versioned setenv symbol | fix | active | 2026-05-18 | 2026-05-18 | docs/brainstorms/2026-05-18-fix-macos-vlc-bundling-brainstorm.md |
fix: macOS bundled VLC discovery broken by versioned setenv symbol
Status: abandoned — Superseded — VLC/VLCJ was removed entirely (kdroidFilter migration); MacOsVlcDiscoverer no longer exists, so the setenv fix is moot. Audited 2026-06-30.
Enhancement Summary
Deepened on: 2026-05-18 Research agents used: VLC --plugin-path audit, macOS JNA symbol research, VlcjPlayerPool init flow audit
Key Improvements from Research
--plugin-pathfactory arg is unreliable — VLC needsVLC_PLUGIN_PATHenv var set beforelibvlc_new()--reset-plugins-cacheis VLC CLI-only, not available via libvlc/vlcj factory args- Simpler fix: replace
LibC.INSTANCE.setenv()with JNA-free env var setting
Overview
Bundled VLC video playback is broken on macOS release builds.
MacOsVlcDiscoverer.setPluginPath() calls LibC.INSTANCE.setenv() via JNA,
but macOS 13+ uses versioned C symbols (setenv$3b99ba0d) that JNA's dlsym
cannot resolve. Without system VLC installed, video is completely unavailable.
(See brainstorm: docs/brainstorms/2026-05-18-fix-macos-vlc-bundling-brainstorm.md)
Problem Statement
VLC: bundled discovery threw Error looking up function 'setenv$3b99ba0d':
dlsym(0x..., setenv$3b99ba0d): symbol not found
VLC: init failed — ...DiscoveryDirectoryProvider: Provider ...not found
- Affects all macOS DMG users without system VLC.app installed
- Audio playback works (separate factory, no plugin path needed for audio codecs)
- Graceful degradation exists (shows "Install VLC" message) but shouldn't be needed
Proposed Solution (Updated from Research)
Replace LibC.INSTANCE.setenv() with a JNA-free approach to set the env var.
VLC requires VLC_PLUGIN_PATH as a process-level environment variable BEFORE
libvlc_new() (called by MediaPlayerFactory). The --plugin-path factory arg
is NOT reliably forwarded to the plugin scanner. So we must set the actual env var,
just without using JNA's broken LibC.setenv binding.
Approach: Use JNA's lower-level Native.getLibrary to call setenv directly
Instead of LibC.INSTANCE.setenv() which goes through vlcj's LibC interface
binding (which triggers the versioned symbol lookup), use JNA's Function.getFunction
to call setenv from libc directly without the problematic interface mapping:
override fun setPluginPath(pluginPath: String?): Boolean {
if (pluginPath == null) return false
return try {
// Call setenv directly via JNA Function API, bypassing LibC interface
// which fails on macOS 13+ due to versioned symbol lookup
val setenv = com.sun.jna.Function.getFunction("c", "setenv")
setenv.invokeInt(arrayOf(PLUGIN_ENV_NAME, pluginPath, 1)) == 0
} catch (e: Throwable) {
// Fallback: set as JVM system property for factory arg approach
System.setProperty("vlc.plugin.path", pluginPath)
true
}
}
If that still hits the symbol issue, the alternative fallback is:
override fun setPluginPath(pluginPath: String?): Boolean {
if (pluginPath == null) return false
// Store for VlcjPlayerPool to pass as --plugin-path factory arg
discoveredPluginPath = pluginPath
return true
}
Then in VlcjPlayerPool.init(), pass --plugin-path to both factories as belt-and-suspenders.
Plugin Cache Fix
Since --reset-plugins-cache is VLC CLI-only (not available via libvlc), fix stale
cache by deleting the cache file before factory creation:
// Delete stale VLC plugin cache before factory init
val cacheDir = File(System.getProperty("user.home"), "Library/Caches/org.videolan.vlc")
cacheDir.listFiles()?.filter { it.name.startsWith("plugins") }?.forEach { it.delete() }
Implementation
File 1: MacOsVlcDiscoverer.kt
Current (line 55):
override fun setPluginPath(pluginPath: String?): Boolean =
LibC.INSTANCE.setenv(PLUGIN_ENV_NAME, pluginPath, 1) == 0
Change to:
var discoveredPluginPath: String? = null
private set
override fun setPluginPath(pluginPath: String?): Boolean {
if (pluginPath == null) return false
discoveredPluginPath = pluginPath
return try {
// Direct JNA Function call bypasses vlcj's LibC interface binding
// which fails on macOS 13+ (versioned symbol setenv$3b99ba0d)
val setenv = com.sun.jna.Function.getFunction("c", "setenv")
setenv.invokeInt(arrayOf(PLUGIN_ENV_NAME, pluginPath, 1)) == 0
} catch (_: Throwable) {
// If JNA call fails, store path for --plugin-path fallback
false
}
}
Remove import: uk.co.caprica.vlcj.binding.lib.LibC
File 2: VlcjPlayerPool.kt
Changes to init() (lines 77-113):
- Hold reference to
MacOsVlcDiscovererfor plugin path access:
val macOsDiscoverer = MacOsVlcDiscoverer()
val nd = NativeDiscovery(BundledVlcDiscoverer(), macOsDiscoverer)
- Build factory args with plugin path fallback:
val factoryArgs = mutableListOf("--no-xlib")
// If setenv failed, pass --plugin-path as fallback
val pluginPath = macOsDiscoverer.discoveredPluginPath
?: System.getProperty("vlc.plugin.path")
?: VlcResourceResolver.findVlcDir()?.let { "${it.absolutePath}/plugins" }
if (pluginPath != null && !envVarSetSuccessfully) {
factoryArgs += "--plugin-path=$pluginPath"
}
val f = MediaPlayerFactory(*factoryArgs.toTypedArray())
- Delete stale plugin cache before factory creation (macOS only):
if ("mac" in System.getProperty("os.name").lowercase()) {
val cacheDir = File(System.getProperty("user.home"), "Library/Caches/org.videolan.vlc")
cacheDir.listFiles()?.filter { it.name.startsWith("plugins") }?.forEach { it.delete() }
}
- Audio factory also gets plugin path if env var wasn't set:
val audioArgs = mutableListOf("--no-video", "--no-xlib")
if (pluginPath != null && !envVarSetSuccessfully) {
audioArgs += "--plugin-path=$pluginPath"
}
MediaPlayerFactory(*audioArgs.toTypedArray()).also { audioFactory = it }
File 3: desktopApp/build.gradle.kts
Add JVM property as ultimate fallback:
jvmArgs += "-Dvlc.plugin.path=\$APPDIR/resources/vlc/plugins"
Edge Cases from Research
| Edge Case | Handling |
|---|---|
Function.getFunction("c", "setenv") also fails on macOS |
Caught by try/catch, falls back to --plugin-path factory arg |
--plugin-path not honored by VLC 3.0.20 |
JVM property -Dvlc.plugin.path as ultimate fallback |
| Audio factory created later without plugin path | Audio factory also receives --plugin-path if env var wasn't set |
findVlcDir() returns root, not plugins dir |
Append /plugins when building --plugin-path value |
| VLC plugin cache stale after VLC update | Cache deleted on startup before factory creation |
| NativeDiscovery swallows MacOsVlcDiscoverer ref | Restructured to hold ref before passing to NativeDiscovery |
Acceptance Criteria
- Video plays in release DMG on macOS without system VLC installed
- No
setenvsymbol errors in console output - No stale plugin cache warnings on launch
- Audio playback still works
- Linux/Windows builds unaffected
./gradlew :desktopApp:run(debug) still works- Fallback chain works: direct setenv → --plugin-path → JVM property
Risks
| Risk | Severity | Mitigation |
|---|---|---|
Function.getFunction("c", "setenv") may also hit versioned symbol |
Medium | Triple fallback: direct call → --plugin-path → JVM property |
--plugin-path ignored by some VLC builds |
Low | Env var approach is primary, --plugin-path is fallback only |
| Plugin cache deletion too aggressive | Low | Only deletes plugins* files in VLC cache dir, not other data |
Sources
- Origin brainstorm: docs/brainstorms/2026-05-18-fix-macos-vlc-bundling-brainstorm.md
- Research: VLC requires
VLC_PLUGIN_PATHenv var beforelibvlc_new()—--plugin-pathnot reliably forwarded - Research:
--reset-plugins-cacheis CLI-only, not available via libvlc/vlcj - Research:
setPluginPath()called DURINGnd.discover(), before factory creation - Research: macOS 13+ versioned symbols affect JNA's
LibCinterface, butFunction.getFunctionmay bypass it MacOsVlcDiscoverer.kt:55— failingsetenvcallVlcjPlayerPool.kt:68-114— init flowVlcResourceResolver.kt— returns VLC root dir (not plugins subdir)- JNA Issue #1423 — macOS symbol resolution changes
- Guardsquare/proguard#460 — related ProGuard bytecode rewriting