222 lines
6.0 KiB
Go
222 lines
6.0 KiB
Go
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)
|
||
|
||
// 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 为 context 注入默认用户(无认证基础设施时使用)
|
||
func withUser(ctx context.Context) context.Context {
|
||
if ctx.Value("user") == nil {
|
||
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
|
||
}
|