update
This commit is contained in:
parent
598d51eba4
commit
5bf9e8b22d
File diff suppressed because one or more lines are too long
1
wails/assets/assets/index-BaD48VVT.css
Normal file
1
wails/assets/assets/index-BaD48VVT.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -20,8 +20,8 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="./assets/index-DSuQhuGl.js"></script>
|
<script type="module" crossorigin src="./assets/index-IwiMqFON.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BDrFF8AO.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BaD48VVT.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
14
wails/ffmpeg_darwin.go
Normal file
14
wails/ffmpeg_darwin.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed resources/ffmpeg/darwin/*
|
||||||
|
var embeddedFFmpeg embed.FS
|
||||||
|
|
||||||
|
func getEmbeddedFFmpeg() embed.FS {
|
||||||
|
return embeddedFFmpeg
|
||||||
|
}
|
||||||
14
wails/ffmpeg_default.go
Normal file
14
wails/ffmpeg_default.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build !darwin && !windows && !linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed resources/ffmpeg
|
||||||
|
var embeddedFFmpeg embed.FS
|
||||||
|
|
||||||
|
func getEmbeddedFFmpeg() embed.FS {
|
||||||
|
return embeddedFFmpeg
|
||||||
|
}
|
||||||
14
wails/ffmpeg_linux.go
Normal file
14
wails/ffmpeg_linux.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed resources/ffmpeg/linux/*
|
||||||
|
var embeddedFFmpeg embed.FS
|
||||||
|
|
||||||
|
func getEmbeddedFFmpeg() embed.FS {
|
||||||
|
return embeddedFFmpeg
|
||||||
|
}
|
||||||
14
wails/ffmpeg_windows.go
Normal file
14
wails/ffmpeg_windows.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed resources/ffmpeg/windows/*
|
||||||
|
var embeddedFFmpeg embed.FS
|
||||||
|
|
||||||
|
func getEmbeddedFFmpeg() embed.FS {
|
||||||
|
return embeddedFFmpeg
|
||||||
|
}
|
||||||
@ -11,6 +11,10 @@ import (
|
|||||||
//go:embed assets
|
//go:embed assets
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
|
// getEmbeddedFFmpeg 在平台特定文件中实现(ffmpeg_darwin.go, ffmpeg_windows.go, ffmpeg_linux.go, ffmpeg_default.go)
|
||||||
|
// 根据编译时的 GOOS 自动选择对应的实现
|
||||||
|
// 注意:此函数没有在此文件中声明,而是在平台特定文件中声明和实现
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 检测开发模式并设置日志级别
|
// 检测开发模式并设置日志级别
|
||||||
if os.Getenv("DEV") == "true" {
|
if os.Getenv("DEV") == "true" {
|
||||||
@ -19,6 +23,9 @@ func main() {
|
|||||||
services.LogDebug("详细日志已启用")
|
services.LogDebug("详细日志已启用")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 FFmpeg 助手(传递嵌入的文件系统,根据编译平台自动选择)
|
||||||
|
services.InitFFmpegHelper(getEmbeddedFFmpeg())
|
||||||
|
|
||||||
// 创建服务
|
// 创建服务
|
||||||
services.LogDebug("开始创建服务...")
|
services.LogDebug("开始创建服务...")
|
||||||
videoService := services.NewVideoService()
|
videoService := services.NewVideoService()
|
||||||
|
|||||||
26
wails/resources/ffmpeg/.gitignore
vendored
Normal file
26
wails/resources/ffmpeg/.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 忽略 FFmpeg 二进制文件(文件太大,通常不应提交到 Git)
|
||||||
|
# 用户需要自行下载并放置二进制文件到编译时嵌入
|
||||||
|
|
||||||
|
# 根目录的二进制文件(向后兼容)
|
||||||
|
ffmpeg
|
||||||
|
ffprobe
|
||||||
|
ffmpeg.exe
|
||||||
|
ffprobe.exe
|
||||||
|
|
||||||
|
# 按平台组织的目录中的二进制文件
|
||||||
|
darwin/ffmpeg
|
||||||
|
darwin/ffprobe
|
||||||
|
windows/ffmpeg.exe
|
||||||
|
windows/ffprobe.exe
|
||||||
|
linux/ffmpeg
|
||||||
|
linux/ffprobe
|
||||||
|
|
||||||
|
# 但保留 README.md、.gitkeep 和目录结构
|
||||||
|
!README.md
|
||||||
|
!.gitkeep
|
||||||
|
!darwin/
|
||||||
|
!windows/
|
||||||
|
!linux/
|
||||||
|
!darwin/.gitkeep
|
||||||
|
!windows/.gitkeep
|
||||||
|
!linux/.gitkeep
|
||||||
1
wails/resources/ffmpeg/.gitkeep
Normal file
1
wails/resources/ffmpeg/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 保留此目录在 Git 中
|
||||||
158
wails/resources/ffmpeg/README.md
Normal file
158
wails/resources/ffmpeg/README.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# FFmpeg 二进制文件放置说明
|
||||||
|
|
||||||
|
此目录用于存放 FFmpeg 和 FFprobe 的二进制文件。这些文件会被**嵌入到 Go 程序**中,随应用一起分发。
|
||||||
|
|
||||||
|
**重要**:二进制文件会被编译到最终的可执行文件中,因此文件大小会增加。建议使用静态构建版本以减小体积。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
### ⭐ 推荐:按平台组织(实现自动平台选择)
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
└── ffmpeg/
|
||||||
|
├── darwin/ # macOS 版本(编译 macOS 时自动嵌入)
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ └── ffprobe
|
||||||
|
├── windows/ # Windows 版本(编译 Windows 时自动嵌入)
|
||||||
|
│ ├── ffmpeg.exe
|
||||||
|
│ └── ffprobe.exe
|
||||||
|
└── linux/ # Linux 版本(编译 Linux 时自动嵌入)
|
||||||
|
├── ffmpeg
|
||||||
|
└── ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- ✅ 每个平台只嵌入对应平台的二进制文件
|
||||||
|
- ✅ 减小最终可执行文件大小
|
||||||
|
- ✅ 避免跨平台打包时的混乱
|
||||||
|
- ✅ 使用 Go 构建标签自动选择
|
||||||
|
|
||||||
|
### 选项 2:所有平台通用(向后兼容)
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
└── ffmpeg/
|
||||||
|
├── ffmpeg # 当前平台的 ffmpeg(macOS/Linux)
|
||||||
|
├── ffmpeg.exe # 或 Windows 版本
|
||||||
|
├── ffprobe # 当前平台的 ffprobe(macOS/Linux)
|
||||||
|
└── ffprobe.exe # 或 Windows 版本
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:这种方式会嵌入所有文件,增加不必要的体积。
|
||||||
|
|
||||||
|
## 获取 FFmpeg 二进制文件
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 Homebrew 安装
|
||||||
|
brew install ffmpeg
|
||||||
|
|
||||||
|
# 复制到资源目录
|
||||||
|
cp /opt/homebrew/bin/ffmpeg resources/ffmpeg/ffmpeg
|
||||||
|
cp /opt/homebrew/bin/ffprobe resources/ffmpeg/ffprobe
|
||||||
|
|
||||||
|
# 或者如果是 Intel Mac
|
||||||
|
cp /usr/local/bin/ffmpeg resources/ffmpeg/ffmpeg
|
||||||
|
cp /usr/local/bin/ffprobe resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
1. 下载 FFmpeg Windows 构建版本:
|
||||||
|
- 访问 https://www.gyan.dev/ffmpeg/builds/
|
||||||
|
- 下载 ffmpeg-release-essentials.zip
|
||||||
|
- 解压后找到 `bin` 目录下的 `ffmpeg.exe` 和 `ffprobe.exe`
|
||||||
|
|
||||||
|
2. 复制到资源目录:
|
||||||
|
```powershell
|
||||||
|
copy ffmpeg.exe resources\ffmpeg\ffmpeg.exe
|
||||||
|
copy ffprobe.exe resources\ffmpeg\ffprobe.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install ffmpeg
|
||||||
|
|
||||||
|
# 复制到资源目录
|
||||||
|
cp /usr/bin/ffmpeg resources/ffmpeg/ffmpeg
|
||||||
|
cp /usr/bin/ffprobe resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
或者下载静态构建版本:
|
||||||
|
```bash
|
||||||
|
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||||
|
tar xf ffmpeg-release-amd64-static.tar.xz
|
||||||
|
cp ffmpeg-*-amd64-static/ffmpeg resources/ffmpeg/ffmpeg
|
||||||
|
cp ffmpeg-*-amd64-static/ffprobe resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **文件权限**:在 macOS/Linux 上,确保二进制文件有执行权限:
|
||||||
|
```bash
|
||||||
|
chmod +x resources/ffmpeg/ffmpeg
|
||||||
|
chmod +x resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **架构匹配**:
|
||||||
|
- macOS:确保使用正确的架构(Intel 或 Apple Silicon)
|
||||||
|
- Linux:确保使用正确的架构(amd64、arm64 等)
|
||||||
|
|
||||||
|
3. **依赖库**:静态构建版本通常更好,因为不需要外部依赖库。
|
||||||
|
|
||||||
|
4. **打包时**:在打包应用时,确保将此目录的内容包含到应用包中。
|
||||||
|
|
||||||
|
## 应用程序查找顺序
|
||||||
|
|
||||||
|
1. **系统 PATH**:首先检查系统 PATH 中是否有 ffmpeg/ffprobe
|
||||||
|
2. **嵌入资源**:如果未找到,从嵌入的 Go 资源中提取并解压到临时目录
|
||||||
|
3. **文件系统资源**:最后尝试从打包的资源目录查找(如果嵌入资源未找到)
|
||||||
|
|
||||||
|
## 嵌入和使用方式
|
||||||
|
|
||||||
|
### 工作原理
|
||||||
|
|
||||||
|
1. **编译时**:
|
||||||
|
- 使用 Go 构建标签(build tags)根据目标平台自动选择嵌入文件
|
||||||
|
- macOS 构建时:只嵌入 `resources/ffmpeg/darwin/*`
|
||||||
|
- Windows 构建时:只嵌入 `resources/ffmpeg/windows/*`
|
||||||
|
- Linux 构建时:只嵌入 `resources/ffmpeg/linux/*`
|
||||||
|
|
||||||
|
2. **运行时**:首次使用时,从嵌入的文件系统中提取二进制文件到临时目录
|
||||||
|
|
||||||
|
3. **缓存**:提取的文件会缓存在临时目录中,避免重复提取
|
||||||
|
|
||||||
|
### 构建标签说明
|
||||||
|
|
||||||
|
项目使用以下文件实现平台特定嵌入:
|
||||||
|
|
||||||
|
- `ffmpeg_darwin.go` - macOS 平台(使用 `//go:build darwin`)
|
||||||
|
- `ffmpeg_windows.go` - Windows 平台(使用 `//go:build windows`)
|
||||||
|
- `ffmpeg_linux.go` - Linux 平台(使用 `//go:build linux`)
|
||||||
|
- `ffmpeg_default.go` - 其他平台(向后兼容)
|
||||||
|
|
||||||
|
编译时,Go 会自动只编译匹配当前平台的文件,因此:
|
||||||
|
- 在 macOS 上编译 → 只嵌入 darwin 目录的文件
|
||||||
|
- 在 Windows 上编译 → 只嵌入 windows 目录的文件
|
||||||
|
- 在 Linux 上编译 → 只嵌入 linux 目录的文件
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- **文件大小**:二进制文件会被嵌入到最终可执行文件中,会增加程序大小
|
||||||
|
- **首次运行**:首次运行时需要提取二进制文件,可能会有短暂的延迟
|
||||||
|
- **临时目录**:提取的文件保存在 `{临时目录}/videoconcat-ffmpeg/{进程ID}/` 中
|
||||||
|
- **权限**:提取的文件会自动设置执行权限(Unix 系统)
|
||||||
|
- **清理**:临时文件在程序退出后通常会被系统清理,但进程异常退出时可能需要手动清理
|
||||||
|
|
||||||
|
## Git 提交注意事项
|
||||||
|
|
||||||
|
默认情况下,`.gitignore` 已配置忽略二进制文件。如果需要将二进制文件提交到 Git:
|
||||||
|
|
||||||
|
1. 移除 `.gitignore` 中的相关规则,或
|
||||||
|
2. 使用 `git add -f` 强制添加
|
||||||
|
|
||||||
|
**注意**:二进制文件通常很大(几十到几百 MB),可能不适合提交到 Git。建议使用构建脚本在编译时自动下载。
|
||||||
5
wails/resources/ffmpeg/darwin/.gitkeep
Normal file
5
wails/resources/ffmpeg/darwin/.gitkeep
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# macOS 平台的 FFmpeg 二进制文件放置目录
|
||||||
|
#
|
||||||
|
# 将 macOS 版本的 ffmpeg 和 ffprobe 放在此目录下:
|
||||||
|
# - ffmpeg
|
||||||
|
# - ffprobe
|
||||||
5
wails/resources/ffmpeg/linux/.gitkeep
Normal file
5
wails/resources/ffmpeg/linux/.gitkeep
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Linux 平台的 FFmpeg 二进制文件放置目录
|
||||||
|
#
|
||||||
|
# 将 Linux 版本的 ffmpeg 和 ffprobe 放在此目录下:
|
||||||
|
# - ffmpeg
|
||||||
|
# - ffprobe
|
||||||
5
wails/resources/ffmpeg/windows/.gitkeep
Normal file
5
wails/resources/ffmpeg/windows/.gitkeep
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Windows 平台的 FFmpeg 二进制文件放置目录
|
||||||
|
#
|
||||||
|
# 将 Windows 版本的 ffmpeg.exe 和 ffprobe.exe 放在此目录下:
|
||||||
|
# - ffmpeg.exe
|
||||||
|
# - ffprobe.exe
|
||||||
220
wails/resources/ffmpeg/使用说明.md
Normal file
220
wails/resources/ffmpeg/使用说明.md
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# FFmpeg 嵌入使用说明
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 下载 FFmpeg 二进制文件
|
||||||
|
|
||||||
|
根据你的目标平台下载对应的 FFmpeg 静态构建版本,并放置到对应的平台目录下:
|
||||||
|
|
||||||
|
**重要**:使用平台特定目录组织,编译时会自动只嵌入对应平台的版本。
|
||||||
|
|
||||||
|
#### macOS (Apple Silicon 或 Intel)
|
||||||
|
```bash
|
||||||
|
# 使用 Homebrew 安装(推荐)
|
||||||
|
brew install ffmpeg
|
||||||
|
|
||||||
|
# 复制到 macOS 专用目录
|
||||||
|
cp /opt/homebrew/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg
|
||||||
|
cp /opt/homebrew/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
|
||||||
|
# 或者如果是 Intel Mac
|
||||||
|
cp /usr/local/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg
|
||||||
|
cp /usr/local/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
chmod +x wails/resources/ffmpeg/darwin/ffmpeg
|
||||||
|
chmod +x wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
**或者下载静态构建版本:**
|
||||||
|
```bash
|
||||||
|
# Apple Silicon
|
||||||
|
wget https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip
|
||||||
|
unzip ffmpeg-6.1.zip
|
||||||
|
cp ffmpeg wails/resources/ffmpeg/darwin/ffmpeg
|
||||||
|
cp ffprobe wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
chmod +x wails/resources/ffmpeg/darwin/ffmpeg wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
```powershell
|
||||||
|
# 下载
|
||||||
|
# 访问 https://www.gyan.dev/ffmpeg/builds/
|
||||||
|
# 下载 ffmpeg-release-essentials.zip
|
||||||
|
# 解压后复制 bin 目录下的文件到 windows 目录
|
||||||
|
|
||||||
|
copy ffmpeg.exe wails\resources\ffmpeg\windows\ffmpeg.exe
|
||||||
|
copy ffprobe.exe wails\resources\ffmpeg\windows\ffprobe.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
```bash
|
||||||
|
# 下载静态构建版本
|
||||||
|
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||||
|
tar xf ffmpeg-release-amd64-static.tar.xz
|
||||||
|
cd ffmpeg-*-amd64-static
|
||||||
|
|
||||||
|
# 复制到 Linux 专用目录
|
||||||
|
cp ffmpeg ../../wails/resources/ffmpeg/linux/ffmpeg
|
||||||
|
cp ffprobe ../../wails/resources/ffmpeg/linux/ffprobe
|
||||||
|
chmod +x ../../wails/resources/ffmpeg/linux/ffmpeg
|
||||||
|
chmod +x ../../wails/resources/ffmpeg/linux/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 编译应用
|
||||||
|
|
||||||
|
二进制文件会在编译时根据目标平台自动选择并嵌入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd wails
|
||||||
|
|
||||||
|
# 在 macOS 上编译 macOS 版本(只嵌入 darwin/ 目录的文件)
|
||||||
|
go build -o VideoConcat
|
||||||
|
|
||||||
|
# 交叉编译 Windows 版本(只嵌入 windows/ 目录的文件)
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o VideoConcat.exe
|
||||||
|
|
||||||
|
# 交叉编译 Linux 版本(只嵌入 linux/ 目录的文件)
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o VideoConcat
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- ✅ macOS 版本只包含 macOS 的 FFmpeg,体积更小
|
||||||
|
- ✅ Windows 版本只包含 Windows 的 FFmpeg,体积更小
|
||||||
|
- ✅ 交叉编译时不会混入其他平台的二进制文件
|
||||||
|
|
||||||
|
### 3. 运行应用
|
||||||
|
|
||||||
|
应用启动时会自动:
|
||||||
|
1. 检查系统 PATH 中的 ffmpeg
|
||||||
|
2. 如果未找到,从嵌入的资源中提取到临时目录
|
||||||
|
3. 使用提取的二进制文件
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### 嵌入过程
|
||||||
|
|
||||||
|
```go
|
||||||
|
// main.go
|
||||||
|
//go:embed resources/ffmpeg
|
||||||
|
var embeddedFFmpeg embed.FS
|
||||||
|
```
|
||||||
|
|
||||||
|
这行代码会在编译时将所有 `resources/ffmpeg/` 目录下的文件嵌入到程序中。
|
||||||
|
|
||||||
|
### 运行时提取
|
||||||
|
|
||||||
|
应用首次启动时:
|
||||||
|
1. 检查系统是否有 ffmpeg
|
||||||
|
2. 如果没有,从嵌入的文件系统中查找匹配的二进制文件
|
||||||
|
3. 提取到临时目录:`{临时目录}/videoconcat-ffmpeg/{进程ID}/`
|
||||||
|
4. 设置执行权限
|
||||||
|
5. 使用提取的文件
|
||||||
|
|
||||||
|
### 文件查找顺序
|
||||||
|
|
||||||
|
1. `resources/ffmpeg/ffmpeg` 或 `resources/ffmpeg/ffmpeg.exe`
|
||||||
|
2. `resources/ffmpeg/{GOOS}/ffmpeg` 或 `resources/ffmpeg/{GOOS}/ffmpeg.exe`
|
||||||
|
3. `resources/ffmpeg/{GOOS}_{GOARCH}/ffmpeg` 等
|
||||||
|
|
||||||
|
## 目录结构示例
|
||||||
|
|
||||||
|
### ⭐ 推荐:按平台组织(自动平台选择)
|
||||||
|
|
||||||
|
```
|
||||||
|
wails/resources/ffmpeg/
|
||||||
|
├── darwin/ # macOS 版本
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ └── ffprobe
|
||||||
|
├── windows/ # Windows 版本
|
||||||
|
│ ├── ffmpeg.exe
|
||||||
|
│ └── ffprobe.exe
|
||||||
|
├── linux/ # Linux 版本
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ └── ffprobe
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译行为**:
|
||||||
|
- 在 macOS 上编译 → 只嵌入 `darwin/` 目录
|
||||||
|
- 在 Windows 上编译 → 只嵌入 `windows/` 目录
|
||||||
|
- 在 Linux 上编译 → 只嵌入 `linux/` 目录
|
||||||
|
|
||||||
|
### 向后兼容:单目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
wails/resources/ffmpeg/
|
||||||
|
├── ffmpeg # 当前平台的 ffmpeg(不推荐)
|
||||||
|
├── ffprobe # 当前平台的 ffprobe
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:这种方式会嵌入所有文件,不推荐使用。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
### 文件大小
|
||||||
|
|
||||||
|
- FFmpeg 静态构建版本通常 50-200 MB
|
||||||
|
- 嵌入后会增加最终可执行文件的大小
|
||||||
|
- 考虑使用 LZMA 压缩或分平台构建来减小体积
|
||||||
|
|
||||||
|
### Git 提交
|
||||||
|
|
||||||
|
默认情况下,二进制文件被 `.gitignore` 忽略。如果需要提交:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方法1:强制添加
|
||||||
|
git add -f wails/resources/ffmpeg/ffmpeg
|
||||||
|
|
||||||
|
# 方法2:修改 .gitignore(不推荐,文件太大)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 权限问题
|
||||||
|
|
||||||
|
在 macOS/Linux 上,确保文件有执行权限:
|
||||||
|
```bash
|
||||||
|
chmod +x wails/resources/ffmpeg/ffmpeg
|
||||||
|
chmod +x wails/resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 临时文件清理
|
||||||
|
|
||||||
|
提取的文件保存在临时目录中。如果进程异常退出,可能需要手动清理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
rm -rf /tmp/videoconcat-ffmpeg
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
rd /s /q %TEMP%\videoconcat-ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题:找不到 ffmpeg
|
||||||
|
|
||||||
|
**检查清单:**
|
||||||
|
1. 确认二进制文件已放置在 `wails/resources/ffmpeg/` 目录
|
||||||
|
2. 检查文件名是否正确(Windows 需要 `.exe` 后缀)
|
||||||
|
3. 检查文件权限(macOS/Linux)
|
||||||
|
4. 查看应用日志,查找详细的错误信息
|
||||||
|
|
||||||
|
### 问题:权限被拒绝
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```bash
|
||||||
|
chmod +x wails/resources/ffmpeg/ffmpeg
|
||||||
|
chmod +x wails/resources/ffmpeg/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:提取失败
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
- 临时目录不可写
|
||||||
|
- 磁盘空间不足
|
||||||
|
- 文件损坏
|
||||||
|
|
||||||
|
**检查:**
|
||||||
|
- 查看应用日志
|
||||||
|
- 检查临时目录权限和空间
|
||||||
149
wails/resources/ffmpeg/平台特定嵌入说明.md
Normal file
149
wails/resources/ffmpeg/平台特定嵌入说明.md
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
# 平台特定 FFmpeg 嵌入说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
项目使用 **Go 构建标签(Build Tags)** 实现平台特定的 FFmpeg 嵌入,确保每个平台的构建只包含对应平台的二进制文件。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
### 构建标签文件
|
||||||
|
|
||||||
|
项目包含以下平台特定文件:
|
||||||
|
|
||||||
|
- `ffmpeg_darwin.go` - macOS 平台(`//go:build darwin`)
|
||||||
|
- `ffmpeg_windows.go` - Windows 平台(`//go:build windows`)
|
||||||
|
- `ffmpeg_linux.go` - Linux 平台(`//go:build linux`)
|
||||||
|
- `ffmpeg_default.go` - 其他平台(向后兼容)
|
||||||
|
|
||||||
|
### 编译时的行为
|
||||||
|
|
||||||
|
Go 编译器会根据当前编译目标平台(`GOOS`)自动选择匹配的文件:
|
||||||
|
|
||||||
|
| 编译平台 | 使用文件 | 嵌入目录 | 结果 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| `GOOS=darwin` | `ffmpeg_darwin.go` | `resources/ffmpeg/darwin/*` | 只包含 macOS 版本 |
|
||||||
|
| `GOOS=windows` | `ffmpeg_windows.go` | `resources/ffmpeg/windows/*` | 只包含 Windows 版本 |
|
||||||
|
| `GOOS=linux` | `ffmpeg_linux.go` | `resources/ffmpeg/linux/*` | 只包含 Linux 版本 |
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
wails/
|
||||||
|
├── main.go # 主文件(声明 getEmbeddedFFmpeg 函数)
|
||||||
|
├── ffmpeg_darwin.go # macOS 实现
|
||||||
|
├── ffmpeg_windows.go # Windows 实现
|
||||||
|
├── ffmpeg_linux.go # Linux 实现
|
||||||
|
├── ffmpeg_default.go # 默认实现(向后兼容)
|
||||||
|
└── resources/
|
||||||
|
└── ffmpeg/
|
||||||
|
├── darwin/ # macOS 二进制文件
|
||||||
|
│ ├── ffmpeg
|
||||||
|
│ └── ffprobe
|
||||||
|
├── windows/ # Windows 二进制文件
|
||||||
|
│ ├── ffmpeg.exe
|
||||||
|
│ └── ffprobe.exe
|
||||||
|
└── linux/ # Linux 二进制文件
|
||||||
|
├── ffmpeg
|
||||||
|
└── ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 准备二进制文件
|
||||||
|
|
||||||
|
将各平台的 FFmpeg 二进制文件放置到对应目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
cp /opt/homebrew/bin/ffmpeg wails/resources/ffmpeg/darwin/ffmpeg
|
||||||
|
cp /opt/homebrew/bin/ffprobe wails/resources/ffmpeg/darwin/ffprobe
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
copy ffmpeg.exe wails\resources\ffmpeg\windows\ffmpeg.exe
|
||||||
|
copy ffprobe.exe wails\resources\ffmpeg\windows\ffprobe.exe
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
cp ffmpeg wails/resources/ffmpeg/linux/ffmpeg
|
||||||
|
cp ffprobe wails/resources/ffmpeg/linux/ffprobe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 编译不同平台
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS 版本(只嵌入 darwin/ 目录)
|
||||||
|
GOOS=darwin go build -o VideoConcat-mac
|
||||||
|
|
||||||
|
# Windows 版本(只嵌入 windows/ 目录)
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o VideoConcat.exe
|
||||||
|
|
||||||
|
# Linux 版本(只嵌入 linux/ 目录)
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o VideoConcat-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证嵌入内容
|
||||||
|
|
||||||
|
编译后,可以使用以下方法验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
strings VideoConcat | grep -i ffmpeg | head -5
|
||||||
|
|
||||||
|
# Windows (使用 PowerShell)
|
||||||
|
Select-String -Path VideoConcat.exe -Pattern "ffmpeg" | Select-Object -First 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
### 1. 减小文件体积
|
||||||
|
|
||||||
|
- 只嵌入当前平台的二进制文件
|
||||||
|
- 减少不必要的文件大小增加
|
||||||
|
- 示例:如果每个平台 FFmpeg 是 50MB,使用平台特定嵌入后:
|
||||||
|
- 之前:所有平台 = 150MB(三个平台都包含)
|
||||||
|
- 现在:每个平台 = 50MB(只包含对应平台)
|
||||||
|
|
||||||
|
### 2. 避免交叉编译问题
|
||||||
|
|
||||||
|
- 编译 Windows 版本时不会误嵌入 macOS 版本
|
||||||
|
- 编译 Linux 版本时不会误嵌入 Windows 版本
|
||||||
|
- 避免运行时找不到正确文件的问题
|
||||||
|
|
||||||
|
### 3. 清晰的目录组织
|
||||||
|
|
||||||
|
- 各平台文件分开管理
|
||||||
|
- 易于维护和更新
|
||||||
|
- 符合 Go 的最佳实践
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题:编译时找不到文件
|
||||||
|
|
||||||
|
**原因**:对应平台的目录中没有二进制文件
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
# 检查文件是否存在
|
||||||
|
ls -la wails/resources/ffmpeg/darwin/
|
||||||
|
ls -la wails/resources/ffmpeg/windows/
|
||||||
|
ls -la wails/resources/ffmpeg/linux/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:交叉编译时仍嵌入所有文件
|
||||||
|
|
||||||
|
**原因**:使用了旧的目录结构(根目录下的文件)
|
||||||
|
|
||||||
|
**解决**:确保使用平台特定目录(`darwin/`, `windows/`, `linux/`)
|
||||||
|
|
||||||
|
### 问题:运行时找不到 FFmpeg
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 查看应用日志,确认 FFmpeg 查找路径
|
||||||
|
2. 检查临时目录中是否成功提取
|
||||||
|
3. 验证二进制文件是否有执行权限
|
||||||
|
|
||||||
|
## 向后兼容
|
||||||
|
|
||||||
|
如果不想使用平台特定目录,仍然可以使用根目录放置文件(`resources/ffmpeg/ffmpeg`),但这样会:
|
||||||
|
- 嵌入所有平台的二进制文件
|
||||||
|
- 增加不必要的文件大小
|
||||||
|
- 不推荐使用
|
||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -54,7 +53,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
|||||||
os.MkdirAll(tempDir, 0755)
|
os.MkdirAll(tempDir, 0755)
|
||||||
|
|
||||||
// 获取视频信息
|
// 获取视频信息
|
||||||
cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration:stream=codec_name,r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", inputPath)
|
helper := GetFFmpegHelper()
|
||||||
|
if !helper.IsProbeAvailable() {
|
||||||
|
return fmt.Errorf("ffprobe 不可用,请确保已安装 ffmpeg")
|
||||||
|
}
|
||||||
|
cmd := helper.ProbeCommand("-v", "error", "-show_entries", "format=duration:stream=codec_name,r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", inputPath)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("获取视频信息失败: %v", err)
|
return fmt.Errorf("获取视频信息失败: %v", err)
|
||||||
@ -94,8 +97,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
|||||||
|
|
||||||
// 如果是 HEVC,先转换为 H.264
|
// 如果是 HEVC,先转换为 H.264
|
||||||
if codecName == "hevc" {
|
if codecName == "hevc" {
|
||||||
|
if !helper.IsAvailable() {
|
||||||
|
return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||||||
|
}
|
||||||
videoConvert := filepath.Join(tempDir, "convert.mp4")
|
videoConvert := filepath.Join(tempDir, "convert.mp4")
|
||||||
cmd := exec.Command("ffmpeg", "-i", inputPath, "-c:v", "libx264", "-y", videoConvert)
|
cmd := helper.Command("-i", inputPath, "-c:v", "libx264", "-y", videoConvert)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("转换HEVC失败: %v", err)
|
return fmt.Errorf("转换HEVC失败: %v", err)
|
||||||
}
|
}
|
||||||
@ -116,13 +122,13 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
|||||||
videoPart2 := filepath.Join(tempDir, "part2.mp4")
|
videoPart2 := filepath.Join(tempDir, "part2.mp4")
|
||||||
|
|
||||||
// 第一部分:0 到 randomFrame - 0.016
|
// 第一部分:0 到 randomFrame - 0.016
|
||||||
cmd = exec.Command("ffmpeg", "-i", inputPath, "-ss", "0", "-t", fmt.Sprintf("%.6f", randomFrame-0.016), "-c", "copy", "-y", videoPart1)
|
cmd = helper.Command("-i", inputPath, "-ss", "0", "-t", fmt.Sprintf("%.6f", randomFrame-0.016), "-c", "copy", "-y", videoPart1)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("裁剪第一部分失败: %v", err)
|
return fmt.Errorf("裁剪第一部分失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第二部分:randomFrame 到结束
|
// 第二部分:randomFrame 到结束
|
||||||
cmd = exec.Command("ffmpeg", "-i", inputPath, "-ss", fmt.Sprintf("%.6f", randomFrame), "-c", "copy", "-y", videoPart2)
|
cmd = helper.Command("-i", inputPath, "-ss", fmt.Sprintf("%.6f", randomFrame), "-c", "copy", "-y", videoPart2)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("裁剪第二部分失败: %v", err)
|
return fmt.Errorf("裁剪第二部分失败: %v", err)
|
||||||
}
|
}
|
||||||
@ -137,7 +143,7 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
|||||||
file.WriteString(fmt.Sprintf("file '%s'\n", strings.ReplaceAll(videoPart2, "\\", "/")))
|
file.WriteString(fmt.Sprintf("file '%s'\n", strings.ReplaceAll(videoPart2, "\\", "/")))
|
||||||
file.Close()
|
file.Close()
|
||||||
|
|
||||||
cmd = exec.Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", concatFile, "-c", "copy", "-y", outputPath)
|
cmd = helper.Command("-f", "concat", "-safe", "0", "-i", concatFile, "-c", "copy", "-y", outputPath)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("合并视频失败: %v", err)
|
return fmt.Errorf("合并视频失败: %v", err)
|
||||||
}
|
}
|
||||||
@ -247,8 +253,12 @@ func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequ
|
|||||||
|
|
||||||
// ModifyByMetadata 通过修改元数据改变文件 MD5
|
// ModifyByMetadata 通过修改元数据改变文件 MD5
|
||||||
func (s *ExtractService) ModifyByMetadata(ctx context.Context, inputPath string, outputPath string) error {
|
func (s *ExtractService) ModifyByMetadata(ctx context.Context, inputPath string, outputPath string) error {
|
||||||
|
helper := GetFFmpegHelper()
|
||||||
|
if !helper.IsAvailable() {
|
||||||
|
return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||||||
|
}
|
||||||
comment := fmt.Sprintf("JSY_%s", time.Now().Format("20060102150405"))
|
comment := fmt.Sprintf("JSY_%s", time.Now().Format("20060102150405"))
|
||||||
cmd := exec.Command("ffmpeg", "-i", inputPath,
|
cmd := helper.Command("-i", inputPath,
|
||||||
"-c", "copy",
|
"-c", "copy",
|
||||||
"-metadata", fmt.Sprintf("comment=%s", comment),
|
"-metadata", fmt.Sprintf("comment=%s", comment),
|
||||||
"-y", outputPath)
|
"-y", outputPath)
|
||||||
|
|||||||
383
wails/services/ffmpeg_helper.go
Normal file
383
wails/services/ffmpeg_helper.go
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FFmpegHelper FFmpeg 工具助手
|
||||||
|
type FFmpegHelper struct {
|
||||||
|
ffmpegPath string
|
||||||
|
ffprobePath string
|
||||||
|
embeddedFS *embed.FS // 使用指针以便检查 nil
|
||||||
|
extractedPath string // 已提取的临时目录路径
|
||||||
|
}
|
||||||
|
|
||||||
|
var ffmpegHelper *FFmpegHelper
|
||||||
|
|
||||||
|
// InitFFmpegHelper 初始化 FFmpeg 助手(传入嵌入的文件系统)
|
||||||
|
func InitFFmpegHelper(embeddedFS embed.FS) {
|
||||||
|
if ffmpegHelper == nil {
|
||||||
|
ffmpegHelper = &FFmpegHelper{
|
||||||
|
embeddedFS: &embeddedFS,
|
||||||
|
}
|
||||||
|
ffmpegHelper.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFFmpegHelper 获取 FFmpeg 助手实例(单例模式)
|
||||||
|
func GetFFmpegHelper() *FFmpegHelper {
|
||||||
|
if ffmpegHelper == nil {
|
||||||
|
// 如果没有初始化,创建一个不包含嵌入文件系统的实例
|
||||||
|
ffmpegHelper = &FFmpegHelper{}
|
||||||
|
ffmpegHelper.init()
|
||||||
|
}
|
||||||
|
return ffmpegHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 初始化 FFmpeg 路径
|
||||||
|
func (h *FFmpegHelper) init() {
|
||||||
|
// 先尝试从系统 PATH 查找
|
||||||
|
h.ffmpegPath = h.findExecutable("ffmpeg")
|
||||||
|
h.ffprobePath = h.findExecutable("ffprobe")
|
||||||
|
|
||||||
|
// 如果系统 PATH 中没有,尝试从嵌入的资源中提取
|
||||||
|
if h.ffmpegPath == "" && h.hasEmbeddedFS() {
|
||||||
|
h.ffmpegPath = h.extractEmbeddedBinary("ffmpeg")
|
||||||
|
}
|
||||||
|
if h.ffprobePath == "" && h.hasEmbeddedFS() {
|
||||||
|
h.ffprobePath = h.extractEmbeddedBinary("ffprobe")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果嵌入的资源也没有,尝试从打包的资源目录中查找
|
||||||
|
if h.ffmpegPath == "" {
|
||||||
|
h.ffmpegPath = h.findBundledBinary("ffmpeg")
|
||||||
|
}
|
||||||
|
if h.ffprobePath == "" {
|
||||||
|
h.ffprobePath = h.findBundledBinary("ffprobe")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
if h.ffmpegPath != "" {
|
||||||
|
LogInfof("找到 ffmpeg: %s", h.ffmpegPath)
|
||||||
|
} else {
|
||||||
|
LogWarn("未找到 ffmpeg,视频处理功能可能无法使用")
|
||||||
|
LogWarn("请确保 ffmpeg 已安装并在系统 PATH 中,或将其嵌入到 resources/ffmpeg/ 目录下")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.ffprobePath != "" {
|
||||||
|
LogInfof("找到 ffprobe: %s", h.ffprobePath)
|
||||||
|
} else {
|
||||||
|
LogWarn("未找到 ffprobe,视频信息获取功能可能无法使用")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findExecutable 从系统 PATH 中查找可执行文件
|
||||||
|
func (h *FFmpegHelper) findExecutable(name string) string {
|
||||||
|
path, err := exec.LookPath(name)
|
||||||
|
if err == nil {
|
||||||
|
// 验证文件是否可执行
|
||||||
|
if info, err := os.Stat(path); err == nil {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
// Unix 系统检查执行权限
|
||||||
|
if info.Mode().Perm()&0111 != 0 {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Windows 系统直接返回
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBundledBinary 从打包的资源中查找二进制文件
|
||||||
|
func (h *FFmpegHelper) findBundledBinary(name string) string {
|
||||||
|
// 获取可执行文件所在目录
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
exeDir := filepath.Dir(exePath)
|
||||||
|
|
||||||
|
// 根据操作系统确定二进制文件名
|
||||||
|
binaryName := name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binaryName = name + ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可能的资源目录路径(按优先级排序)
|
||||||
|
possiblePaths := []string{
|
||||||
|
// macOS app bundle 中的路径
|
||||||
|
filepath.Join(exeDir, "..", "Resources", "ffmpeg", binaryName),
|
||||||
|
// Windows/Linux 相对路径
|
||||||
|
filepath.Join(exeDir, "resources", "ffmpeg", binaryName),
|
||||||
|
// 开发环境的相对路径
|
||||||
|
filepath.Join(exeDir, "..", "resources", "ffmpeg", binaryName),
|
||||||
|
// 当前目录
|
||||||
|
filepath.Join(".", "resources", "ffmpeg", binaryName),
|
||||||
|
// 直接在可执行文件目录
|
||||||
|
filepath.Join(exeDir, binaryName),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
// 转换为绝对路径
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
|
||||||
|
// 在首次使用时,可能需要提取到临时目录并设置执行权限
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
// 确保文件有执行权限
|
||||||
|
os.Chmod(absPath, 0755)
|
||||||
|
}
|
||||||
|
return absPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFFmpegPath 获取 ffmpeg 路径
|
||||||
|
func (h *FFmpegHelper) GetFFmpegPath() string {
|
||||||
|
return h.ffmpegPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFFprobePath 获取 ffprobe 路径
|
||||||
|
func (h *FFmpegHelper) GetFFprobePath() string {
|
||||||
|
return h.ffprobePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command 创建 FFmpeg 命令
|
||||||
|
func (h *FFmpegHelper) Command(args ...string) *exec.Cmd {
|
||||||
|
if h.ffmpegPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return exec.Command(h.ffmpegPath, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbeCommand 创建 FFprobe 命令
|
||||||
|
func (h *FFmpegHelper) ProbeCommand(args ...string) *exec.Cmd {
|
||||||
|
if h.ffprobePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return exec.Command(h.ffprobePath, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAvailable 检查 FFmpeg 是否可用
|
||||||
|
func (h *FFmpegHelper) IsAvailable() bool {
|
||||||
|
return h.ffmpegPath != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsProbeAvailable 检查 FFprobe 是否可用
|
||||||
|
func (h *FFmpegHelper) IsProbeAvailable() bool {
|
||||||
|
return h.ffprobePath != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasEmbeddedFS 检查是否有嵌入的文件系统
|
||||||
|
func (h *FFmpegHelper) hasEmbeddedFS() bool {
|
||||||
|
return h.embeddedFS != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEmbeddedBinary 从嵌入的文件系统中提取二进制文件
|
||||||
|
func (h *FFmpegHelper) extractEmbeddedBinary(name string) string {
|
||||||
|
if !h.hasEmbeddedFS() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定二进制文件名(根据当前平台)
|
||||||
|
binaryName := name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binaryName = name + ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在嵌入的文件系统中查找文件
|
||||||
|
// 根据不同的嵌入方式,可能的路径有所不同:
|
||||||
|
// - 如果使用平台特定目录:resources/ffmpeg/darwin/ffmpeg
|
||||||
|
// - 如果使用通用目录:resources/ffmpeg/ffmpeg
|
||||||
|
possiblePaths := []string{
|
||||||
|
// 平台特定目录(推荐,用于分离打包)
|
||||||
|
fmt.Sprintf("resources/ffmpeg/%s/%s", runtime.GOOS, binaryName),
|
||||||
|
fmt.Sprintf("resources/ffmpeg/%s/%s", runtime.GOOS, name),
|
||||||
|
fmt.Sprintf("resources/ffmpeg/%s_%s/%s", runtime.GOOS, runtime.GOARCH, binaryName),
|
||||||
|
// 通用路径(向后兼容)
|
||||||
|
fmt.Sprintf("resources/ffmpeg/%s", binaryName),
|
||||||
|
fmt.Sprintf("resources/ffmpeg/%s", name),
|
||||||
|
}
|
||||||
|
|
||||||
|
var binaryData []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
binaryData, err = h.embeddedFS.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
LogDebugf("从嵌入资源中找到二进制文件: %s", path)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
LogDebugf("未在嵌入资源中找到 %s,尝试遍历目录", name)
|
||||||
|
// 如果直接路径找不到,尝试遍历目录查找
|
||||||
|
binaryData, _ = h.findBinaryInEmbeddedFS(name)
|
||||||
|
if binaryData == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取到临时目录
|
||||||
|
path, err := h.extractToTemp(name, binaryData)
|
||||||
|
if err != nil {
|
||||||
|
LogErrorf("提取嵌入的二进制文件失败: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// findBinaryInEmbeddedFS 在嵌入的文件系统中查找二进制文件
|
||||||
|
func (h *FFmpegHelper) findBinaryInEmbeddedFS(name string) ([]byte, string) {
|
||||||
|
if !h.hasEmbeddedFS() {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryName := name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binaryName = name + ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用结构体存储结果
|
||||||
|
type result struct {
|
||||||
|
data []byte
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
var found *result
|
||||||
|
|
||||||
|
// 遍历 resources/ffmpeg 目录
|
||||||
|
err := fs.WalkDir(h.embeddedFS, "resources/ffmpeg", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil // 忽略错误,继续遍历
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件名是否匹配
|
||||||
|
if !d.IsDir() && (d.Name() == name || d.Name() == binaryName) {
|
||||||
|
data, readErr := h.embeddedFS.ReadFile(path)
|
||||||
|
if readErr == nil {
|
||||||
|
// 找到匹配的文件
|
||||||
|
found = &result{
|
||||||
|
data: data,
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
return fs.SkipAll // 找到后停止遍历
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil && found != nil {
|
||||||
|
return found.data, found.path
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractToTemp 提取二进制文件到临时目录
|
||||||
|
func (h *FFmpegHelper) extractToTemp(name string, data []byte) (string, error) {
|
||||||
|
// 创建临时目录(只创建一次)
|
||||||
|
if h.extractedPath == "" {
|
||||||
|
tempDir := filepath.Join(os.TempDir(), "videoconcat-ffmpeg", fmt.Sprintf("%d", os.Getpid()))
|
||||||
|
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("创建临时目录失败: %v", err)
|
||||||
|
}
|
||||||
|
h.extractedPath = tempDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定二进制文件名
|
||||||
|
binaryName := name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binaryName = name + ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标路径
|
||||||
|
targetPath := filepath.Join(h.extractedPath, binaryName)
|
||||||
|
|
||||||
|
// 如果文件已存在且大小一致,直接返回
|
||||||
|
if info, err := os.Stat(targetPath); err == nil {
|
||||||
|
if info.Size() == int64(len(data)) {
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
// 文件大小不一致,删除重新写入
|
||||||
|
os.Remove(targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
file, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err := file.Write(data); err != nil {
|
||||||
|
return "", fmt.Errorf("写入文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置执行权限(Unix 系统)
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := os.Chmod(targetPath, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("设置执行权限失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件
|
||||||
|
if _, err := os.Stat(targetPath); err != nil {
|
||||||
|
return "", fmt.Errorf("验证文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfof("已提取嵌入的二进制文件到: %s", targetPath)
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractBundledBinary 从嵌入的资源中提取二进制文件到临时目录(兼容旧接口)
|
||||||
|
func (h *FFmpegHelper) ExtractBundledBinary(name string, data []byte) (string, error) {
|
||||||
|
return h.extractToTemp(name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyFile 复制文件(用于从嵌入资源复制到目标位置)
|
||||||
|
func (h *FFmpegHelper) CopyFile(src, dst string) error {
|
||||||
|
sourceFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开源文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建目标文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("复制文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置执行权限(Unix 系统)
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := os.Chmod(dst, 0755); err != nil {
|
||||||
|
return fmt.Errorf("设置执行权限失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -129,7 +128,11 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 FFmpeg 转换
|
// 使用 FFmpeg 转换
|
||||||
cmd := exec.Command("ffmpeg", "-i", videoPath,
|
helper := GetFFmpegHelper()
|
||||||
|
if !helper.IsAvailable() {
|
||||||
|
return "", fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||||||
|
}
|
||||||
|
cmd := helper.Command("-i", videoPath,
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
"-c:a", "aac",
|
"-c:a", "aac",
|
||||||
"-ar", "44100",
|
"-ar", "44100",
|
||||||
@ -143,7 +146,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
|||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 尝试备用方案
|
// 尝试备用方案
|
||||||
cmd2 := exec.Command("ffmpeg", "-i", videoPath,
|
cmd2 := helper.Command("-i", videoPath,
|
||||||
"-c", "copy",
|
"-c", "copy",
|
||||||
"-f", "mpegts",
|
"-f", "mpegts",
|
||||||
"-y", tsPath)
|
"-y", tsPath)
|
||||||
@ -151,6 +154,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
|||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
return "", fmt.Errorf("视频转换失败: %v, 输出: %s, 备用方案失败: %v, 输出: %s", err, string(output), err2, string(output2))
|
return "", fmt.Errorf("视频转换失败: %v, 输出: %s, 备用方案失败: %v, 输出: %s", err, string(output), err2, string(output2))
|
||||||
}
|
}
|
||||||
|
// 备用方案成功,继续执行
|
||||||
}
|
}
|
||||||
|
|
||||||
return tsPath, nil
|
return tsPath, nil
|
||||||
@ -343,7 +347,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
|||||||
"-y", outputPath,
|
"-y", outputPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg", args...)
|
helper := GetFFmpegHelper()
|
||||||
|
if !helper.IsAvailable() {
|
||||||
|
LogError("ffmpeg 不可用")
|
||||||
|
result.Status = "拼接失败"
|
||||||
|
result.Progress = "失败"
|
||||||
|
mu.Lock()
|
||||||
|
results = append(results, result)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := helper.Command(args...)
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError(fmt.Sprintf("拼接视频失败: %v", err))
|
LogError(fmt.Sprintf("拼接视频失败: %v", err))
|
||||||
@ -366,7 +380,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
|||||||
"-y", outputImgPath,
|
"-y", outputImgPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg", args...)
|
helper := GetFFmpegHelper()
|
||||||
|
if !helper.IsAvailable() {
|
||||||
|
LogError("ffmpeg 不可用")
|
||||||
|
result.Status = "拼接失败"
|
||||||
|
result.Progress = "失败"
|
||||||
|
mu.Lock()
|
||||||
|
results = append(results, result)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := helper.Command(args...)
|
||||||
cmd.Run() // 忽略错误,水印是可选的
|
cmd.Run() // 忽略错误,水印是可选的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,13 +407,19 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
|||||||
|
|
||||||
// 获取视频时长(使用 ffprobe)
|
// 获取视频时长(使用 ffprobe)
|
||||||
seconds := 0
|
seconds := 0
|
||||||
cmd = exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", outputPath)
|
if helper.IsProbeAvailable() {
|
||||||
|
cmd = helper.ProbeCommand("-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", outputPath)
|
||||||
|
} else {
|
||||||
|
cmd = nil
|
||||||
|
}
|
||||||
|
if cmd != nil {
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var duration float64
|
var duration float64
|
||||||
fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration)
|
fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration)
|
||||||
seconds = int(duration)
|
seconds = int(duration)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sizeMB := fileInfo.Size() / 1024 / 1024
|
sizeMB := fileInfo.Size() / 1024 / 1024
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user