365 lines
8.4 KiB
Go
365 lines
8.4 KiB
Go
/**
|
||
* 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 != ""
|
||
}
|
||
|
||
|