This commit is contained in:
2025-02-07 22:58:01 +08:00
commit ba4aad0ae5
23 changed files with 1061 additions and 0 deletions

297
util/ffmpeg.go Normal file
View File

@ -0,0 +1,297 @@
package util
import (
"ZhenTuLocalPassiveAdapter/config"
"ZhenTuLocalPassiveAdapter/dto"
"bytes"
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"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 := 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
}
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: os.TempDir() + "/" + task.TaskID + ".mp4",
}
if ffmpegTask.Offset > (config.Config.Record.Duration) {
log.Printf("分析FFMPEG任务失败:ID:【%s】,文件片段:【%s】,无法完整覆盖时间点【%s】", task.TaskID, fileList[len(fileList)-1].Name, endDt)
return nil, fmt.Errorf("无法完整覆盖时间点")
}
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",
"-ss", strconv.FormatInt(offset, 10),
"-i", inputFile,
"-c:v", "copy",
"-an",
"-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
}
}