视频剪辑上传
This commit is contained in:
@@ -204,22 +204,40 @@ func (s *concatService) concatByFilter(ctx context.Context, ffmpegPath string, i
|
||||
inputArgs = append(inputArgs, "-i", p)
|
||||
}
|
||||
|
||||
// 3. 构建 filter_complex:每个视频 scale+pad 到统一尺寸,然后 concat
|
||||
// 3. 检测每个视频是否有音频轨道及时长
|
||||
hasAudio := make([]bool, n)
|
||||
videoDuration := make([]float64, n)
|
||||
for i, p := range inputs {
|
||||
hasAudio[i] = s.hasVideoAudio(ctx, ffmpegPath, p)
|
||||
videoDuration[i], _ = s.getVideoDuration(ctx, ffmpegPath, p)
|
||||
}
|
||||
|
||||
// 4. 构建 filter_complex:每个视频 scale+pad 到统一尺寸,然后 concat
|
||||
var filterParts []string
|
||||
var concatInputs []string
|
||||
for i := 0; i < n; i++ {
|
||||
filterParts = append(filterParts, fmt.Sprintf(
|
||||
"[%d:v]scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v%d]",
|
||||
i, maxW, maxH, maxW, maxH, i,
|
||||
))
|
||||
filterParts = append(filterParts, fmt.Sprintf(
|
||||
"[%d:a]aresample=44100[a%d]",
|
||||
i, i,
|
||||
))
|
||||
}
|
||||
// 收集归一化后的流
|
||||
var concatInputs []string
|
||||
for i := 0; i < n; i++ {
|
||||
concatInputs = append(concatInputs, fmt.Sprintf("[v%d][a%d]", i, i))
|
||||
if hasAudio[i] {
|
||||
filterParts = append(filterParts, fmt.Sprintf(
|
||||
"[%d:a]aresample=44100[a%d]",
|
||||
i, i,
|
||||
))
|
||||
concatInputs = append(concatInputs, fmt.Sprintf("[v%d][a%d]", i, i))
|
||||
} else {
|
||||
// 无音频轨道,生成匹配视频时长的静音音频
|
||||
dur := videoDuration[i]
|
||||
if dur <= 0 {
|
||||
dur = 30 // 保底30秒
|
||||
}
|
||||
filterParts = append(filterParts, fmt.Sprintf(
|
||||
"aevalsrc=0:n=2:s=44100:d=%.2f[a%d]",
|
||||
dur, i,
|
||||
))
|
||||
concatInputs = append(concatInputs, fmt.Sprintf("[v%d][a%d]", i, i))
|
||||
}
|
||||
}
|
||||
filterStr := fmt.Sprintf("%s;%sconcat=n=%d:v=1:a=1[outv][outa]",
|
||||
strings.Join(filterParts, ";"),
|
||||
@@ -230,8 +248,10 @@ func (s *concatService) concatByFilter(ctx context.Context, ffmpegPath string, i
|
||||
"-filter_complex", filterStr,
|
||||
"-map", "[outv]",
|
||||
"-map", "[outa]",
|
||||
"-preset", "fast",
|
||||
"-crf", "23",
|
||||
"-c:v", "h264_videotoolbox",
|
||||
"-b:v", "5M",
|
||||
"-allow_sw", "true",
|
||||
"-c:a", "aac",
|
||||
"-y",
|
||||
output,
|
||||
)
|
||||
@@ -300,6 +320,28 @@ func (s *concatService) getVideoDuration(ctx context.Context, ffmpegPath, videoP
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// hasVideoAudio 检测视频文件是否有音频轨道
|
||||
func (s *concatService) hasVideoAudio(ctx context.Context, ffmpegPath, videoPath string) bool {
|
||||
ffprobePath := filepath.Join(filepath.Dir(ffmpegPath), "ffprobe")
|
||||
if _, err := os.Stat(ffprobePath); os.IsNotExist(err) {
|
||||
ffprobePath = "ffprobe"
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, ffprobePath,
|
||||
"-v", "error",
|
||||
"-select_streams", "a:0",
|
||||
"-show_entries", "stream=codec_type",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
videoPath,
|
||||
)
|
||||
output, err := cmd.Output()
|
||||
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
|
||||
return false
|
||||
}
|
||||
// 检测视频时长,如果为0则用 aevalsrc 生成静音
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *concatService) getFFmpegPath() (string, error) {
|
||||
ffmpegPath := g.Cfg().MustGet(context.Background(), "ffmpeg.path", "").String()
|
||||
if ffmpegPath != "" {
|
||||
@@ -359,7 +401,7 @@ func (s *concatService) UploadToMinIO(ctx context.Context, localFilePath string)
|
||||
client.Transport = newTransport
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
|
||||
// 透传认证 headers
|
||||
// 透传认证 headers(优先从 HTTP 请求头取)
|
||||
hasAuthHeader := false
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Header {
|
||||
@@ -369,9 +411,10 @@ func (s *concatService) UploadToMinIO(ctx context.Context, localFilePath string)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 原始请求无认证信息时,注入默认用户上下文
|
||||
// 无 HTTP 请求时(异步 goroutine),从 context 的用户信息构造 header
|
||||
if !hasAuthHeader {
|
||||
userJSON, _ := json.Marshal(beans.User{UserName: "admin", TenantId: 1})
|
||||
uploadUser := getUserFromCtx(ctx)
|
||||
userJSON, _ := json.Marshal(uploadUser)
|
||||
client.SetHeader("X-User-Info", string(userJSON))
|
||||
}
|
||||
|
||||
@@ -438,10 +481,13 @@ func (s *concatService) CreateAsyncTask(ctx context.Context, videoURLs []string,
|
||||
return "", fmt.Errorf("创建任务失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取调用方用户信息,传给 goroutine
|
||||
user := getUserFromCtx(ctx)
|
||||
|
||||
g.Log().Infof(ctx, "[异步拼接] 创建任务 %s, 视频数=%d, 回调=%s", taskID, len(videoURLs), callbackURL)
|
||||
|
||||
// 异步处理:先下载再拼接
|
||||
go s.processAsyncTask(taskID, videoURLs, method, upload, callbackURL)
|
||||
go s.processAsyncTask(user, taskID, videoURLs, method, upload, callbackURL)
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
@@ -462,14 +508,27 @@ func (s *concatService) CreateAsyncTaskWithFiles(ctx context.Context, filePaths
|
||||
return "", fmt.Errorf("创建任务失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取调用方用户信息,传给 goroutine
|
||||
user := getUserFromCtx(ctx)
|
||||
|
||||
g.Log().Infof(ctx, "[异步拼接-文件] 创建任务 %s, 文件数=%d, 回调=%s", taskID, len(filePaths), callbackURL)
|
||||
|
||||
// 异步处理:已有本地文件,直接拼接
|
||||
go s.processAsyncTaskWithFiles(taskID, filePaths, method, upload, callbackURL)
|
||||
go s.processAsyncTaskWithFiles(user, taskID, filePaths, method, upload, callbackURL)
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// getUserFromCtx 从 context 中提取用户信息,如果没有则返回默认 admin
|
||||
func getUserFromCtx(ctx context.Context) *beans.User {
|
||||
if u := ctx.Value("user"); u != nil {
|
||||
if user, ok := u.(*beans.User); ok {
|
||||
return user
|
||||
}
|
||||
}
|
||||
return &beans.User{UserName: "admin", TenantId: 1}
|
||||
}
|
||||
|
||||
// GetTaskResult 查询异步任务结果
|
||||
func (s *concatService) GetTaskResult(ctx context.Context, taskID string) (*dto.GetConcatTaskRes, error) {
|
||||
task, err := dao.ConcatTask.GetByTaskID(ctx, taskID)
|
||||
@@ -484,9 +543,9 @@ func (s *concatService) GetTaskResult(ctx context.Context, taskID string) (*dto.
|
||||
}
|
||||
|
||||
// processAsyncTaskWithFiles 后台处理异步拼接任务(文件上传模式,文件已在本地)
|
||||
func (s *concatService) processAsyncTaskWithFiles(taskID string, filePaths []string, method string, upload bool, callbackURL string) {
|
||||
func (s *concatService) processAsyncTaskWithFiles(user *beans.User, taskID string, filePaths []string, method string, upload bool, callbackURL string) {
|
||||
bgCtx := context.Background()
|
||||
bgCtx = context.WithValue(bgCtx, "user", &beans.User{UserName: "admin", TenantId: 1})
|
||||
bgCtx = context.WithValue(bgCtx, "user", user)
|
||||
|
||||
dao.ConcatTask.UpdateRunning(bgCtx, taskID)
|
||||
|
||||
@@ -514,9 +573,9 @@ func (s *concatService) processAsyncTaskWithFiles(taskID string, filePaths []str
|
||||
}
|
||||
|
||||
// processAsyncTask 后台处理异步拼接任务(URL模式,需要先下载)
|
||||
func (s *concatService) processAsyncTask(taskID string, videoURLs []string, method string, upload bool, callbackURL string) {
|
||||
func (s *concatService) processAsyncTask(user *beans.User, taskID string, videoURLs []string, method string, upload bool, callbackURL string) {
|
||||
bgCtx := context.Background()
|
||||
bgCtx = context.WithValue(bgCtx, "user", &beans.User{UserName: "admin", TenantId: 1})
|
||||
bgCtx = context.WithValue(bgCtx, "user", user)
|
||||
|
||||
dao.ConcatTask.UpdateRunning(bgCtx, taskID)
|
||||
|
||||
@@ -628,10 +687,12 @@ func (s *concatService) concatCallback(ctx context.Context, taskID, callbackURL
|
||||
|
||||
req, _ := http.NewRequest("POST", callbackURL, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
userJSON, _ := json.Marshal(beans.User{UserName: "admin", TenantId: 1})
|
||||
// 透传调用方用户信息
|
||||
cbUser := getUserFromCtx(ctx)
|
||||
userJSON, _ := json.Marshal(cbUser)
|
||||
req.Header.Set("X-User-Info", string(userJSON))
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
client := &http.Client{Timeout: 2 * time.Minute}
|
||||
resp, reqErr := client.Do(req)
|
||||
if reqErr != nil {
|
||||
g.Log().Errorf(ctx, "[异步拼接回调 %s] 请求失败: %v", taskID, reqErr)
|
||||
|
||||
Reference in New Issue
Block a user