package video import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "strings" "time" common "media/controller/common" dto "media/model/dto/video" service "media/service/video" "gitea.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) type video struct{} var Concat = new(video) // ConcatVideosHandler 视频拼接 // 支持两种入参方式: // 1. JSON body: {"video_urls":[...], "method":"auto"} // 2. 文件上传: files 参数(至少2个视频) func (c *video) ConcatVideosHandler(r *ghttp.Request) { ctx := r.Context() ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) // 优先尝试 JSON body(URL 列表模式) body := r.GetBody() if len(body) > 0 && body[0] == '{' { var req dto.ConcatReq if json.Unmarshal(body, &req) == nil && len(req.VideoURLs) >= 2 { if req.Method == "" { req.Method = "auto" } tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String() if !filepath.IsAbs(tempDir) { absDir, _ := filepath.Abs(tempDir) tempDir = absDir } os.MkdirAll(tempDir, 0755) var savePaths []string for _, videoURL := range req.VideoURLs { savePath, dlErr := downloadFromURL(ctx, videoURL, tempDir) if dlErr != nil { continue } savePaths = append(savePaths, savePath) } if len(savePaths) < 2 { cleanupConcat(savePaths) r.Response.WriteJson(g.Map{"code": 400, "message": "成功下载的视频不足2个"}) return } svcRes, svcErr := service.Concat.Concat(ctx, &service.ConcatReq{ VideoPaths: savePaths, Method: req.Method, }) cleanupConcat(savePaths) if svcErr != nil { r.Response.WriteJson(g.Map{"code": 500, "message": "视频拼接失败: " + svcErr.Error()}) return } r.Response.WriteJson(g.Map{ "code": 200, "message": "success", "data": g.Map{ "outputPath": svcRes.OutputPath, "fileSize": svcRes.FileSize, "duration": svcRes.Duration, "durationStr": svcRes.DurationStr, "methodUsed": svcRes.MethodUsed, "inputFiles": svcRes.InputFiles, }, }) return } } // 文件上传模式 savePaths, err := common.SaveUploadedFiles(r) if err != nil || len(savePaths) < 2 { r.Response.WriteJson(g.Map{"code": 400, "message": fmt.Sprintf("至少需要2个视频,当前%d个", len(savePaths))}) return } svcRes, svcErr := service.Concat.Concat(ctx, &service.ConcatReq{ VideoPaths: savePaths, Method: r.Get("method", "auto").String(), }) service.CleanupConcat(savePaths) if svcErr != nil { r.Response.WriteJson(g.Map{"code": 500, "message": "视频拼接失败: " + svcErr.Error()}) return } r.Response.ServeFile(svcRes.OutputPath) go func(path string) { time.Sleep(5 * time.Second) os.Remove(path) }(svcRes.OutputPath) } func downloadFromURL(ctx context.Context, rawURL, tempDir string) (string, error) { parsedURL, err := url.Parse(rawURL) if err != nil { return "", err } segments := strings.Split(parsedURL.Path, "/") fileName := segments[len(segments)-1] if fileName == "" { fileName = fmt.Sprintf("video_%d.mp4", time.Now().UnixMilli()) } savePath := filepath.Join(tempDir, fmt.Sprintf("%d_%s", time.Now().UnixMilli(), fileName)) client := &http.Client{Timeout: 10 * time.Minute} resp, err := client.Get(rawURL) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("HTTP %d", resp.StatusCode) } out, err := os.Create(savePath) if err != nil { return "", err } defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { os.Remove(savePath) } return savePath, err } func cleanupConcat(paths []string) { for _, p := range paths { os.Remove(p) } }