/** * package mediaserver * * FFmpeg 自动检测与安装模块 * 在服务启动时检测 FFmpeg 是否可用,如不可用则自动下载安装 */ package mediaserver import ( "archive/tar" "archive/zip" "compress/gzip" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" ) // FFmpeg 下载地址配置 const ( // Linux 静态编译版本 (来自 johnvansickle.com) FFmpegLinuxURL = "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" // Windows 版本 (来自 gyan.dev) FFmpegWindowsURL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" // 默认安装目录 DefaultInstallDir = "/ffmpegs" ) var ( ffmpegPath string ffmpegInitOnce sync.Once ffmpegInitErr error ) // EnsureFFmpeg 确保 FFmpeg 可用,返回可执行文件路径 // 检测顺序: // 1. 系统 PATH 中的 ffmpeg // 2. 自定义安装目录 /ffmpegs/ffmpeg // 3. 如果都不存在,自动下载安装 func EnsureFFmpeg() (string, error) { ffmpegInitOnce.Do(func() { ffmpegPath, ffmpegInitErr = initFFmpeg() }) return ffmpegPath, ffmpegInitErr } // initFFmpeg 初始化 FFmpeg func initFFmpeg() (string, error) { // 1. 检查系统 PATH 中的 ffmpeg if path, err := exec.LookPath("ffmpeg"); err == nil { log.Printf("✅ [FFmpeg] 在系统 PATH 中找到: %s", path) // 验证版本 if err := verifyFFmpeg(path); err == nil { return path, nil } log.Printf("⚠️ [FFmpeg] 系统 ffmpeg 验证失败,尝试其他路径") } // 2. 检查自定义安装目录 customPath := getCustomFFmpegPath() if _, err := os.Stat(customPath); err == nil { log.Printf("✅ [FFmpeg] 在自定义目录找到: %s", customPath) if err := verifyFFmpeg(customPath); err == nil { return customPath, nil } log.Printf("⚠️ [FFmpeg] 自定义 ffmpeg 验证失败,尝试重新下载") } // 3. 下载安装 log.Printf("📥 [FFmpeg] 未找到可用的 ffmpeg,开始下载安装...") return downloadAndInstallFFmpeg(DefaultInstallDir) } // getCustomFFmpegPath 获取自定义安装路径 func getCustomFFmpegPath() string { if runtime.GOOS == "windows" { return filepath.Join(DefaultInstallDir, "ffmpeg.exe") } return filepath.Join(DefaultInstallDir, "ffmpeg") } // verifyFFmpeg 验证 FFmpeg 是否可用 func verifyFFmpeg(path string) error { cmd := exec.Command(path, "-version") output, err := cmd.Output() if err != nil { return fmt.Errorf("执行 ffmpeg -version 失败: %w", err) } // 检查输出是否包含版本信息 outputStr := string(output) if !strings.Contains(outputStr, "ffmpeg version") { return fmt.Errorf("ffmpeg 输出异常: %s", outputStr[:min(len(outputStr), 100)]) } log.Printf("📋 [FFmpeg] 版本信息: %s", strings.Split(outputStr, "\n")[0]) return nil } // downloadAndInstallFFmpeg 下载并安装 FFmpeg func downloadAndInstallFFmpeg(installDir string) (string, error) { // 创建安装目录 if err := os.MkdirAll(installDir, 0755); err != nil { return "", fmt.Errorf("创建安装目录失败: %w", err) } var downloadURL string var extractFunc func(string, string) (string, error) switch runtime.GOOS { case "linux": downloadURL = FFmpegLinuxURL extractFunc = extractTarXz case "windows": downloadURL = FFmpegWindowsURL extractFunc = extractZip default: return "", fmt.Errorf("不支持的操作系统: %s", runtime.GOOS) } // 下载文件 log.Printf("📥 [FFmpeg] 正在从 %s 下载...", downloadURL) tmpFile, err := downloadFile(downloadURL) if err != nil { return "", fmt.Errorf("下载失败: %w", err) } defer os.Remove(tmpFile) // 解压并安装 log.Printf("📦 [FFmpeg] 正在解压安装到 %s ...", installDir) ffmpegBinary, err := extractFunc(tmpFile, installDir) if err != nil { return "", fmt.Errorf("解压失败: %w", err) } // 验证安装 if err := verifyFFmpeg(ffmpegBinary); err != nil { return "", fmt.Errorf("安装验证失败: %w", err) } log.Printf("✅ [FFmpeg] 安装成功: %s", ffmpegBinary) return ffmpegBinary, nil } // downloadFile 下载文件到临时目录 func downloadFile(url string) (string, error) { resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("HTTP 状态码: %d", resp.StatusCode) } // 创建临时文件 tmpFile, err := os.CreateTemp("", "ffmpeg-download-*") if err != nil { return "", err } defer tmpFile.Close() // 下载并显示进度 written, err := io.Copy(tmpFile, resp.Body) if err != nil { os.Remove(tmpFile.Name()) return "", err } log.Printf("📥 [FFmpeg] 下载完成,大小: %.2f MB", float64(written)/1024/1024) return tmpFile.Name(), nil } // extractTarXz 解压 tar.xz 文件 (Linux) func extractTarXz(archivePath, destDir string) (string, error) { // 使用 xz 命令解压(需要系统安装 xz-utils) // 或者使用纯 Go 的 gzip 解压 (如果是 .tar.gz) // 先尝试使用系统命令 cmd := exec.Command("tar", "-xf", archivePath, "-C", destDir) if err := cmd.Run(); err != nil { return "", fmt.Errorf("tar 解压失败: %w", err) } // 查找解压后的 ffmpeg 二进制文件 var ffmpegBinary string err := filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && info.Name() == "ffmpeg" { ffmpegBinary = path return filepath.SkipAll } return nil }) if err != nil && err != filepath.SkipAll { return "", err } if ffmpegBinary == "" { return "", fmt.Errorf("解压后未找到 ffmpeg 二进制文件") } // 移动到目标位置 targetPath := filepath.Join(destDir, "ffmpeg") if ffmpegBinary != targetPath { if err := os.Rename(ffmpegBinary, targetPath); err != nil { // 如果 rename 失败(跨设备),使用复制 if err := copyFile(ffmpegBinary, targetPath); err != nil { return "", err } } } // 设置执行权限 if err := os.Chmod(targetPath, 0755); err != nil { return "", err } return targetPath, nil } // extractZip 解压 zip 文件 (Windows) func extractZip(archivePath, destDir string) (string, error) { r, err := zip.OpenReader(archivePath) if err != nil { return "", err } defer r.Close() var ffmpegBinary string for _, f := range r.File { // 只查找 ffmpeg.exe if strings.HasSuffix(f.Name, "ffmpeg.exe") { rc, err := f.Open() if err != nil { return "", err } targetPath := filepath.Join(destDir, "ffmpeg.exe") outFile, err := os.Create(targetPath) if err != nil { rc.Close() return "", err } _, err = io.Copy(outFile, rc) outFile.Close() rc.Close() if err != nil { return "", err } ffmpegBinary = targetPath break } } if ffmpegBinary == "" { return "", fmt.Errorf("zip 包中未找到 ffmpeg.exe") } return ffmpegBinary, nil } // extractTarGz 解压 tar.gz 文件 func extractTarGz(archivePath, destDir string) (string, error) { f, err := os.Open(archivePath) if err != nil { return "", err } defer f.Close() gzr, err := gzip.NewReader(f) if err != nil { return "", err } defer gzr.Close() tr := tar.NewReader(gzr) var ffmpegBinary string for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return "", err } // 只查找 ffmpeg 二进制文件 if strings.HasSuffix(header.Name, "/ffmpeg") || header.Name == "ffmpeg" { targetPath := filepath.Join(destDir, "ffmpeg") outFile, err := os.Create(targetPath) if err != nil { return "", err } _, err = io.Copy(outFile, tr) outFile.Close() if err != nil { return "", err } // 设置执行权限 if err := os.Chmod(targetPath, 0755); err != nil { return "", err } ffmpegBinary = targetPath break } } if ffmpegBinary == "" { return "", fmt.Errorf("tar.gz 包中未找到 ffmpeg 二进制文件") } return ffmpegBinary, nil } // copyFile 复制文件 func copyFile(src, dst string) error { sourceFile, err := os.Open(src) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(dst) if err != nil { return err } defer destFile.Close() _, err = io.Copy(destFile, sourceFile) return err } // GetFFmpegPath 获取 FFmpeg 路径(仅在已初始化后调用) func GetFFmpegPath() string { return ffmpegPath } // IsFFmpegAvailable 检查 FFmpeg 是否可用 func IsFFmpegAvailable() bool { path, err := EnsureFFmpeg() return err == nil && path != "" }