2025-04-13 11:33:55 +08:00

396 lines
12 KiB
Go
Raw 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 util
import (
"ZhenTuLocalPassiveAdapter/dto"
"bytes"
"context"
"fmt"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"log"
"math/rand"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"time"
)
const FfmpegExec = "ffmpeg"
func RunFfmpegTask(ctx context.Context, task *dto.FfmpegTask) bool {
_, span := tracer.Start(ctx, "RunFfmpegTask")
defer span.End()
var result bool
if len(task.Files) == 1 {
// 单个文件切割,用简单方法
result = runFfmpegForSingleFile(ctx, task)
} else {
// 多个文件切割,用速度快的
result = runFfmpegForMultipleFile1(ctx, task)
}
// 先尝试方法1
if result {
span.SetStatus(codes.Ok, "FFMPEG简易方法成功")
return true
}
log.Printf("FFMPEG简易方法失败尝试复杂方法转码")
// 不行再尝试方法二
result = runFfmpegForMultipleFile2(ctx, task)
if result {
span.SetStatus(codes.Ok, "FFMPEG复杂方法成功")
return true
}
span.SetStatus(codes.Error, "FFMPEG复杂方法失败")
return result
}
func runFfmpegForMultipleFile1(ctx context.Context, task *dto.FfmpegTask) bool {
_, span := tracer.Start(ctx, "runFfmpegForMultipleFile1")
defer span.End()
// 多文件方法一先转换成ts然后合并切割
// 步骤一先转换成ts并行转换
var wg sync.WaitGroup
var mu sync.Mutex
var notOk bool
for i := range task.Files {
wg.Add(1)
go func(file *dto.File) {
defer wg.Done()
tmpFile := path.Join(os.TempDir(), file.Name+".ts")
result, err := convertMp4ToTs(ctx, *file, tmpFile)
if err != nil {
log.Printf("转码出错: %v", err)
mu.Lock()
notOk = true
mu.Unlock()
return
}
if result {
mu.Lock()
file.Url = tmpFile
mu.Unlock()
} else {
// 失败了,务必删除临时文件
os.Remove(tmpFile)
}
}(&task.Files[i])
}
wg.Wait()
if notOk {
span.SetStatus(codes.Error, "FFMPEG多文件转码失败")
return false
}
// 步骤二使用concat协议拼接裁切
result, err := QuickConcatVideoCut(ctx, task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
span.SetAttributes(attribute.String("error", err.Error()))
span.SetStatus(codes.Error, "FFMPEG多文件concat协议转码失败")
return false
}
// 步骤三:删除临时文件
for _, file := range task.Files {
if err := os.Remove(file.Url); err != nil {
log.Printf("删除临时文件失败: %v", err)
}
}
if result {
span.SetStatus(codes.Ok, "FFMPEG多文件concat协议转码成功")
} else {
span.SetStatus(codes.Error, "FFMPEG多文件concat协议转码失败")
}
return result
}
func runFfmpegForMultipleFile2(ctx context.Context, task *dto.FfmpegTask) bool {
_, span := tracer.Start(ctx, "runFfmpegForMultipleFile2")
defer span.End()
// 多文件,方法二:使用计算资源编码
result, err := SlowVideoCut(ctx, task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
return false
}
return result
}
func runFfmpegForSingleFile(ctx context.Context, task *dto.FfmpegTask) bool {
_, span := tracer.Start(ctx, "runFfmpegForSingleFile")
defer span.End()
result, err := QuickVideoCut(ctx, task.Files[0].Url, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
span.SetStatus(codes.Error, "FFMPEG单个文件裁切失败")
return false
}
stat, err := os.Stat(task.OutputFile)
if err != nil {
span.SetStatus(codes.Error, "文件不存在")
log.Printf("文件不存在:%s", task.OutputFile)
return false
}
span.SetAttributes(attribute.String("file.name", task.OutputFile))
span.SetAttributes(attribute.Int64("file.size", stat.Size()))
return result
}
func CheckFileCoverageAndConstructTask(ctx context.Context, fileList []dto.File, beginDt, endDt time.Time, task dto.Task) (*dto.FfmpegTask, error) {
_, span := tracer.Start(ctx, "CheckFileCoverageAndConstructTask")
defer span.End()
if fileList == nil || len(fileList) == 0 {
span.SetStatus(codes.Error, "无法根据要求找到对应录制片段")
log.Printf("无法根据要求找到对应录制片段ID【%s】开始时间【%s】结束时间【%s】", task.TaskID, beginDt, endDt)
return nil, fmt.Errorf("无法根据要求找到对应录制片段")
}
// 如果片段在中间断开时间过长
if len(fileList) > 1 {
var lastFile *dto.File
for _, file := range fileList {
if lastFile == nil {
lastFile = &file
continue
}
if file.StartTime.Sub(lastFile.EndTime).Milliseconds() > 2000 {
// 片段断开
span.SetStatus(codes.Error, "FFMPEG片段断开")
log.Printf("分析FFMPEG任务失败ID【%s】文件片段【%s,%s】中间断开【%f】秒(超过2秒)", task.TaskID, lastFile.Name, file.Name, file.StartTime.Sub(lastFile.EndTime).Seconds())
return nil, fmt.Errorf("片段断开")
}
lastFile = &file
}
}
// 通过文件列表构造的任务仍然是缺失的
if fileList[len(fileList)-1].EndTime.Before(endDt) {
span.SetStatus(codes.Error, "FFMPEG片段断开")
log.Printf("分析FFMPEG任务失败ID【%s】文件片段【%s】无法完整覆盖时间点【%s】", task.TaskID, fileList[len(fileList)-1].Name, endDt)
return nil, fmt.Errorf("片段断开")
}
// 构造FfmpegTaskPo
ffmpegTask := &dto.FfmpegTask{
Files: fileList,
Length: int(endDt.Sub(beginDt).Seconds()),
Offset: int(beginDt.Sub(fileList[0].StartTime).Seconds()),
OutputFile: path.Join(os.TempDir(), task.TaskID+".mp4"),
}
span.SetAttributes(attribute.Int("task.files", len(ffmpegTask.Files)))
span.SetAttributes(attribute.Int("task.offset", ffmpegTask.Offset))
span.SetAttributes(attribute.Int("task.length", ffmpegTask.Length))
span.SetStatus(codes.Ok, "FFMPEG任务构造成功")
return ffmpegTask, nil
}
func convertMp4ToTs(ctx context.Context, file dto.File, outFileName string) (bool, error) {
_, span := tracer.Start(ctx, "convertMp4ToTs")
defer span.End()
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-i", file.Url,
"-c", "copy",
"-bsf:v", "h264_mp4toannexb",
"-f", "mpegts",
outFileName,
}
return handleFfmpegProcess(ctx, ffmpegCmd)
}
func convertHevcToTs(ctx context.Context, file dto.File, outFileName string) (bool, error) {
_, span := tracer.Start(ctx, "convertHevcToTs")
defer span.End()
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-i", file.Url,
"-c", "copy",
"-bsf:v", "hevc_mp4toannexb",
"-f", "mpegts",
outFileName,
}
return handleFfmpegProcess(ctx, ffmpegCmd)
}
func QuickVideoCut(ctx context.Context, inputFile string, offset, length int64, outputFile string) (bool, error) {
_, span := tracer.Start(ctx, "QuickVideoCut")
defer span.End()
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-i", inputFile,
"-c:v", "copy",
"-an",
"-reset_timestamps", "1",
"-ss", strconv.FormatInt(offset, 10),
"-t", strconv.FormatInt(length, 10),
"-fflags", "+genpts",
"-f", "mp4",
outputFile,
}
return handleFfmpegProcess(ctx, ffmpegCmd)
}
func QuickConcatVideoCut(ctx context.Context, inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
_, span := tracer.Start(ctx, "QuickConcatVideoCut")
defer span.End()
tmpFile := fmt.Sprintf("tmp%.10f.txt", rand.Float64())
tmpFileObj, err := os.Create(tmpFile)
if err != nil {
span.SetAttributes(attribute.String("error", err.Error()))
span.SetStatus(codes.Error, "创建临时文件失败")
log.Printf("创建临时文件失败:%s", tmpFile)
return false, err
}
defer os.Remove(tmpFile)
defer tmpFileObj.Close()
for _, filePo := range inputFiles {
_, err := tmpFileObj.WriteString(fmt.Sprintf("file '%s'\n", filePo.Url))
if err != nil {
span.SetAttributes(attribute.String("error", err.Error()))
span.SetStatus(codes.Error, "写入临时文件失败")
log.Printf("写入临时文件失败:%s", tmpFile)
return false, err
}
}
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-f", "concat",
"-safe", "0",
"-i", tmpFile,
"-c:v", "copy",
"-an",
"-ss", strconv.FormatInt(offset, 10),
"-t", strconv.FormatInt(length, 10),
"-f", "mp4",
outputFile,
}
return handleFfmpegProcess(ctx, ffmpegCmd)
}
func SlowVideoCut(ctx context.Context, inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
_, span := tracer.Start(ctx, "SlowVideoCut")
defer span.End()
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
}
for _, file := range inputFiles {
ffmpegCmd = append(ffmpegCmd, "-i", file.Url)
}
inputCount := len(inputFiles)
filterComplex := strings.Builder{}
for i := 0; i < inputCount; i++ {
filterComplex.WriteString(fmt.Sprintf("[%d:v]", i))
}
filterComplex.WriteString(fmt.Sprintf("concat=n=%d:v=1[v]", inputCount))
ffmpegCmd = append(ffmpegCmd,
"-filter_complex", filterComplex.String(),
"-map", "[v]",
"-preset:v", "fast",
"-an",
"-ss", strconv.FormatInt(offset, 10),
"-t", strconv.FormatInt(length, 10),
"-f", "mp4",
outputFile,
)
return handleFfmpegProcess(ctx, ffmpegCmd)
}
func handleFfmpegProcess(ctx context.Context, ffmpegCmd []string) (bool, error) {
_, span := tracer.Start(ctx, "handleFfmpegProcess")
defer span.End()
span.SetAttributes(attribute.String("ffmpeg.cmd", strings.Join(ffmpegCmd, " ")))
startTime := time.Now()
defer func() {
span.SetAttributes(attribute.Int64("ffmpeg.duration", int64(time.Since(startTime).Seconds())))
}()
log.Printf("FFMPEG执行命令【%s】", strings.Join(ffmpegCmd, " "))
cmd := exec.Command(ffmpegCmd[0], ffmpegCmd[1:]...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
err := cmd.Start()
if err != nil {
span.SetAttributes(attribute.String("ffmpeg.stderr", stderr.String()))
span.SetStatus(codes.Error, "FFMPEG执行命令失败")
log.Printf("FFMPEG执行命令失败错误信息%s命令【%s】", stderr.String(), strings.Join(ffmpegCmd, " "))
return false, err
}
defer cmd.Process.Kill()
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-time.After(1 * time.Minute):
span.SetAttributes(attribute.String("ffmpeg.stderr", stderr.String()))
span.SetStatus(codes.Error, "FFMPEG执行命令没有在1分钟内退出")
log.Printf("FFMPEG执行命令没有在1分钟内退出命令【%s】", strings.Join(ffmpegCmd, " "))
return false, fmt.Errorf("ffmpeg command timed out")
case err := <-done:
if err != nil {
span.SetAttributes(attribute.String("ffmpeg.stderr", stderr.String()))
span.SetStatus(codes.Error, "FFMPEG执行命令失败")
log.Printf("FFMPEG执行命令失败错误信息%s命令【%s】", stderr.String(), strings.Join(ffmpegCmd, " "))
return false, err
}
endTime := time.Now()
log.Printf("FFMPEG执行命令结束耗费时间【%dms】命令【%s】", endTime.Sub(startTime).Milliseconds(), strings.Join(ffmpegCmd, " "))
return true, nil
}
}
func GetVideoDuration(ctx context.Context, filePath string) (float64, error) {
_, span := tracer.Start(ctx, "GetVideoDuration")
defer span.End()
ffprobeCmd := []string{
"ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
filePath,
}
span.SetAttributes(attribute.String("ffprobe.cmd", strings.Join(ffprobeCmd, " ")))
cmd := exec.Command(ffprobeCmd[0], ffprobeCmd[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
span.SetAttributes(attribute.String("error", err.Error()))
span.SetStatus(codes.Error, "failed to get video duration")
return 0, fmt.Errorf("failed to get video duration: %w", err)
}
span.SetAttributes(attribute.String("ffmpeg.stdout", out.String()))
durationStr := strings.TrimSpace(out.String())
duration, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
span.SetAttributes(attribute.String("error", err.Error()))
span.SetStatus(codes.Error, "failed to parse video duration")
return 0, fmt.Errorf("failed to parse video duration: %w", err)
}
span.SetAttributes(attribute.Float64("video.duration", duration))
return duration, nil
}