2025-03-16 18:02:40 +08:00

326 lines
8.1 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"
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"time"
)
const FfmpegExec = "ffmpeg"
func RunFfmpegTask(task *dto.FfmpegTask) bool {
var result bool
if len(task.Files) == 1 {
// 单个文件切割,用简单方法
result = runFfmpegForSingleFile(task)
} else {
// 多个文件切割,用速度快的
result = runFfmpegForMultipleFile1(task)
}
// 先尝试方法1
if result {
return true
}
log.Printf("FFMPEG简易方法失败尝试复杂方法转码")
// 不行再尝试方法二
return runFfmpegForMultipleFile2(task)
}
func runFfmpegForMultipleFile1(task *dto.FfmpegTask) bool {
// 多文件方法一先转换成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(*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 {
return false
}
// 步骤二使用concat协议拼接裁切
result, err := QuickConcatVideoCut(task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
return false
}
// 步骤三:删除临时文件
for _, file := range task.Files {
if err := os.Remove(file.Url); err != nil {
log.Printf("删除临时文件失败: %v", err)
}
}
return result
}
func runFfmpegForMultipleFile2(task *dto.FfmpegTask) bool {
// 多文件,方法二:使用计算资源编码
result, err := SlowVideoCut(task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
return false
}
return result
}
func runFfmpegForSingleFile(task *dto.FfmpegTask) bool {
result, err := QuickVideoCut(task.Files[0].Url, int64(task.Offset), int64(task.Length), task.OutputFile)
if err != nil {
return false
}
_, err = os.Stat(task.OutputFile)
if err != nil {
log.Printf("文件不存在:%s", task.OutputFile)
return false
}
return result
}
func CheckFileCoverageAndConstructTask(fileList []dto.File, beginDt, endDt time.Time, task dto.Task) (*dto.FfmpegTask, error) {
if fileList == nil || len(fileList) == 0 {
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 {
// 片段断开
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) {
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"),
}
return ffmpegTask, nil
}
func convertMp4ToTs(file dto.File, outFileName string) (bool, error) {
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-i", file.Url,
"-c", "copy",
"-bsf:v", "h264_mp4toannexb",
"-f", "mpegts",
outFileName,
}
return handleFfmpegProcess(ffmpegCmd)
}
func convertHevcToTs(file dto.File, outFileName string) (bool, error) {
ffmpegCmd := []string{
FfmpegExec,
"-hide_banner",
"-y",
"-i", file.Url,
"-c", "copy",
"-bsf:v", "hevc_mp4toannexb",
"-f", "mpegts",
outFileName,
}
return handleFfmpegProcess(ffmpegCmd)
}
func QuickVideoCut(inputFile string, offset, length int64, outputFile string) (bool, error) {
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),
"-f", "mp4",
outputFile,
}
return handleFfmpegProcess(ffmpegCmd)
}
func QuickConcatVideoCut(inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
tmpFile := fmt.Sprintf("tmp%.10f.txt", rand.Float64())
tmpFileObj, err := os.Create(tmpFile)
if err != nil {
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 {
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(ffmpegCmd)
}
func SlowVideoCut(inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
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(ffmpegCmd)
}
func handleFfmpegProcess(ffmpegCmd []string) (bool, error) {
startTime := time.Now()
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 {
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):
log.Printf("FFMPEG执行命令没有在1分钟内退出命令【%s】", strings.Join(ffmpegCmd, " "))
return false, fmt.Errorf("ffmpeg command timed out")
case err := <-done:
if err != nil {
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(filePath string) (float64, error) {
ffprobeCmd := []string{
"ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
filePath,
}
cmd := exec.Command(ffprobeCmd[0], ffprobeCmd[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return 0, fmt.Errorf("failed to get video duration: %w", err)
}
durationStr := strings.TrimSpace(out.String())
duration, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse video duration: %w", err)
}
return duration, nil
}