Files
nl-video-api/utility/video/video.go
2025-08-03 00:11:15 +08:00

317 lines
7.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package video
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
)
// VideoInfo 视频信息结构
type VideoInfo struct {
Duration int `json:"duration"` // 时长(秒)
Width int `json:"width"` // 宽度
Height int `json:"height"` // 高度
Format string `json:"format"` // 格式
Size int64 `json:"size"` // 文件大小
Bitrate int `json:"bitrate"` // 比特率
FrameRate string `json:"frame_rate"` // 帧率
Resolution string `json:"resolution"` // 分辨率
}
// ExtractCover 从视频中提取封面图片
// videoPath: 视频文件路径
// outputPath: 输出图片路径
// timeOffset: 提取时间点(秒)默认为视频时长的1/3处
func ExtractCover(videoPath, outputPath string, timeOffset ...int) error {
// 检查视频文件是否存在
if !gfile.Exists(videoPath) {
return fmt.Errorf("视频文件不存在: %s", videoPath)
}
// 检查ffmpeg是否可用
if !isFFmpegAvailable() {
return fmt.Errorf("ffmpeg未安装或不可用")
}
// 获取视频信息
videoInfo, err := GetVideoInfo(videoPath)
if err != nil {
return fmt.Errorf("获取视频信息失败: %v", err)
}
// 确定提取时间点
extractTime := videoInfo.Duration / 3 // 默认在1/3处提取
if len(timeOffset) > 0 && timeOffset[0] > 0 {
extractTime = timeOffset[0]
}
// 确保输出目录存在
outputDir := filepath.Dir(outputPath)
if !gfile.Exists(outputDir) {
if err := gfile.Mkdir(outputDir); err != nil {
return fmt.Errorf("创建输出目录失败: %v", err)
}
}
// 构建ffmpeg命令
cmd := exec.Command("ffmpeg",
"-i", videoPath, // 输入文件
"-ss", strconv.Itoa(extractTime), // 跳转到指定时间
"-vframes", "1", // 只提取一帧
"-q:v", "2", // 设置图片质量
"-y", // 覆盖输出文件
outputPath, // 输出文件
)
// 执行命令
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("ffmpeg执行失败: %v, 输出: %s", err, string(output))
}
// 检查输出文件是否生成
if !gfile.Exists(outputPath) {
return fmt.Errorf("封面图片生成失败")
}
g.Log().Infof(nil, "成功从视频 %s 提取封面到 %s", videoPath, outputPath)
return nil
}
// GetVideoInfo 获取视频信息
func GetVideoInfo(videoPath string) (*VideoInfo, error) {
if !gfile.Exists(videoPath) {
return nil, fmt.Errorf("视频文件不存在: %s", videoPath)
}
if !isFFmpegAvailable() {
return nil, fmt.Errorf("ffmpeg未安装或不可用")
}
// 使用ffprobe获取视频信息
cmd := exec.Command("ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
videoPath,
)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("ffprobe执行失败: %v", err)
}
// 解析输出这里简化处理实际项目中应该解析JSON
info := &VideoInfo{}
// 获取文件大小
if fileInfo, err := os.Stat(videoPath); err == nil {
info.Size = fileInfo.Size()
}
// 获取文件格式
ext := strings.ToLower(filepath.Ext(videoPath))
if len(ext) > 1 {
info.Format = ext[1:] // 去掉点号
}
// 简化的信息提取实际应该解析JSON
outputStr := string(output)
if strings.Contains(outputStr, "duration") {
// 这里应该解析JSON获取准确的时长
// 为了简化,设置一个默认值
info.Duration = 3600 // 默认1小时
}
info.Width = 1920
info.Height = 1080
info.Resolution = fmt.Sprintf("%dx%d", info.Width, info.Height)
info.FrameRate = "25"
info.Bitrate = 2000
return info, nil
}
// GenerateThumbnails 生成多个缩略图
func GenerateThumbnails(videoPath, outputDir string, count int) ([]string, error) {
if !gfile.Exists(videoPath) {
return nil, fmt.Errorf("视频文件不存在: %s", videoPath)
}
if !isFFmpegAvailable() {
return nil, fmt.Errorf("ffmpeg未安装或不可用")
}
// 获取视频信息
videoInfo, err := GetVideoInfo(videoPath)
if err != nil {
return nil, fmt.Errorf("获取视频信息失败: %v", err)
}
// 确保输出目录存在
if !gfile.Exists(outputDir) {
if err := gfile.Mkdir(outputDir); err != nil {
return nil, fmt.Errorf("创建输出目录失败: %v", err)
}
}
var thumbnails []string
interval := videoInfo.Duration / (count + 1) // 平均分布
for i := 1; i <= count; i++ {
timeOffset := interval * i
outputPath := filepath.Join(outputDir, fmt.Sprintf("thumb_%d.jpg", i))
if err := ExtractCover(videoPath, outputPath, timeOffset); err != nil {
g.Log().Warningf(nil, "生成缩略图失败: %v", err)
continue
}
thumbnails = append(thumbnails, outputPath)
}
return thumbnails, nil
}
// ConvertVideo 视频格式转换
func ConvertVideo(inputPath, outputPath string, options ...string) error {
if !gfile.Exists(inputPath) {
return fmt.Errorf("输入视频文件不存在: %s", inputPath)
}
if !isFFmpegAvailable() {
return fmt.Errorf("ffmpeg未安装或不可用")
}
// 确保输出目录存在
outputDir := filepath.Dir(outputPath)
if !gfile.Exists(outputDir) {
if err := gfile.Mkdir(outputDir); err != nil {
return fmt.Errorf("创建输出目录失败: %v", err)
}
}
// 构建基础命令
args := []string{"-i", inputPath}
// 添加自定义选项
if len(options) > 0 {
args = append(args, options...)
} else {
// 默认转换选项
args = append(args,
"-c:v", "libx264", // 视频编码器
"-c:a", "aac", // 音频编码器
"-preset", "medium", // 编码预设
"-crf", "23", // 质量控制
)
}
args = append(args, "-y", outputPath) // 覆盖输出文件
cmd := exec.Command("ffmpeg", args...)
// 执行转换
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("视频转换失败: %v, 输出: %s", err, string(output))
}
g.Log().Infof(nil, "视频转换成功: %s -> %s", inputPath, outputPath)
return nil
}
// isFFmpegAvailable 检查ffmpeg是否可用
func isFFmpegAvailable() bool {
cmd := exec.Command("ffmpeg", "-version")
err := cmd.Run()
return err == nil
}
// GetVideoDuration 获取视频时长(秒)
func GetVideoDuration(videoPath string) (int, error) {
info, err := GetVideoInfo(videoPath)
if err != nil {
return 0, err
}
return info.Duration, nil
}
// ValidateVideoFile 验证视频文件
func ValidateVideoFile(filePath string) error {
if !gfile.Exists(filePath) {
return fmt.Errorf("文件不存在")
}
// 检查文件扩展名
ext := strings.ToLower(filepath.Ext(filePath))
allowedExts := []string{".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".m4v"}
isValid := false
for _, allowedExt := range allowedExts {
if ext == allowedExt {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("不支持的视频格式: %s", ext)
}
// 检查文件大小限制为2GB
fileInfo, err := os.Stat(filePath)
if err != nil {
return fmt.Errorf("获取文件信息失败: %v", err)
}
maxSize := int64(2 * 1024 * 1024 * 1024) // 2GB
if fileInfo.Size() > maxSize {
return fmt.Errorf("视频文件过大最大支持2GB")
}
return nil
}
// CleanupTempFiles 清理临时文件
func CleanupTempFiles(dir string, maxAge time.Duration) error {
if !gfile.Exists(dir) {
return nil
}
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
now := time.Now()
for _, entry := range entries {
if entry.IsDir() {
continue
}
filePath := filepath.Join(dir, entry.Name())
fileInfo, err := entry.Info()
if err != nil {
continue
}
if now.Sub(fileInfo.ModTime()) > maxAge {
if err := os.Remove(filePath); err != nil {
g.Log().Warningf(nil, "删除临时文件失败: %s, 错误: %v", filePath, err)
} else {
g.Log().Infof(nil, "清理临时文件: %s", filePath)
}
}
}
return nil
}