VideoConcat/wails/services/ffmpeg_helper.go
2026-01-08 13:25:06 +08:00

384 lines
9.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}