最近在写一个自动匹配弹幕的动漫播放器,里面需要使用 FFmpeg 对视频进行解析,但发现如何根据不同的平台打包不同 FFmpeg 到 Electron 里,是个挺麻烦的问题,这篇文章就来讲述下我的解决思路。
安装
用户的电脑很有可能没有安装 FFmpeg,所以我们需要把 FFmpeg 打包进我们的应用里面。
想要在 Electron 开发环境里面导入 FFmpeg 还是比较简单的,只需要安装下面的包,然后就能够在 Electron 中使用了。
bash代码解读复制代码pnpm add -D @ffmpeg-installer/ffmpeg @ffprobe-installer/ffprobe fluent-ffmpeg
创建 ffmpeg.ts
,具体使用方式可以阅读 fluent-ffmpeg。
typescript 代码解读复制代码import ffmpegPath from '@ffmpeg-installer/ffmpeg' // 安装 ffmpeg 的二进制文件
import ffprobePath from '@ffprobe-installer/ffprobe' // 安装 ffprobe 的二进制文件
import ffmpeg from 'fluent-ffmpeg' // 一个封装了 ffmpeg API 的库,当然可以选择不安装,直接使用字符串拼接的方式调用
ffmpeg.setFfmpegPath(ffmpegPath.path)
ffmpeg.setFfprobePath(ffprobePath.path)
export default class FFmpeg {
ffmpeg: ffmpeg.FfmpegCommand
constructor(inputPath: string) {
this.ffmpeg = ffmpeg(inputPath)
}
}
之后我们在开发环境里面就能正常使用 FFmpeg 了。
打包
路径问题
我的项目是使用 electron builder 进行打包的(具体的 electron-builder.yml
配置可以在 Electron 代码签名和公证 中查看),打包之后你会发现项目是无法正确使用 FFmpeg,但在 dev 环境下到是正常的。
这是因为 ffmpeg 是二进制文件,会被打包进 app.asar.unpacked
而非 app.asar
从而导致 setFfmpegPath 路径出现问题,所以修改对应的 path 即可,这个问题在 @ffmpeg-installer/ffmpeg 中也有提到。
typescript 代码解读复制代码import ffmpegPath from '@ffmpeg-installer/ffmpeg'
import ffprobePath from '@ffprobe-installer/ffprobe'
import ffmpeg from 'fluent-ffmpeg'
ffmpeg.setFfmpegPath(ffmpegPath.path.replace('app.asar', 'app.asar.unpacked')) // 修改
ffmpeg.setFfprobePath(ffprobePath.path.replace('app.asar', 'app.asar.unpacked')) // 修改
export default class FFmpeg {
ffmpeg: ffmpeg.FfmpegCommand
constructor(inputPath: string) {
this.ffmpeg = ffmpeg(inputPath)
}
}
我使用的电脑是 MacBook Pro M1 Pro 即 macOS ARM64
打包完成之后,此时运行 ARM64 版本的 .app 是没有问题的,FFmpeg 也能正确运行。
FFmpeg 的架构版本问题
启动报错
但可不要高兴的太早,我们换一台运行 macOS x64 的电脑,运行刚才用 macOS ARM64 电脑打包出来的 x64 版本的 .app 就会直接报错,然后显示一个完全摸不到头脑的错误。
原因分析
我一开始看到这个错误也是完全懵逼的,使用 debugtron 对主线程进行调试也完全没有输出。之后尝试对包进行分析,发现 x64 版本的 .app 打包的 FFmpeg 竟然是 ARM64 版本的,这不报错才怪呢。
这里我便对 @ffmpeg-installer/ffmpeg
的实现感到了好奇,他是如何匹配不同的平台,从而安装对应其平台的 FFmpeg 二进制文件。通过阅读其源码:
json 代码解读复制代码{
"name": "@ffmpeg-installer/ffmpeg",
"optionalDependencies": {
"@ffmpeg-installer/darwin-arm64": "4.1.5",
"@ffmpeg-installer/darwin-x64": "4.1.0",
"@ffmpeg-installer/linux-arm": "4.1.3",
"@ffmpeg-installer/linux-arm64": "4.1.4",
"@ffmpeg-installer/linux-ia32": "4.1.0",
"@ffmpeg-installer/linux-x64": "4.1.0",
"@ffmpeg-installer/win32-ia32": "4.1.0",
"@ffmpeg-installer/win32-x64": "4.1.0"
}
}
json 代码解读复制代码{
"name": "@ffmpeg-installer/darwin-x64",
"os": [
"darwin"
],
"cpu": [
"x64"
],
}
发现@ffmpeg-installer/ffmpeg
封装了多个平台 FFmpeg 依赖,然后放入 optionalDependencies
中。每个平台的 FFmpeg 包再通过设置 cpu + os
字段,从而实现用户安装 @ffmpeg-installer/ffmpeg
即可匹配用户系统,来安装对应的 ffmpeg,这也让我涨知识了。
因此也难怪 x64 版本的 .app 打包的 FFmpeg 是 ARM64 版本的,因为我们在最开始 pnpm install 的时候,就只安装了对应操作系统的 FFmpeg,build 的时候也只能打包当前安装的 FFmpeg。
举个例子,我是 ARM64 macOS, pnpm install 的时候只会安装 ARM 版本 FFmpeg,打包 x64 的时候,当然也只能打包 ARM 版本 FFmpeg 了,从而导致的错误。
整理思路
那么我们的思路就很明确了:
- ARM64 macOS -> 打包 ARM64 应用 -> 使用 ARM64 FFmpeg
- ARM64 macOS -> 打包 x64 应用 -> 使用 x64 FFmpeg
同理:
- x64 macOS -> 打包 ARM64 应用 -> 使用 ARM64 FFmpeg
- x64 macOS -> 打包 x64 应用 -> 使用 x64 FFmpeg
那么如何实现呢?
这里思路完全是自己想的,或许有更好的方法,也请多多指教。
说一下我的思路,首先我们在 pnpm install 的时候只安装当前操作系统的 FFmpeg 是不变的,这样可以节约我们电脑的空间和安装依赖的速度。
之后只需要在执行 pnpm build:mac
的时候,执行一个安装 mac 平台全部架构的 FFmpeg 依赖脚本就可以了。
解决问题
编写 scripts/install-darwin-deps.js
javascript 代码解读复制代码/* eslint-disable no-console */
import { exec } from 'node:child_process'
import os from 'node:os'
const platform = os.platform()
if (platform === 'darwin') {
console.log('Detected macOS, installing darwin dependencies...')
// 为了在 macos arm64 架构下进行打包 x64 架构的 APP, 所以需要同时安装 x64 arm64 架构的 ffmpeg 和 ffprobe
exec(
'pnpm i @ffmpeg-installer/darwin-x64@^4.1.0 @ffprobe-installer/darwin-x64@^5.1.0 @ffmpeg-installer/darwin-arm64@^4.1.5 @ffprobe-installer/darwin-arm64@^5.0.1 -D',
(err, stdout, stderr) => {
if (err) {
console.error(`Error installing optional dependencies: ${stderr}`)
throw new Error('Error installing optional dependencies')
} else {
console.log(`Optional dependencies installed: ${stdout}`)
}
},
)
} else {
console.log('Non-macOS platform detected, skipping optional darwin installation.')
}
之后在 package.json 里面加上 "build:mac": "node scripts/install-darwin-deps.js && electron-vite build && electron-builder --mac --publish never"
即可。
执行打包命令之后,macOS x64 也是正确运行 macOS ARM64 打包出来的 x64 版本的 xxx.app ,不再会出现之前那个摸不着头脑报错了。
不同平台只打包对应的 FFmpeg
这里新的问题有又出现了,我们发现当前 xxx.dmg 包体积大了很多,那是因为所有平台的 FFmpeg 都被打包进去了。例如,ARM64 版本 xxx.app 把 x64 和 ARM64 FFmpeg 都打包进去了。
这里我们得写一个脚本,在 electron builder 打包之后,把与目标平台不相符的 FFmpeg 给删除掉,编写 scripts/cleaned-unused-arch-deps.js
javascript 代码解读复制代码/* eslint-disable no-console */
import fs from 'node:fs'
import path from 'node:path'
export default async function cleanDeps(context) {
const { packager, arch, appOutDir } = context
const platform = packager.platform.nodeName
if (platform !== 'darwin') {
return
}
const archMap = {
1: 'x64',
3: 'arm64',
}
const currentArch = archMap[arch]
if (!currentArch) {
return
}
const unpackedPath = path.resolve(
appOutDir,
'Marchen.app',
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules',
)
if (!fs.existsSync(unpackedPath)) {
return
}
const ffmpegPath = path.resolve(unpackedPath, '@ffmpeg-installer')
const ffprobePath = path.resolve(unpackedPath, '@ffprobe-installer')
if (!fs.existsSync(ffmpegPath) || !fs.existsSync(ffprobePath)) {
return
}
const removeUnusedArch = (basePath, unusedArch) => {
const unusedPath = path.resolve(basePath, `darwin-${unusedArch}`)
if (fs.existsSync(unusedPath)) {
fs.rmSync(unusedPath, { recursive: true })
}
}
if (currentArch === 'x64') {
removeUnusedArch(ffmpegPath, 'arm64')
removeUnusedArch(ffprobePath, 'arm64')
} else if (currentArch === 'arm64') {
removeUnusedArch(ffmpegPath, 'x64')
removeUnusedArch(ffprobePath, 'x64')
}
console.log('Cleaned unused arch dependencies.')
}
之后在 electron-builder.yml 里面使用 afterPack: scripts/cleaned-unused-arch-deps.js
导入脚本。
然后执行 pnpm build:mac
就实现了不同平台只打包对应的 FFmpeg,并且运行都正常了。查看包内容,发现确实只包含了目标平台的 FFmpeg。
评论记录:
回复评论: