2026-05-19 14:33:06 +08:00
|
|
|
|
package video
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type video struct{}
|
|
|
|
|
|
|
|
|
|
|
|
var Concat = new(video)
|
|
|
|
|
|
|
2026-05-20 11:32:39 +08:00
|
|
|
|
// Concat 视频拼接(URL模式) POST /video/concat
|
|
|
|
|
|
func (c *video) Concat(ctx context.Context, req *dto.ConcatReq) (res *dto.ConcatRes, err error) {
|
|
|
|
|
|
ctx = withUser(ctx)
|
|
|
|
|
|
if req.Method == "" {
|
|
|
|
|
|
req.Method = "auto"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
savePaths, err := downloadVideos(ctx, req.VideoURLs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer cleanupConcat(savePaths)
|
|
|
|
|
|
|
|
|
|
|
|
svcRes, err := service.Concat.Concat(ctx, &service.ConcatReq{
|
|
|
|
|
|
VideoPaths: savePaths,
|
|
|
|
|
|
Method: req.Method,
|
|
|
|
|
|
Upload: req.Upload,
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
2026-05-19 14:33:06 +08:00
|
|
|
|
}
|
2026-05-20 11:32:39 +08:00
|
|
|
|
return toDTORes(svcRes), nil
|
|
|
|
|
|
}
|
2026-05-19 14:33:06 +08:00
|
|
|
|
|
2026-05-20 11:32:39 +08:00
|
|
|
|
// ConcatUpload 视频拼接(文件上传模式) POST /video/concat/upload
|
|
|
|
|
|
func (c *video) ConcatUpload(ctx context.Context, req *dto.ConcatUploadReq) (res *dto.ConcatRes, err error) {
|
|
|
|
|
|
ctx = withUser(ctx)
|
|
|
|
|
|
savePaths, err := common.SaveUploadedFilesFromCtx(ctx)
|
2026-05-19 14:33:06 +08:00
|
|
|
|
if err != nil || len(savePaths) < 2 {
|
2026-05-20 11:32:39 +08:00
|
|
|
|
return nil, fmt.Errorf("至少需要2个视频,当前%d个", len(savePaths))
|
2026-05-19 14:33:06 +08:00
|
|
|
|
}
|
2026-05-20 11:32:39 +08:00
|
|
|
|
defer service.CleanupConcat(savePaths)
|
2026-05-19 14:33:06 +08:00
|
|
|
|
|
2026-05-20 11:32:39 +08:00
|
|
|
|
if req.Method == "" {
|
|
|
|
|
|
req.Method = "auto"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
svcRes, err := service.Concat.Concat(ctx, &service.ConcatReq{
|
2026-05-19 14:33:06 +08:00
|
|
|
|
VideoPaths: savePaths,
|
2026-05-20 11:32:39 +08:00
|
|
|
|
Method: req.Method,
|
|
|
|
|
|
Upload: req.Upload,
|
2026-05-19 14:33:06 +08:00
|
|
|
|
})
|
2026-05-20 11:32:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return toDTORes(svcRes), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// withUser 为 context 注入默认用户(无认证基础设施时使用)
|
|
|
|
|
|
func withUser(ctx context.Context) context.Context {
|
|
|
|
|
|
if ctx.Value("user") == nil {
|
|
|
|
|
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1})
|
2026-05-19 14:33:06 +08:00
|
|
|
|
}
|
2026-05-20 11:32:39 +08:00
|
|
|
|
return ctx
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// toDTORes 将 Service 内部响应类型转换为 DTO 响应类型
|
|
|
|
|
|
func toDTORes(svcRes *service.ConcatRes) *dto.ConcatRes {
|
|
|
|
|
|
return &dto.ConcatRes{
|
|
|
|
|
|
OutputPath: svcRes.OutputPath,
|
|
|
|
|
|
FileSize: svcRes.FileSize,
|
|
|
|
|
|
Duration: svcRes.Duration,
|
|
|
|
|
|
DurationStr: svcRes.DurationStr,
|
|
|
|
|
|
MethodUsed: svcRes.MethodUsed,
|
|
|
|
|
|
InputFiles: svcRes.InputFiles,
|
|
|
|
|
|
FileURL: svcRes.FileURL,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// downloadVideos 下载视频URL列表
|
|
|
|
|
|
func downloadVideos(ctx context.Context, videoURLs []string) ([]string, error) {
|
|
|
|
|
|
tempDir := getTempDir(ctx)
|
|
|
|
|
|
os.MkdirAll(tempDir, 0755)
|
2026-05-19 14:33:06 +08:00
|
|
|
|
|
2026-05-20 11:32:39 +08:00
|
|
|
|
var savePaths []string
|
|
|
|
|
|
for _, videoURL := range videoURLs {
|
|
|
|
|
|
savePath, dlErr := downloadFromURL(ctx, videoURL, tempDir)
|
|
|
|
|
|
if dlErr != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
savePaths = append(savePaths, savePath)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(savePaths) < 2 {
|
|
|
|
|
|
return savePaths, fmt.Errorf("成功下载的视频不足2个")
|
|
|
|
|
|
}
|
|
|
|
|
|
return savePaths, nil
|
2026-05-19 14:33:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-20 11:32:39 +08:00
|
|
|
|
|
|
|
|
|
|
func getTempDir(ctx context.Context) string {
|
|
|
|
|
|
tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String()
|
|
|
|
|
|
if tempDir == "" {
|
|
|
|
|
|
tempDir = "resource/temp"
|
|
|
|
|
|
}
|
|
|
|
|
|
if !filepath.IsAbs(tempDir) {
|
|
|
|
|
|
absDir, _ := filepath.Abs(tempDir)
|
|
|
|
|
|
tempDir = absDir
|
|
|
|
|
|
}
|
|
|
|
|
|
return tempDir
|
|
|
|
|
|
}
|