初始化v1

This commit is contained in:
2025-08-03 00:11:15 +08:00
commit b88c8b8afa
171 changed files with 22353 additions and 0 deletions

317
utility/video/video.go Normal file
View File

@@ -0,0 +1,317 @@
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
}