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 }