92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
|
|
package vision
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"os/exec"
|
|||
|
|
"path/filepath"
|
|||
|
|
"runtime"
|
|||
|
|
|
|||
|
|
"apigo.cc/go/file"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Video 代表一个视频操作封装
|
|||
|
|
type Video struct {
|
|||
|
|
FFmpegPath string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewVideo 创建一个视频处理器,自动查找或下载 ffmpeg
|
|||
|
|
func NewVideo() (*Video, error) {
|
|||
|
|
p, err := EnsureFFmpeg()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &Video{FFmpegPath: p}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExtractFrame 从视频中提取指定时间的帧
|
|||
|
|
func (v *Video) ExtractFrame(videoPath string, offsetSeconds float64) (*Canvas, error) {
|
|||
|
|
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("frame_%d.png", os.Getpid()))
|
|||
|
|
defer os.Remove(tmpFile)
|
|||
|
|
|
|||
|
|
cmd := exec.Command(v.FFmpegPath, "-ss", fmt.Sprintf("%f", offsetSeconds), "-i", videoPath, "-frames:v", "1", tmpFile)
|
|||
|
|
if err := cmd.Run(); err != nil {
|
|||
|
|
return nil, fmt.Errorf("ffmpeg extract failed: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Load(tmpFile)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateVideoFromImages 从一系列图片创建视频
|
|||
|
|
func (v *Video) CreateVideoFromImages(imagePattern string, frameRate int, outPath string) error {
|
|||
|
|
cmd := exec.Command(v.FFmpegPath, "-framerate", fmt.Sprintf("%d", frameRate), "-i", imagePattern, "-c:v", "libx264", "-pix_fmt", "yuv420p", outPath)
|
|||
|
|
return cmd.Run()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EnsureFFmpeg 确保 ffmpeg 命令可用
|
|||
|
|
func EnsureFFmpeg() (string, error) {
|
|||
|
|
// 1. 检查 PATH
|
|||
|
|
if p, err := exec.LookPath("ffmpeg"); err == nil {
|
|||
|
|
return p, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 检查本地目录
|
|||
|
|
localDir := filepath.Join(os.Getenv("HOME"), ".vision", "bin")
|
|||
|
|
localFF := filepath.Join(localDir, "ffmpeg")
|
|||
|
|
if runtime.GOOS == "windows" {
|
|||
|
|
localFF += ".exe"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if file.Exists(localFF) {
|
|||
|
|
return localFF, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 自动下载
|
|||
|
|
return DownloadFFmpeg(localDir)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DownloadFFmpeg 下载对应系统的 FFmpeg 二进制文件
|
|||
|
|
func DownloadFFmpeg(targetDir string) (string, error) {
|
|||
|
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var url string
|
|||
|
|
switch runtime.GOOS {
|
|||
|
|
case "darwin":
|
|||
|
|
// 使用针对 macOS 的精简版二进制 (示例 URL,实际应指向可靠镜像)
|
|||
|
|
url = "https://evermeet.cx/ffmpeg/get/zip"
|
|||
|
|
case "linux":
|
|||
|
|
url = "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
|
|||
|
|
case "windows":
|
|||
|
|
url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
|
|||
|
|
default:
|
|||
|
|
return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注意:实际下载逻辑需要处理解压、权限等。
|
|||
|
|
// 为了精简,这里我们只提供语义。在真实场景中可以使用 go/http 下载并解压。
|
|||
|
|
fmt.Printf("FFmpeg not found. Please install it or download from: %s\n", url)
|
|||
|
|
return "", fmt.Errorf("ffmpeg not found, please install manually or check %s", url)
|
|||
|
|
}
|