107 lines
3.2 KiB
Go
107 lines
3.2 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)
|
||
}
|
||
|
||
// WatermarkVideo 给视频添加水印
|
||
// videoPath: 输入视频
|
||
// markPath: 水印图片路径
|
||
// outPath: 输出视频路径
|
||
// pos: 水印位置 (使用 FFmpeg 语法, 如 '10:10', 'main_w-overlay_w-10:10')
|
||
func (v *Video) WatermarkVideo(videoPath, markPath, outPath, pos string) error {
|
||
if pos == "" {
|
||
pos = "main_w-overlay_w-10:main_h-overlay_h-10" // 默认右下角
|
||
}
|
||
|
||
filter := fmt.Sprintf("overlay=%s", pos)
|
||
cmd := exec.Command(v.FFmpegPath, "-i", videoPath, "-i", markPath, "-filter_complex", filter, "-codec:a", "copy", outPath)
|
||
return cmd.Run()
|
||
}
|
||
|
||
// 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)
|
||
}
|