Files
media/controller/video/concat_controller.go

241 lines
6.6 KiB
Go
Raw Permalink Normal View History

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"
2026-06-10 16:10:10 +08:00
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/utils"
2026-05-19 14:33:06 +08:00
"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)
g.Log().Infof(ctx, "[视频拼接] 收到请求 入参: method=%s, upload=%v, video_urls=%v",
req.Method, req.Upload, req.VideoURLs)
2026-05-20 11:32:39 +08:00
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
}
defer os.Remove(svcRes.OutputPath)
2026-05-20 11:32:39 +08:00
return toDTORes(svcRes), nil
}
2026-05-19 14:33:06 +08:00
// ConcatAsync 视频拼接-异步URL模式 POST /video/concat/async
func (c *video) ConcatAsync(ctx context.Context, req *dto.ConcatAsyncReq) (res *dto.CreateConcatTaskRes, err error) {
ctx = withUser(ctx)
g.Log().Infof(ctx, "[视频拼接-异步] 收到请求 入参: method=%s, upload=%v, callback=%s, video_urls=%v",
req.Method, req.Upload, req.CallbackURL, req.VideoURLs)
if req.Method == "" {
req.Method = "auto"
}
taskID, taskErr := service.Concat.CreateAsyncTask(ctx, req.VideoURLs, req.Method, req.Upload, req.CallbackURL)
if taskErr != nil {
return nil, taskErr
}
return &dto.CreateConcatTaskRes{TaskID: taskID}, nil
}
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)
g.Log().Infof(ctx, "[视频拼接-上传] 收到请求 入参: method=%s, upload=%v", req.Method, req.Upload)
2026-05-20 11:32:39 +08:00
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
}
defer os.Remove(svcRes.OutputPath)
2026-05-20 11:32:39 +08:00
return toDTORes(svcRes), nil
}
// ConcatUploadAsync 视频拼接-异步(文件上传模式) POST /video/concat/upload/async
func (c *video) ConcatUploadAsync(ctx context.Context, req *dto.ConcatUploadAsyncReq) (res *dto.CreateConcatTaskRes, err error) {
ctx = withUser(ctx)
g.Log().Infof(ctx, "[视频拼接-上传-异步] 收到请求 入参: method=%s, upload=%v, callback=%s",
req.Method, req.Upload, req.CallbackURL)
savePaths, err := common.SaveUploadedFilesFromCtx(ctx)
if err != nil || len(savePaths) < 2 {
return nil, fmt.Errorf("至少需要2个视频当前%d个", len(savePaths))
}
defer service.CleanupConcat(savePaths)
if req.Method == "" {
req.Method = "auto"
}
taskID, taskErr := service.Concat.CreateAsyncTaskWithFiles(ctx, savePaths, req.Method, req.Upload, req.CallbackURL)
if taskErr != nil {
return nil, taskErr
}
return &dto.CreateConcatTaskRes{TaskID: taskID}, nil
}
// GetConcatTask 查询异步拼接任务结果 GET /video/concat/task/{taskId}
func (c *video) GetConcatTask(ctx context.Context, req *dto.GetConcatTaskReq) (res *dto.GetConcatTaskRes, err error) {
ctx = withUser(ctx)
return service.Concat.GetTaskResult(ctx, req.TaskID)
}
2026-05-22 17:07:36 +08:00
// withUser 优先从请求头/X-User-Info/Token 提取用户信息,没有则用默认 admin
2026-05-20 11:32:39 +08:00
func withUser(ctx context.Context) context.Context {
2026-05-22 17:07:36 +08:00
if ctx.Value("user") != nil {
return ctx
2026-05-19 14:33:06 +08:00
}
2026-05-22 17:07:36 +08:00
2026-05-25 15:08:47 +08:00
// 调试:打印 Authorization 头
if req := g.RequestFromCtx(ctx); req != nil {
g.Log().Debugf(ctx, "[withUser] Authorization头=%q", req.Header.Get("Authorization"))
}
2026-05-22 17:07:36 +08:00
user, err := utils.GetUserInfo(ctx)
if err == nil && user != nil && user.TenantId > 0 {
g.Log().Infof(ctx, "[用户信息] 从请求头解析到用户: userName=%s, tenantId=%d", user.UserName, user.TenantId)
ctx = context.WithValue(ctx, "user", user)
return ctx
}
if err != nil {
g.Log().Debugf(ctx, "[用户信息] 解析失败(%v), 使用默认admin/tenant=1", err)
}
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1})
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
}