Merge branch 'main' of https://gitea.xiangbing.vip/xiangbing/VideoConcat
This commit is contained in:
commit
7717bf13a1
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
17
wails/assets/assets/index-IwiMqFON.js
Normal file
17
wails/assets/assets/index-IwiMqFON.js
Normal file
File diff suppressed because one or more lines are too long
@ -20,8 +20,8 @@
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="./assets/index-DSuQhuGl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BDrFF8AO.css">
|
||||
<script type="module" crossorigin src="./assets/index-IwiMqFON.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BaD48VVT.css">
|
||||
</head>
|
||||
<body>
|
||||
<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
|
||||
}
|
||||
@ -4,13 +4,18 @@ import (
|
||||
"embed"
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"videoconcat/services"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
|
||||
// getEmbeddedFFmpeg 在平台特定文件中实现(ffmpeg_darwin.go, ffmpeg_windows.go, ffmpeg_linux.go, ffmpeg_default.go)
|
||||
// 根据编译时的 GOOS 自动选择对应的实现
|
||||
// 注意:此函数没有在此文件中声明,而是在平台特定文件中声明和实现
|
||||
|
||||
func main() {
|
||||
// 检测开发模式并设置日志级别
|
||||
if os.Getenv("DEV") == "true" {
|
||||
@ -19,6 +24,9 @@ func main() {
|
||||
services.LogDebug("详细日志已启用")
|
||||
}
|
||||
|
||||
// 初始化 FFmpeg 助手(传递嵌入的文件系统,根据编译平台自动选择)
|
||||
services.InitFFmpegHelper(getEmbeddedFFmpeg())
|
||||
|
||||
// 创建服务
|
||||
services.LogDebug("开始创建服务...")
|
||||
videoService := services.NewVideoService()
|
||||
@ -47,9 +55,9 @@ func main() {
|
||||
// 创建窗口
|
||||
services.LogDebug("创建应用窗口...")
|
||||
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "视频拼接工具",
|
||||
Width: 1100,
|
||||
Height: 800,
|
||||
Title: "视频拼接工具",
|
||||
Width: 1100,
|
||||
Height: 800,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
})
|
||||
|
||||
31
wails/resources/ffmpeg/.gitignore
vendored
Normal file
31
wails/resources/ffmpeg/.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# 忽略 FFmpeg 二进制文件(文件太大,通常不应提交到 Git)
|
||||
# 用户需要自行下载并放置二进制文件到编译时嵌入
|
||||
|
||||
# 根目录的二进制文件(向后兼容)
|
||||
ffmpeg
|
||||
ffprobe
|
||||
ffplay
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
ffplay.exe
|
||||
|
||||
# 按平台组织的目录中的二进制文件
|
||||
darwin/ffmpeg
|
||||
darwin/ffprobe
|
||||
darwin/ffplay
|
||||
windows/ffmpeg.exe
|
||||
windows/ffprobe.exe
|
||||
windows/ffplay.exe
|
||||
linux/ffmpeg
|
||||
linux/ffprobe
|
||||
linux/ffplay
|
||||
|
||||
# 但保留 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"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -24,16 +23,16 @@ func NewExtractService() *ExtractService {
|
||||
|
||||
// ExtractFrameRequest 抽帧请求
|
||||
type ExtractFrameRequest struct {
|
||||
FolderPath string `json:"folderPath"`
|
||||
ExtractCount int `json:"extractCount"` // 每个视频生成的数量
|
||||
FolderPath string `json:"folderPath"`
|
||||
ExtractCount int `json:"extractCount"` // 每个视频生成的数量
|
||||
}
|
||||
|
||||
// ExtractFrameResult 抽帧结果
|
||||
type ExtractFrameResult struct {
|
||||
VideoPath string `json:"videoPath"`
|
||||
VideoPath string `json:"videoPath"`
|
||||
OutputPath string `json:"outputPath"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ListVideos 列出文件夹中的视频文件
|
||||
@ -54,7 +53,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
||||
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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取视频信息失败: %v", err)
|
||||
@ -94,8 +97,11 @@ func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string
|
||||
|
||||
// 如果是 HEVC,先转换为 H.264
|
||||
if codecName == "hevc" {
|
||||
if !helper.IsAvailable() {
|
||||
return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||||
}
|
||||
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 {
|
||||
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")
|
||||
|
||||
// 第一部分: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 {
|
||||
return fmt.Errorf("裁剪第一部分失败: %v", err)
|
||||
}
|
||||
|
||||
// 第二部分: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 {
|
||||
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.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 {
|
||||
return fmt.Errorf("合并视频失败: %v", err)
|
||||
}
|
||||
@ -207,37 +213,37 @@ func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequ
|
||||
|
||||
// 检查文件是否已存在
|
||||
if _, err := os.Stat(t.OutputPath); err == nil {
|
||||
mu.Lock()
|
||||
current++
|
||||
result := ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
OutputPath: t.OutputPath,
|
||||
Success: true,
|
||||
}
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
err := s.RemoveFrameRandom(ctx, t.VideoPath, t.OutputPath)
|
||||
mu.Lock()
|
||||
current++
|
||||
result := ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
OutputPath: t.OutputPath,
|
||||
Success: true,
|
||||
var result ExtractFrameResult
|
||||
if err != nil {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
} else {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
OutputPath: t.OutputPath,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
err := s.RemoveFrameRandom(ctx, t.VideoPath, t.OutputPath)
|
||||
mu.Lock()
|
||||
current++
|
||||
var result ExtractFrameResult
|
||||
if err != nil {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
} else {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: t.VideoPath,
|
||||
OutputPath: t.OutputPath,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
}(task)
|
||||
}
|
||||
|
||||
@ -247,8 +253,12 @@ func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequ
|
||||
|
||||
// ModifyByMetadata 通过修改元数据改变文件 MD5
|
||||
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"))
|
||||
cmd := exec.Command("ffmpeg", "-i", inputPath,
|
||||
cmd := helper.Command("-i", inputPath,
|
||||
"-c", "copy",
|
||||
"-metadata", fmt.Sprintf("comment=%s", comment),
|
||||
"-y", outputPath)
|
||||
@ -291,29 +301,28 @@ func (s *ExtractService) ModifyVideosMetadata(ctx context.Context, folderPath st
|
||||
outputFileName := fmt.Sprintf("modify%d%s", randomNum, filepath.Base(videoPath))
|
||||
outputPath := filepath.Join(outputDir, outputFileName)
|
||||
|
||||
err := s.ModifyByMetadata(ctx, videoPath, outputPath)
|
||||
mu.Lock()
|
||||
current++
|
||||
var result ExtractFrameResult
|
||||
if err != nil {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: videoPath,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
err := s.ModifyByMetadata(ctx, videoPath, outputPath)
|
||||
mu.Lock()
|
||||
current++
|
||||
var result ExtractFrameResult
|
||||
if err != nil {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: videoPath,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
} else {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: videoPath,
|
||||
OutputPath: outputPath,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = ExtractFrameResult{
|
||||
VideoPath: videoPath,
|
||||
OutputPath: outputPath,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
}(video)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -129,7 +128,11 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
||||
}
|
||||
|
||||
// 使用 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:a", "aac",
|
||||
"-ar", "44100",
|
||||
@ -143,7 +146,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// 尝试备用方案
|
||||
cmd2 := exec.Command("ffmpeg", "-i", videoPath,
|
||||
cmd2 := helper.Command("-i", videoPath,
|
||||
"-c", "copy",
|
||||
"-f", "mpegts",
|
||||
"-y", tsPath)
|
||||
@ -151,6 +154,7 @@ func (s *VideoService) ConvertVideoToTS(videoPath string) (string, error) {
|
||||
if err2 != nil {
|
||||
return "", fmt.Errorf("视频转换失败: %v, 输出: %s, 备用方案失败: %v, 输出: %s", err, string(output), err2, string(output2))
|
||||
}
|
||||
// 备用方案成功,继续执行
|
||||
}
|
||||
|
||||
return tsPath, nil
|
||||
@ -343,7 +347,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
||||
"-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()
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("拼接视频失败: %v", err))
|
||||
@ -366,7 +380,17 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
||||
"-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() // 忽略错误,水印是可选的
|
||||
}
|
||||
|
||||
@ -383,12 +407,18 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
||||
|
||||
// 获取视频时长(使用 ffprobe)
|
||||
seconds := 0
|
||||
cmd = exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", outputPath)
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
var duration float64
|
||||
fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration)
|
||||
seconds = int(duration)
|
||||
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()
|
||||
if err == nil {
|
||||
var duration float64
|
||||
fmt.Sscanf(strings.TrimSpace(string(output)), "%f", &duration)
|
||||
seconds = int(duration)
|
||||
}
|
||||
}
|
||||
|
||||
sizeMB := fileInfo.Size() / 1024 / 1024
|
||||
@ -408,4 +438,3 @@ func (s *VideoService) JoinVideos(ctx context.Context, req VideoConcatRequest) (
|
||||
wg.Wait()
|
||||
return results, nil
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user