330 lines
8.8 KiB
Go
330 lines
8.8 KiB
Go
package services
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"math/rand"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// ExtractService 抽帧服务
|
||
type ExtractService struct {
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// NewExtractService 创建抽帧服务实例
|
||
func NewExtractService() *ExtractService {
|
||
return &ExtractService{}
|
||
}
|
||
|
||
// ExtractFrameRequest 抽帧请求
|
||
type ExtractFrameRequest struct {
|
||
FolderPath string `json:"folderPath"`
|
||
ExtractCount int `json:"extractCount"` // 每个视频生成的数量
|
||
}
|
||
|
||
// ExtractFrameResult 抽帧结果
|
||
type ExtractFrameResult struct {
|
||
VideoPath string `json:"videoPath"`
|
||
OutputPath string `json:"outputPath"`
|
||
Success bool `json:"success"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// ListVideos 列出文件夹中的视频文件
|
||
func (s *ExtractService) ListVideos(ctx context.Context, folderPath string) ([]string, error) {
|
||
pattern := filepath.Join(folderPath, "*.mp4")
|
||
matches, err := filepath.Glob(pattern)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("查找视频文件失败: %v", err)
|
||
}
|
||
return matches, nil
|
||
}
|
||
|
||
// RemoveFrameRandom 随机删除视频中的一帧
|
||
func (s *ExtractService) RemoveFrameRandom(ctx context.Context, inputPath string, outputPath string) error {
|
||
// 创建临时目录
|
||
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("extract_%d", time.Now().UnixNano()))
|
||
defer os.RemoveAll(tempDir)
|
||
os.MkdirAll(tempDir, 0755)
|
||
|
||
// 获取视频信息
|
||
helper := GetFFmpegHelper()
|
||
if !helper.IsProbeAvailable() {
|
||
return fmt.Errorf("ffprobe 不可用,请确保已安装 ffmpeg")
|
||
}
|
||
cmd := helper.ProbeCommand("-v", "error", "-show_entries", "format=duration:stream=codec_name,r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", inputPath)
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
return fmt.Errorf("获取视频信息失败: %v", err)
|
||
}
|
||
|
||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||
var duration float64
|
||
var codecName string
|
||
var frameRate float64 = 30.0 // 默认帧率
|
||
|
||
for i, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if i == 0 && line != "" {
|
||
fmt.Sscanf(line, "%f", &duration)
|
||
}
|
||
if strings.HasPrefix(line, "hevc") || strings.HasPrefix(line, "h264") {
|
||
codecName = line
|
||
}
|
||
if strings.Contains(line, "/") {
|
||
parts := strings.Split(line, "/")
|
||
if len(parts) == 2 {
|
||
var num, den float64
|
||
fmt.Sscanf(parts[0], "%f", &num)
|
||
fmt.Sscanf(parts[1], "%f", &den)
|
||
if den > 0 {
|
||
frameRate = num / den
|
||
_ = frameRate // 避免未使用变量警告
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查时长
|
||
if duration < 20 {
|
||
return fmt.Errorf("视频时长太短(%.2f秒),无法抽帧(需要至少20秒)", duration)
|
||
}
|
||
|
||
// 如果是 HEVC,先转换为 H.264
|
||
if codecName == "hevc" {
|
||
if !helper.IsAvailable() {
|
||
return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||
}
|
||
videoConvert := filepath.Join(tempDir, "convert.mp4")
|
||
cmd := helper.Command("-i", inputPath, "-c:v", "libx264", "-y", videoConvert)
|
||
if err := cmd.Run(); err != nil {
|
||
return fmt.Errorf("转换HEVC失败: %v", err)
|
||
}
|
||
inputPath = videoConvert
|
||
}
|
||
|
||
// 随机选择要删除的帧时间点(避开开头和结尾)
|
||
rand.Seed(time.Now().UnixNano())
|
||
minFrameTime := 20.0
|
||
maxFrameTime := duration - 5.0
|
||
if maxFrameTime <= minFrameTime {
|
||
maxFrameTime = minFrameTime + 1.0
|
||
}
|
||
randomFrame := minFrameTime + rand.Float64()*(maxFrameTime-minFrameTime)
|
||
|
||
// 分割视频
|
||
videoPart1 := filepath.Join(tempDir, "part1.mp4")
|
||
videoPart2 := filepath.Join(tempDir, "part2.mp4")
|
||
|
||
// 第一部分:0 到 randomFrame - 0.016
|
||
cmd = helper.Command("-i", inputPath, "-ss", "0", "-t", fmt.Sprintf("%.6f", randomFrame-0.016), "-c", "copy", "-y", videoPart1)
|
||
if err := cmd.Run(); err != nil {
|
||
return fmt.Errorf("裁剪第一部分失败: %v", err)
|
||
}
|
||
|
||
// 第二部分:randomFrame 到结束
|
||
cmd = helper.Command("-i", inputPath, "-ss", fmt.Sprintf("%.6f", randomFrame), "-c", "copy", "-y", videoPart2)
|
||
if err := cmd.Run(); err != nil {
|
||
return fmt.Errorf("裁剪第二部分失败: %v", err)
|
||
}
|
||
|
||
// 合并视频
|
||
concatFile := filepath.Join(tempDir, "concat.txt")
|
||
file, err := os.Create(concatFile)
|
||
if err != nil {
|
||
return fmt.Errorf("创建concat文件失败: %v", err)
|
||
}
|
||
file.WriteString(fmt.Sprintf("file '%s'\n", strings.ReplaceAll(videoPart1, "\\", "/")))
|
||
file.WriteString(fmt.Sprintf("file '%s'\n", strings.ReplaceAll(videoPart2, "\\", "/")))
|
||
file.Close()
|
||
|
||
cmd = helper.Command("-f", "concat", "-safe", "0", "-i", concatFile, "-c", "copy", "-y", outputPath)
|
||
if err := cmd.Run(); err != nil {
|
||
return fmt.Errorf("合并视频失败: %v", err)
|
||
}
|
||
|
||
// 验证输出文件
|
||
if _, err := os.Stat(outputPath); err != nil {
|
||
return fmt.Errorf("输出文件不存在: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ExtractFrames 批量抽帧
|
||
func (s *ExtractService) ExtractFrames(ctx context.Context, req ExtractFrameRequest) ([]ExtractFrameResult, error) {
|
||
var results []ExtractFrameResult
|
||
// 列出所有视频
|
||
videos, err := s.ListVideos(ctx, req.FolderPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(videos) == 0 {
|
||
return nil, fmt.Errorf("没有找到视频文件")
|
||
}
|
||
|
||
// 创建输出目录
|
||
outputDir := filepath.Join(req.FolderPath, "out")
|
||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||
return nil, fmt.Errorf("创建输出目录失败: %v", err)
|
||
}
|
||
|
||
// 生成任务列表
|
||
type Task struct {
|
||
VideoPath string
|
||
Index int
|
||
OutputPath string
|
||
}
|
||
|
||
var tasks []Task
|
||
for _, video := range videos {
|
||
originalFileName := strings.TrimSuffix(filepath.Base(video), filepath.Ext(video))
|
||
extension := filepath.Ext(video)
|
||
for i := 1; i <= req.ExtractCount; i++ {
|
||
outputFileName := fmt.Sprintf("%s_%04d%s", originalFileName, i, extension)
|
||
outputPath := filepath.Join(outputDir, outputFileName)
|
||
tasks = append(tasks, Task{
|
||
VideoPath: video,
|
||
Index: i,
|
||
OutputPath: outputPath,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 并发处理(限制并发数)
|
||
semaphore := make(chan struct{}, 10)
|
||
var wg sync.WaitGroup
|
||
// total := len(tasks) // 未使用
|
||
current := 0
|
||
var mu sync.Mutex
|
||
|
||
for _, task := range tasks {
|
||
wg.Add(1)
|
||
go func(t Task) {
|
||
defer wg.Done()
|
||
semaphore <- struct{}{}
|
||
defer func() { <-semaphore }()
|
||
|
||
// 检查文件是否已存在
|
||
if _, err := os.Stat(t.OutputPath); err == nil {
|
||
mu.Lock()
|
||
current++
|
||
result := ExtractFrameResult{
|
||
VideoPath: t.VideoPath,
|
||
OutputPath: t.OutputPath,
|
||
Success: true,
|
||
}
|
||
results = append(results, result)
|
||
mu.Unlock()
|
||
return
|
||
}
|
||
|
||
err := s.RemoveFrameRandom(ctx, t.VideoPath, t.OutputPath)
|
||
mu.Lock()
|
||
current++
|
||
var result ExtractFrameResult
|
||
if err != nil {
|
||
result = ExtractFrameResult{
|
||
VideoPath: t.VideoPath,
|
||
Success: false,
|
||
Error: err.Error(),
|
||
}
|
||
} else {
|
||
result = ExtractFrameResult{
|
||
VideoPath: t.VideoPath,
|
||
OutputPath: t.OutputPath,
|
||
Success: true,
|
||
}
|
||
}
|
||
results = append(results, result)
|
||
mu.Unlock()
|
||
}(task)
|
||
}
|
||
|
||
wg.Wait()
|
||
return results, nil
|
||
}
|
||
|
||
// ModifyByMetadata 通过修改元数据改变文件 MD5
|
||
func (s *ExtractService) ModifyByMetadata(ctx context.Context, inputPath string, outputPath string) error {
|
||
helper := GetFFmpegHelper()
|
||
if !helper.IsAvailable() {
|
||
return fmt.Errorf("ffmpeg 不可用,请确保已安装 ffmpeg")
|
||
}
|
||
comment := fmt.Sprintf("JSY_%s", time.Now().Format("20060102150405"))
|
||
cmd := helper.Command("-i", inputPath,
|
||
"-c", "copy",
|
||
"-metadata", fmt.Sprintf("comment=%s", comment),
|
||
"-y", outputPath)
|
||
|
||
if err := cmd.Run(); err != nil {
|
||
return fmt.Errorf("修改元数据失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ModifyVideosMetadata 批量修改视频元数据
|
||
func (s *ExtractService) ModifyVideosMetadata(ctx context.Context, folderPath string) ([]ExtractFrameResult, error) {
|
||
var results []ExtractFrameResult
|
||
videos, err := s.ListVideos(ctx, folderPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
outputDir := filepath.Join(folderPath, "out")
|
||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||
return nil, fmt.Errorf("创建输出目录失败: %v", err)
|
||
}
|
||
|
||
semaphore := make(chan struct{}, 10)
|
||
var wg sync.WaitGroup
|
||
// total := len(videos) // 未使用
|
||
current := 0
|
||
var mu sync.Mutex
|
||
|
||
for _, video := range videos {
|
||
wg.Add(1)
|
||
go func(videoPath string) {
|
||
defer wg.Done()
|
||
semaphore <- struct{}{}
|
||
defer func() { <-semaphore }()
|
||
|
||
rand.Seed(time.Now().UnixNano())
|
||
randomNum := rand.Intn(90000) + 10000
|
||
outputFileName := fmt.Sprintf("modify%d%s", randomNum, filepath.Base(videoPath))
|
||
outputPath := filepath.Join(outputDir, outputFileName)
|
||
|
||
err := s.ModifyByMetadata(ctx, videoPath, outputPath)
|
||
mu.Lock()
|
||
current++
|
||
var result ExtractFrameResult
|
||
if err != nil {
|
||
result = ExtractFrameResult{
|
||
VideoPath: videoPath,
|
||
Success: false,
|
||
Error: err.Error(),
|
||
}
|
||
} else {
|
||
result = ExtractFrameResult{
|
||
VideoPath: videoPath,
|
||
OutputPath: outputPath,
|
||
Success: true,
|
||
}
|
||
}
|
||
results = append(results, result)
|
||
mu.Unlock()
|
||
}(video)
|
||
}
|
||
|
||
wg.Wait()
|
||
return results, nil
|
||
}
|
||
|