mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
Drops uk.co.caprica:vlcj 4.8.3 (GPL-3.0) from desktopApp and replaces it
with an MIT-dominant stack:
- Video / audio playback: io.github.kdroidfilter:composemediaplayer:0.10.0
(MIT) — OS-native backends (Media Foundation on Windows, AVFoundation on
macOS, GStreamer on Linux). First-class Compose VideoPlayerSurface.
- Thumbnail extraction: org.jcodec:jcodec(+javase):0.2.5 (BSD-2) primary
H.264 path, raw ProcessBuilder FFmpeg fallback for HEVC / VP9 / AV1 /
HLS / non-faststart MP4.
- Binary SPDX: MIT AND LGPL-2.1-or-later AND BSD-2-Clause AND Apache-2.0.
rpmLicenseType updated accordingly (previously misdeclared as MIT
while shipping GPLv3 vlcj).
Code changes:
- Deleted: VlcjPlayerPool, MacOsVlcDiscoverer, BundledVlcDiscoverer,
VlcResourceResolver
- Rewrote: GlobalMediaPlayer (kdroidFilter engine + snapshotFlow-based
state sync into the preserved MediaPlaybackState contract);
VideoThumbnailCache (JCodec → ProcessBuilder ffmpeg cascade, with a
hard 4 MiB download cap, Content-Type sniff to reject HTML error
pages, and cleanup of zero-byte cache entries); DesktopVideoPlayer
(mounts VideoPlayerSurface for the active URL, codec/network error
UX with "Open in default player" fallback)
- Updated: NowPlayingBar + GlobalFullscreenOverlay to render
VideoPlayerSurface directly (drops the videoFrame ImageBitmap relay)
- Main.kt: drops vlcj pre-init / shutdown calls (kdroidFilter lazy-loads
natives + registers its own shutdown hook on Windows)
Build / packaging:
- Removes ir.mahozad.vlc-setup plugin + vlcSetup{} block + the per-OS
bundled VLC tree + the -Dvlc.plugin.path JVM arg
- Adds NOTICE.md + per-component LICENSE-*.txt under appResources/common
(LGPL-2.1 license text is a placeholder — replace with verbatim FSF
text before release)
- Adds per-OS LGPL FFmpeg drop-in directories with README pointing at
the recommended LGPL binary source (osxexperts.net / Crigges Windows
LGPL build)
- Adds Flathub manifest skeleton (Gitnuro-style: org.freedesktop.Platform
24.08 + openjdk21 extension + org.freedesktop.Platform.ffmpeg-full
add-extension for patent codecs)
- AppRun: drops VLC LD_LIBRARY_PATH / VLC_PLUGIN_PATH env wiring
Verified on macOS arm64:
- ./gradlew :desktopApp:compileKotlin BUILD SUCCESSFUL
- ./gradlew :desktopApp:test BUILD SUCCESSFUL
- ./gradlew :desktopApp:spotlessApply clean
- Smoke launch: no VLC/vlcj/libvlc log lines, kdroidFilter native
library extracts to ~/.cache/composemediaplayer/native/, thumbnail
cache populates at ~/.cache/amethyst-desktop/video-thumbs/ with the
4 MiB cap enforced
- H.264 MP4 playback (active + thumbnail extraction) confirmed
- VP9-in-WebM playback fails on macOS as AVFoundation cannot decode it —
expected codec gap; surfaced via PlaybackErrorMessage + "Open in
default player" handoff in DesktopVideoPlayer
Docs:
- docs/plans/2026-06-11-feat-replace-vlcj-with-kdroidfilter-plan.md
- docs/plans/2026-06-11-vlcj-replacement-testing-sheet.md
245 lines
9.9 KiB
Kotlin
245 lines
9.9 KiB
Kotlin
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
import java.nio.file.Files
|
|
|
|
plugins {
|
|
alias(libs.plugins.jetbrainsKotlinJvm)
|
|
alias(libs.plugins.composeMultiplatform)
|
|
alias(libs.plugins.jetbrainsComposeCompiler)
|
|
}
|
|
|
|
// RPM rejects dashes in version strings — replace with tilde (~) which RPM uses
|
|
// for prerelease ordering: 1.08.0~rc1 < 1.08.0 per RPM version comparison rules.
|
|
val appVersion: String = project.version.toString()
|
|
|
|
sourceSets {
|
|
main {
|
|
kotlin.srcDir("src/jvmMain/kotlin")
|
|
resources.srcDir("src/jvmMain/resources")
|
|
}
|
|
test {
|
|
kotlin.srcDir("src/jvmTest/kotlin")
|
|
resources.srcDir("src/jvmTest/resources")
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(21)
|
|
}
|
|
|
|
dependencies {
|
|
implementation(compose.desktop.currentOs)
|
|
implementation(libs.jetbrains.compose.material3)
|
|
implementation(libs.jetbrains.compose.components.resources)
|
|
|
|
// Quartz Nostr library (will use JVM target)
|
|
implementation(project(":quartz"))
|
|
|
|
// Commons library
|
|
implementation(project(":commons"))
|
|
|
|
// Lifecycle ViewModel (needed to access ViewModel supertype from commons)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
// Coroutines
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
implementation(libs.kotlinx.coroutines.swing)
|
|
|
|
// Networking
|
|
implementation(libs.okhttp)
|
|
|
|
// JSON
|
|
implementation(libs.jackson.module.kotlin)
|
|
|
|
// Image loading (Coil3 — explicit because commons uses implementation, not api)
|
|
implementation(libs.coil.compose)
|
|
implementation(libs.coil.okhttp)
|
|
implementation(libs.coil.svg)
|
|
|
|
// Video / audio playback — MIT, OS-native backends (MF / AVFoundation / GStreamer)
|
|
implementation(libs.composemediaplayer)
|
|
|
|
// Thumbnail extraction — JCodec (pure-Java H.264). LGPL FFmpeg subprocess
|
|
// for non-H.264 / HLS fallback is invoked via plain ProcessBuilder; no
|
|
// wrapper library needed (see VideoThumbnailCache.runFfmpegToImage).
|
|
implementation(libs.jcodec)
|
|
implementation(libs.jcodec.javase)
|
|
|
|
// EXIF stripping (lossless)
|
|
implementation(libs.commons.imaging)
|
|
|
|
// Collections
|
|
implementation(libs.kotlinx.collections.immutable)
|
|
implementation(libs.androidx.collection)
|
|
|
|
// Tor daemon (desktop embedded via kmp-tor)
|
|
implementation(libs.kmp.tor.runtime)
|
|
implementation(libs.kmp.tor.resource.exec.tor)
|
|
|
|
// SLF4J no-op — silence "No SLF4J providers" warnings from transitive deps
|
|
implementation(libs.slf4j.nop)
|
|
|
|
// QR code generation (ZXing core)
|
|
implementation(libs.zxing)
|
|
|
|
// Testing
|
|
testImplementation(libs.kotlin.test)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.mockk)
|
|
testImplementation(libs.okhttp)
|
|
|
|
// Compose UI testing (createComposeRule / onNodeWithText / etc.)
|
|
testImplementation(compose.desktop.uiTestJUnit4)
|
|
}
|
|
|
|
compose.desktop {
|
|
application {
|
|
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
|
|
jvmArgs += "--add-opens=java.base/java.nio=ALL-UNNAMED"
|
|
|
|
jvmArgs += "-Xmx2g"
|
|
|
|
// Forward platform-preview overrides from the gradle invocation to the
|
|
// launched app's JVM so `./gradlew :desktopApp:run -Damethyst.platform=GNOME`
|
|
// works in addition to the env-var form (`AMETHYST_PLATFORM=GNOME`).
|
|
listOf("amethyst.platform", "amethyst.appearance", "amethyst.accent").forEach { key ->
|
|
System.getProperty(key)?.let { jvmArgs += "-D$key=$it" }
|
|
}
|
|
|
|
nativeDistributions {
|
|
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
|
// Output of ./gradlew suggestRuntimeModules (+ java.management already present)
|
|
modules(
|
|
"java.instrument", // Runtime instrumentation (agent/profiler hooks)
|
|
"java.management", // Required by kmp-tor TorRuntime
|
|
"java.prefs", // java.util.prefs (desktop persistence)
|
|
"java.sql", // JDBC metadata (Jackson, SQLite driver)
|
|
"jdk.security.auth", // JAAS authentication callbacks
|
|
"jdk.unsupported", // sun.misc.Unsafe (secp256k1-kmp-jni-jvm, JNA)
|
|
)
|
|
|
|
packageName = "Amethyst"
|
|
packageVersion = appVersion
|
|
description = "Nostr client for desktop"
|
|
vendor = "Amethyst Contributors"
|
|
|
|
macOS {
|
|
bundleID = "com.vitorpamplona.amethyst.desktop"
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
|
}
|
|
|
|
windows {
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
|
|
menuGroup = "Amethyst"
|
|
upgradeUuid = "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
|
|
}
|
|
|
|
linux {
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
|
menuGroup = "Network"
|
|
appCategory = "Network"
|
|
debMaintainer = "vitor@vitorpamplona.com"
|
|
// SPDX compound expression. Bundled components:
|
|
// MIT — Amethyst + kdroidFilter ComposeMediaPlayer
|
|
// LGPL-2.1-or-later — FFmpeg (LGPL build, bundled per OS for thumbnail fallback) +
|
|
// GStreamer (Linux runtime dep, system-installed)
|
|
// BSD-2-Clause — JCodec
|
|
// Apache-2.0 — Jaffree + many transitive Java libraries
|
|
rpmLicenseType = "MIT AND LGPL-2.1-or-later AND BSD-2-Clause AND Apache-2.0"
|
|
// RPM version: replace dashes with tilde (1.08.0~rc1 < 1.08.0 per RPM ordering).
|
|
rpmPackageVersion = appVersion.replace("-", "~")
|
|
}
|
|
}
|
|
|
|
// Compose Multiplatform 1.11.0 wired ProGuard 7.7.0 into the release
|
|
// build. The Android (mobile) module already solved the same class of
|
|
// problems with `-dontobfuscate` plus global `-keepnames` / `-keep enum`
|
|
// rules (see `amethyst/proguard-rules.pro`). We mirror that strategy in
|
|
// `compose-rules.pro` so the desktop release survives JNI callbacks
|
|
// (secp256k1-kmp, sqlite-bundled, jkeychain, kdroidFilter native)
|
|
// and reflection-heavy libraries (Jackson, JNA) without renaming.
|
|
//
|
|
// Shrink and optimize stay ON. One ProGuard optimize sub-pass is
|
|
// disabled in `compose-rules.pro` to avoid a generated okio bridge
|
|
// whose declared return type the JVM verifier rejects (R8 doesn't hit
|
|
// this — it generates bridges differently from ProGuard).
|
|
buildTypes.release.proguard {
|
|
version.set("7.9.1") // Kotlin 2.3 metadata support
|
|
configurationFiles.from(project.file("compose-rules.pro"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- AppImage packaging (Linux) ---
|
|
//
|
|
// Compose Multiplatform's TargetFormat.AppImage is known-broken in 1.10.x (CMP-7101).
|
|
// Instead: wrap `createReleaseDistributable` output with `appimagetool`, which
|
|
// just packages an AppDir as-is. We deliberately avoid `linuxdeploy` here —
|
|
// linuxdeploy auto-walks every binary in the AppDir with ldd to bundle deps,
|
|
// but jpackage already ships a self-contained tree we don't want it touching
|
|
// (the bundled JRE has libjvm.so under usr/lib/runtime/lib/server/ while sibling
|
|
// libs use $ORIGIN RPATH — ldd can't resolve without help).
|
|
// appimagetool sidesteps that — it only embeds the AppDir into a SquashFS,
|
|
// runtime-prepended, signed AppImage. AppRun handles LD_LIBRARY_PATH at launch.
|
|
//
|
|
// kdroidFilter (video/audio) links against system GStreamer at runtime — the
|
|
// AppImage does not bundle GStreamer; the host system must have it installed.
|
|
//
|
|
// Build inputs live in desktopApp/packaging/appimage/:
|
|
// - AppRun shell launcher
|
|
// - amethyst.desktop XDG desktop entry
|
|
// - amethyst.png 512x512 icon
|
|
//
|
|
// appimagetool binary is fetched by CI (SHA-verified) into
|
|
// desktopApp/packaging/appimage/ as appimagetool-x86_64.AppImage.
|
|
// BUILDING.md documents local-dev fetch.
|
|
val createReleaseAppImage by tasks.registering(Exec::class) {
|
|
group = "compose desktop"
|
|
description = "Package createReleaseDistributable output into a Linux AppImage via appimagetool."
|
|
dependsOn("createReleaseDistributable")
|
|
|
|
val distDir = layout.buildDirectory.dir("compose/binaries/main-release/app/Amethyst")
|
|
val appDir = layout.buildDirectory.dir("appimage/Amethyst.AppDir")
|
|
val outFile = layout.buildDirectory.file("appimage/Amethyst-$appVersion-x86_64.AppImage")
|
|
val toolRoot = layout.projectDirectory.dir("packaging/appimage")
|
|
val appimagetool = toolRoot.file("appimagetool-x86_64.AppImage")
|
|
|
|
inputs.dir(distDir)
|
|
inputs.dir(toolRoot)
|
|
outputs.file(outFile)
|
|
|
|
doFirst {
|
|
val dir = appDir.get().asFile
|
|
dir.deleteRecursively()
|
|
dir.mkdirs()
|
|
copy {
|
|
from(distDir) { into("usr") }
|
|
from(toolRoot.file("AppRun")) {
|
|
rename { "AppRun" }
|
|
filePermissions { unix("0755") }
|
|
}
|
|
from(toolRoot.file("amethyst.desktop"))
|
|
from(toolRoot.file("amethyst.png"))
|
|
into(dir)
|
|
}
|
|
// DirIcon is used by desktop integrations (file managers, AppImageLauncher)
|
|
val dirIcon = File(dir, ".DirIcon")
|
|
if (dirIcon.exists()) dirIcon.delete()
|
|
Files.createSymbolicLink(dirIcon.toPath(), File("amethyst.png").toPath())
|
|
|
|
if (!appimagetool.asFile.canExecute()) {
|
|
appimagetool.asFile.setExecutable(true)
|
|
}
|
|
}
|
|
|
|
commandLine(
|
|
appimagetool.asFile.absolutePath,
|
|
appDir.get().asFile.absolutePath,
|
|
outFile.get().asFile.absolutePath,
|
|
)
|
|
environment("ARCH", "x86_64")
|
|
// Bypass FUSE requirement on CI runners (ubuntu-latest lacks libfuse.so.2).
|
|
// AppImage standard env var: extracts + runs without mounting.
|
|
environment("APPIMAGE_EXTRACT_AND_RUN", "1")
|
|
}
|