Files
media/controller/video/concat_controller.go
2026-05-25 15:08:47 +08:00

241 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
)
type video struct{}
var Concat = new(video)
// 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)
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
}
defer os.Remove(svcRes.OutputPath)
return toDTORes(svcRes), nil
}
// 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
}
// 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)
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"
}
svcRes, err := service.Concat.Concat(ctx, &service.ConcatReq{
VideoPaths: savePaths,
Method: req.Method,
Upload: req.Upload,
})
if err != nil {
return nil, err
}
defer os.Remove(svcRes.OutputPath)
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)
}
// withUser 优先从请求头/X-User-Info/Token 提取用户信息,没有则用默认 admin
func withUser(ctx context.Context) context.Context {
if ctx.Value("user") != nil {
return ctx
}
// 调试:打印 Authorization 头
if req := g.RequestFromCtx(ctx); req != nil {
g.Log().Debugf(ctx, "[withUser] Authorization头=%q", req.Header.Get("Authorization"))
}
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})
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)
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
}
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)
}
}
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
}