Kotlin Jetpack Compose Desktop打包FFmpeg的权限问题
解决方案
1. 程序化设置FFmpeg权限(跨平台)
问题核心:打包后的应用资源目录通常是只读状态(比如macOS的App Bundle、Windows的Program Files目录),无法直接修改其中文件的权限。正确做法是将资源中的二进制文件复制到用户可写的应用专属数据目录,再设置可执行权限。
修改你的FFmpegUtils实现,加入复制和权限设置逻辑:
import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption import org.jetbrains.compose.desktop.application.currentApplication object FFmpegUtils { // 获取应用专属的可写数据目录(自动适配跨平台路径) private val appDataDir = currentApplication().applicationInfo.dataDirectory.toFile() private val ffmpegDest = File(appDataDir, if (isWindows()) "ffmpeg.exe" else "ffmpeg") private val ffprobeDest = File(appDataDir, if (isWindows()) "ffprobe.exe" else "ffprobe") val FFmpegPath: String by lazy { setupBinary(if (isWindows()) "ffmpeg.exe" else "ffmpeg", ffmpegDest) ffmpegDest.path } val FFprobePath: String by lazy { setupBinary(if (isWindows()) "ffprobe.exe" else "ffprobe", ffprobeDest) ffprobeDest.path } private fun setupBinary(resourceName: String, destFile: File) { if (!destFile.exists()) { // 从资源目录读取二进制文件 val resourceStream = FFmpegUtils::class.java.classLoader.getResourceAsStream(resourceName) ?: throw RuntimeException("无法从资源中加载 $resourceName") // 确保目标目录存在 destFile.parentFile.mkdirs() // 复制文件到可写目录 Files.copy(resourceStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) resourceStream.close() // 针对非Windows平台设置可执行权限 if (!isWindows()) { destFile.setExecutable(true, false) // false表示所有用户都拥有执行权限 } } } private fun isWindows() = System.getProperty("os.name").lowercase().contains("win") }
关键说明:
currentApplication().applicationInfo.dataDirectory是Compose Desktop提供的跨平台可写目录,不同系统对应路径:- macOS:
~/Library/Application Support/你的应用名 - Windows:
C:\Users\你的用户名\AppData\Roaming\你的应用名 - Linux:
~/.local/share/你的应用名
- macOS:
- 首次启动时自动复制二进制文件,后续启动直接复用已复制的文件,避免重复操作。
- Windows平台无需额外设置可执行权限,只要文件是
.exe格式即可被系统识别。
2. 跨平台处理打包二进制的通用实践
(1)二进制文件的打包策略
为每个目标平台准备对应的FFmpeg/FFprobe二进制文件,在构建时根据平台自动打包对应版本:
在build.gradle.kts中配置:
import org.jetbrains.compose.desktop.application.dsl.TargetPlatform plugins { kotlin("jvm") id("org.jetbrains.compose") } compose.desktop { application { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) appResourcesRootDir.set(project.file("src/main/resources")) // 针对不同平台复制对应二进制到资源目录 distributions.forEach { dist -> val ffmpegSrc = when (dist.targetPlatform) { TargetPlatform.Windows -> project.file("bin/windows/ffmpeg.exe") TargetPlatform.MacOS -> project.file("bin/macos/ffmpeg") TargetPlatform.Linux -> project.file("bin/linux/ffmpeg") } val ffprobeSrc = when (dist.targetPlatform) { TargetPlatform.Windows -> project.file("bin/windows/ffprobe.exe") TargetPlatform.MacOS -> project.file("bin/macos/ffprobe") TargetPlatform.Linux -> project.file("bin/linux/ffprobe") } tasks.named("processResources") { dependsOn(copy { from(ffmpegSrc) into("${buildDir}/resources/main") rename { if (dist.targetPlatform.isWindows) "ffmpeg.exe" else "ffmpeg" } }) dependsOn(copy { from(ffprobeSrc) into("${buildDir}/resources/main") rename { if (dist.targetPlatform.isWindows) "ffprobe.exe" else "ffprobe" } }) } } } } }
推荐项目目录结构:
your-app/ ├── bin/ │ ├── windows/ │ │ ├── ffmpeg.exe │ │ └── ffprobe.exe │ ├── macos/ │ │ ├── ffmpeg │ │ └── ffprobe │ └── linux/ │ ├── ffmpeg │ └── ffprobe └── src/main/resources/
(2)文件操作的通用原则
- 禁止修改安装目录文件:应用安装目录通常是只读的,直接修改会触发权限错误或被系统阻止。
- 复用系统可写目录:优先使用系统提供的应用专属可写目录,避免使用临时目录(临时文件可能被系统自动清理)。
- 添加错误处理:在复制文件、设置权限、执行二进制时捕获异常,给用户明确的错误提示(比如"FFmpeg初始化失败,请检查应用权限")。
- 版本管理:如果后续需要更新二进制文件,可以在启动时检查版本号,自动替换旧版本的二进制。
(3)与ffmpeg-cli-wrapper的集成
确保使用FFmpegUtils提供的路径初始化FFmpeg实例:
import bramp.ffmpeg.FFmpeg import bramp.ffmpeg.FFprobe class VideoRepositoryImpl : VideoRepository { private val ffmpeg = FFmpeg(FFmpegUtils.FFmpegPath) private val ffprobe = FFprobe(FFmpegUtils.FFprobePath) override fun getVideoDuration(path: Path): Long { // 使用ffprobe获取视频时长的逻辑 val probeResult = ffprobe.probe(path.toString()) return probeResult.format.duration.toLong() } // 其他业务方法... }
内容的提问来源于stack exchange,提问作者FaroukAbichou
相关产品推荐
相关产品推荐

