package setup import ( "bytes" "context" "os" "os/exec" "runtime" "strings" "github.com/gogf/gf/v2/frame/g" ) func init() { ensureDependencies() } // ensureDependencies 启动时检查 ffmpeg 依赖 func ensureDependencies() { ctx := context.Background() g.Log().Info(ctx, "========== 检查依赖环境 ==========") // 打印当前运行环境信息 g.Log().Infof(ctx, "平台: %s/%s, Docker: %v", runtime.GOOS, runtime.GOARCH, isRunningInContainer()) ensureFFmpeg(ctx) ensurePython3(ctx) ensureSceneDetect(ctx) ensureHyperFrames(ctx) g.Log().Info(ctx, "依赖检查完成,所有依赖已就绪") g.Log().Info(ctx, "===================================") } // isRunningInContainer 检测是否运行在 Docker 容器中 func isRunningInContainer() bool { // 方法1: 检查 /.dockerenv 文件 if _, err := os.Stat("/.dockerenv"); err == nil { return true } // 方法2: 检查 /proc/1/cgroup 是否包含 docker 关键字 if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { if strings.Contains(string(data), "docker") || strings.Contains(string(data), "kubepods") || strings.Contains(string(data), "containerd") { return true } } return false } // inContainer 是否为容器环境(简化调用) var inContainer = isRunningInContainer() // ensureFFmpeg 确保 ffmpeg 可用 func ensureFFmpeg(ctx context.Context) { if _, err := exec.LookPath("ffmpeg"); err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 已安装") return } if inContainer { g.Log().Fatalf(ctx, "[ffmpeg] ❌ 容器中未找到 ffmpeg,请在 Dockerfile 中预装: RUN apk add --no-cache ffmpeg") return } g.Log().Infof(ctx, "[ffmpeg] 未找到,尝试自动安装...") switch runtime.GOOS { case "darwin": installFFmpegOnMac(ctx) case "linux": installFFmpegOnLinux(ctx) case "windows": installFFmpegOnWindows(ctx) default: g.Log().Warningf(ctx, "[ffmpeg] ⚠ 不支持的平台(%s),请手动安装 ffmpeg", runtime.GOOS) } } // installFFmpegOnMac 通过 Homebrew 安装 ffmpeg func installFFmpegOnMac(ctx context.Context) { if _, err := exec.LookPath("brew"); err != nil { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 未检测到 Homebrew,请手动安装:\n brew install ffmpeg") return } cmd := exec.CommandContext(ctx, "brew", "install", "ffmpeg") output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") } // installFFmpegOnLinux 在 Linux(含 Docker)上安装 ffmpeg func installFFmpegOnLinux(ctx context.Context) { // Docker 容器通常以 root 运行,不需要 sudo sudoPrefix := "" if !inContainer { // 非容器环境,检查是否需要 sudo if _, err := exec.LookPath("sudo"); err == nil { sudoPrefix = "sudo" } } // 1. 尝试 apt (Debian/Ubuntu) if _, err := exec.LookPath("apt-get"); err == nil { args := []string{"install", "-y", "ffmpeg"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "apt-get", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ apt-get 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") // 更新库缓存(Debian/Ubuntu 会用 ldconfig 更新) return } // 2. 尝试 apk (Alpine Linux,常见于 Docker 精简镜像) if _, err := exec.LookPath("apk"); err == nil { // Alpine 的 apk 不需要 sudo(默认以 root 运行) cmd := exec.CommandContext(ctx, "apk", "add", "ffmpeg") output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ apk 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } // 3. 尝试 yum (CentOS/RHEL) if _, err := exec.LookPath("yum"); err == nil { args := []string{"install", "-y", "ffmpeg"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "yum", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Errorf(ctx, "[ffmpeg] ❌ yum 安装失败: %v\n%s", err, string(output)) return } g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } if inContainer { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 容器中未找到 apt-get/apk/yum,请将 ffmpeg 预装在 Docker 镜像中") } else { g.Log().Warningf(ctx, "[ffmpeg] ⚠ 请手动安装: sudo apt-get install ffmpeg") } } // installFFmpegOnWindows 在 Windows 上安装 ffmpeg func installFFmpegOnWindows(ctx context.Context) { // 1. 尝试 winget (Windows 10/11 内置) if _, err := exec.LookPath("winget"); err == nil { g.Log().Infof(ctx, "[ffmpeg] 通过 winget 安装...") cmd := exec.CommandContext(ctx, "winget", "install", "--id", "FFmpeg.FFmpeg", "-e", "--accept-package-agreements") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ winget 安装失败: %v\n%s", err, string(output)) } // 2. 尝试 choco (Chocolatey) if _, err := exec.LookPath("choco"); err == nil { // choco 安装可能需要管理员权限 g.Log().Infof(ctx, "[ffmpeg] 通过 choco 安装...") cmd := exec.CommandContext(ctx, "choco", "install", "ffmpeg", "-y") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ choco 安装失败: %v\n%s", err, string(output)) } // 3. 尝试 scoop if _, err := exec.LookPath("scoop"); err == nil { g.Log().Infof(ctx, "[ffmpeg] 通过 scoop 安装...") cmd := exec.CommandContext(ctx, "scoop", "install", "ffmpeg") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[ffmpeg] ✔ 安装成功") return } g.Log().Warningf(ctx, "[ffmpeg] ⚠ scoop 安装失败: %v\n%s", err, string(output)) } g.Log().Warningf(ctx, `[ffmpeg] ⚠ 请手动安装 ffmpeg,推荐方式: 1. winget install --id FFmpeg.FFmpeg -e 2. choco install ffmpeg -y 3. 从 https://ffmpeg.org/download.html 下载并加入 PATH`) } // ensureHyperFrames 检查 HyperFrames 是否可用,不可用时自动安装 func ensureHyperFrames(ctx context.Context) { // 1. 优先检查全局 hyperframes 命令 if path, err := exec.LookPath("hyperframes"); err == nil { version := getHyperFramesVersion(path) g.Log().Infof(ctx, "[hyperframes] ✔ 已安装, 版本=%s, 路径=%s", version, path) return } // 2. 检查 npx hyperframes 是否可用 var outBuf bytes.Buffer checkCmd := exec.Command("npx", "hyperframes", "--version") checkCmd.Stdout = &outBuf checkCmd.Stderr = nil if checkCmd.Run() == nil { ver := strings.TrimSpace(outBuf.String()) if ver == "" { ver = "未知" } g.Log().Infof(ctx, "[hyperframes] ✔ 已安装(npx), 版本=%s", ver) return } // 3. 未安装,检查 npm/node 可用性再决定是否自动安装 if _, err := exec.LookPath("npm"); err != nil { if inContainer { g.Log().Infof(ctx, "[hyperframes] npm 不可用,跳过自动安装(Docker 中请预装 Node.js)") } else { g.Log().Infof(ctx, "[hyperframes] npm 不可用,跳过自动安装(请先安装 Node.js: https://nodejs.org)") } return } if inContainer { g.Log().Fatalf(ctx, "[hyperframes] ❌ 容器中未找到 hyperframes,请在 Dockerfile 中预装: RUN npm install -g hyperframes") return } g.Log().Infof(ctx, "[hyperframes] 未找到,尝试自动安装 (npm install -g hyperframes)...") installCmd := exec.Command("npm", "install", "-g", "hyperframes") output, err := installCmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[hyperframes] ⚠ 自动安装失败: %v\n%s", err, string(output)) g.Log().Infof(ctx, "[hyperframes] 请手动安装: npm install -g hyperframes") return } g.Log().Info(ctx, "[hyperframes] ✔ 自动安装成功") // 安装后检查路径 if path, err := exec.LookPath("hyperframes"); err == nil { version := getHyperFramesVersion(path) g.Log().Infof(ctx, "[hyperframes] 路径=%s, 版本=%s", path, version) } } // getHyperFramesVersion 获取 HyperFrames 版本号 func getHyperFramesVersion(path string) string { var outBuf bytes.Buffer vCmd := exec.Command(path, "--version") vCmd.Stdout = &outBuf if vCmd.Run() == nil { return strings.TrimSpace(outBuf.String()) } return "未知" } // ensurePython3 确保 Python3 可用 func ensurePython3(ctx context.Context) { // 优先检查 python3 if path, err := exec.LookPath("python3"); err == nil { version := getPythonVersion(ctx, path) g.Log().Infof(ctx, "[python3] ✔ 已安装, 版本=%s, 路径=%s", version, path) return } // 回退检查 python(部分系统用 python 指向 python3) if path, err := exec.LookPath("python"); err == nil { version := getPythonVersion(ctx, path) if strings.HasPrefix(version, "3.") { g.Log().Infof(ctx, "[python3] ✔ 已安装(python), 版本=%s, 路径=%s", version, path) return } // Python 2,不满足需求 g.Log().Infof(ctx, "[python3] 检测到 python=%s, 需要 Python 3,尝试自动安装...", version) } else { if inContainer { g.Log().Fatalf(ctx, "[python3] ❌ 容器中未找到 python3,请在 Dockerfile 中预装: RUN apk add --no-cache python3 py3-pip") return } g.Log().Infof(ctx, "[python3] 未找到,尝试自动安装...") } installPython3(ctx) } // ensureSceneDetect 确保 scenedetect 库可用 func ensureSceneDetect(ctx context.Context) { // 检查 python3 是否可导入 scenedetect checkCmd := exec.Command("python3", "-c", "import scenedetect; print(scenedetect.__version__)") var outBuf bytes.Buffer checkCmd.Stdout = &outBuf checkCmd.Stderr = nil if checkCmd.Run() == nil { ver := strings.TrimSpace(outBuf.String()) if ver == "" { ver = "未知" } g.Log().Infof(ctx, "[scenedetect] ✔ 已安装, 版本=%s", ver) return } if inContainer { g.Log().Fatalf(ctx, "[scenedetect] ❌ 容器中未找到 scenedetect,请在 Dockerfile 中预装: RUN pip3 install --break-system-packages scenedetect[opencv-headless,ffmpeg]") return } g.Log().Infof(ctx, "[scenedetect] 未找到,尝试自动安装 (pip install scenedetect[opencv,ffmpeg])...") installCmd := exec.Command("pip3", "install", "scenedetect[opencv,ffmpeg]") output, err := installCmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[scenedetect] ⚠ pip3 安装失败: %v\n%s", err, string(output)) g.Log().Infof(ctx, "[scenedetect] 尝试使用 pip 安装...") installCmd = exec.Command("pip", "install", "scenedetect[opencv,ffmpeg]") output, err = installCmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[scenedetect] ⚠ pip 安装也失败: %v\n%s", err, string(output)) if inContainer { g.Log().Infof(ctx, "[scenedetect] Docker 提示: pip 安装失败可能是因为缺少系统级依赖(如 libopencv-dev)。") g.Log().Infof(ctx, "[scenedetect] 建议在 Dockerfile 中预装: apt-get install -y python3-opencv ffmpeg && pip install scenedetect") } else { g.Log().Infof(ctx, "[scenedetect] 请手动安装: pip install 'scenedetect[opencv,ffmpeg]'") } return } } g.Log().Info(ctx, "[scenedetect] ✔ 安装成功") // 验证安装 verifyCmd := exec.Command("python3", "-c", "import scenedetect; print(scenedetect.__version__)") outBuf.Reset() verifyCmd.Stdout = &outBuf if verifyCmd.Run() == nil { g.Log().Infof(ctx, "[scenedetect] 验证通过, 版本=%s", strings.TrimSpace(outBuf.String())) } } // getPythonVersion 获取 Python 版本号 func getPythonVersion(ctx context.Context, pythonPath string) string { var outBuf bytes.Buffer vCmd := exec.CommandContext(ctx, pythonPath, "--version") vCmd.Stdout = &outBuf vCmd.Stderr = &outBuf if vCmd.Run() == nil { return strings.TrimPrefix(strings.TrimSpace(outBuf.String()), "Python ") } return "未知" } // installPython3 根据平台自动安装 Python3 func installPython3(ctx context.Context) { switch runtime.GOOS { case "darwin": if _, err := exec.LookPath("brew"); err == nil { g.Log().Infof(ctx, "[python3] 通过 brew 安装...") cmd := exec.CommandContext(ctx, "brew", "install", "python@3") output, err := cmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[python3] ⚠ brew 安装失败: %v\n%s", err, string(output)) g.Log().Infof(ctx, "[python3] 请手动安装: brew install python@3") } else { g.Log().Info(ctx, "[python3] ✔ 安装成功") } } else { g.Log().Warningf(ctx, "[python3] ⚠ 未检测到 Homebrew,请手动安装 Python3:\n https://www.python.org/downloads/") } case "linux": sudoPrefix := "" if !inContainer { if _, err := exec.LookPath("sudo"); err == nil { sudoPrefix = "sudo" } } // 1. apt (Debian/Ubuntu) if _, err := exec.LookPath("apt-get"); err == nil { args := []string{"install", "-y", "python3", "python3-pip"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "apt-get", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[python3] ⚠ apt-get 安装失败: %v\n%s", err, string(output)) } else { g.Log().Info(ctx, "[python3] ✔ 安装成功") } return } // 2. apk (Alpine) if _, err := exec.LookPath("apk"); err == nil { cmd := exec.CommandContext(ctx, "apk", "add", "python3", "py3-pip") output, err := cmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[python3] ⚠ apk 安装失败: %v\n%s", err, string(output)) } else { g.Log().Info(ctx, "[python3] ✔ 安装成功") } return } // 3. yum (CentOS/RHEL) if _, err := exec.LookPath("yum"); err == nil { args := []string{"install", "-y", "python3", "python3-pip"} if sudoPrefix != "" { args = append([]string{sudoPrefix}, args...) } cmd := exec.CommandContext(ctx, "yum", args...) output, err := cmd.CombinedOutput() if err != nil { g.Log().Warningf(ctx, "[python3] ⚠ yum 安装失败: %v\n%s", err, string(output)) } else { g.Log().Info(ctx, "[python3] ✔ 安装成功") } return } if inContainer { g.Log().Warningf(ctx, "[python3] ⚠ 容器中未找到包管理器,请将 python3 预装在 Docker 镜像中") } else { g.Log().Warningf(ctx, "[python3] ⚠ 请手动安装: sudo apt-get install python3 python3-pip") } case "windows": // 1. winget if _, err := exec.LookPath("winget"); err == nil { g.Log().Infof(ctx, "[python3] 通过 winget 安装...") cmd := exec.CommandContext(ctx, "winget", "install", "--id", "Python.Python.3", "-e", "--accept-package-agreements") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[python3] ✔ 安装成功") return } g.Log().Warningf(ctx, "[python3] ⚠ winget 安装失败: %v\n%s", err, string(output)) } // 2. choco if _, err := exec.LookPath("choco"); err == nil { g.Log().Infof(ctx, "[python3] 通过 choco 安装...") cmd := exec.CommandContext(ctx, "choco", "install", "python", "-y") output, err := cmd.CombinedOutput() if err == nil { g.Log().Info(ctx, "[python3] ✔ 安装成功") return } g.Log().Warningf(ctx, "[python3] ⚠ choco 安装失败: %v\n%s", err, string(output)) } g.Log().Warningf(ctx, `[python3] ⚠ 请手动安装 Python3: 1. winget install --id Python.Python.3 -e 2. 从 https://www.python.org/downloads/ 下载安装(记得勾选"Add to PATH")`) default: g.Log().Warningf(ctx, "[python3] ⚠ 不支持的平台(%s),请手动安装 Python3", runtime.GOOS) } }