You've already forked VptPassiveAdapter
Initial
This commit is contained in:
297
util/ffmpeg.go
Normal file
297
util/ffmpeg.go
Normal 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
|
||||
}
|
||||
}
|
41
util/file_filter.go
Normal file
41
util/file_filter.go
Normal file
@ -0,0 +1,41 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FilterAndSortFiles(fileList []dto.File, beginDt, endDt time.Time) []dto.File {
|
||||
var filteredFiles []dto.File
|
||||
|
||||
for _, file := range fileList {
|
||||
fileStartTime := file.StartTime
|
||||
nextFileStartTime := file.EndTime
|
||||
|
||||
// 如果当前文件还没有开始
|
||||
if beginDt.After(fileStartTime) {
|
||||
// 没有下一个文件的情况下,就是最后一个文件
|
||||
if nextFileStartTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
// 但是下一个文件已经开始
|
||||
if beginDt.Before(nextFileStartTime) {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
// 已经开始,但是也已经结束了
|
||||
} else if fileStartTime.After(endDt) {
|
||||
continue
|
||||
// 已经开始,但未结束
|
||||
} else {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
// 按照 GetDiffMs 的值降序排序
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].GetDiffMs() > filteredFiles[j].GetDiffMs()
|
||||
})
|
||||
|
||||
return filteredFiles
|
||||
}
|
26
util/file_filter_test.go
Normal file
26
util/file_filter_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// 示例数据
|
||||
fileList := []dto.File{
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 2, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 2, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 3, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 3, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 4, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 4, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 5, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 5, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 6, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 6, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 7, 0, 0, time.UTC)},
|
||||
}
|
||||
beginDt := time.Date(2023, 1, 1, 0, 2, 10, 0, time.UTC)
|
||||
endDt := time.Date(2023, 1, 1, 0, 2, 20, 0, time.UTC)
|
||||
filteredFiles := FilterAndSortFiles(fileList, beginDt, endDt)
|
||||
for _, file := range filteredFiles {
|
||||
println(file.StartTime.String(), file.EndTime.String())
|
||||
}
|
||||
}
|
10
util/file_recognizer.go
Normal file
10
util/file_recognizer.go
Normal file
@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
import "ZhenTuLocalPassiveAdapter/config"
|
||||
|
||||
func IsVideoFile(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
return name[len(name)-len(config.Config.FileName.FileExt):] == config.Config.FileName.FileExt || name[len(name)-len(config.Config.FileName.UnfinishedFileExt):] == config.Config.FileName.UnfinishedFileExt
|
||||
}
|
16
util/parse_date.go
Normal file
16
util/parse_date.go
Normal file
@ -0,0 +1,16 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseDate(filePath string) (time.Time, error) {
|
||||
re := regexp.MustCompile(`(\d{4}).?(\d{2}).?(\d{2})`)
|
||||
matches := re.FindStringSubmatch(filePath)
|
||||
if len(matches) != 4 {
|
||||
return time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
return time.Parse("2006.01.02", fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
|
||||
}
|
39
util/parse_time.go
Normal file
39
util/parse_time.go
Normal file
@ -0,0 +1,39 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseStartStopTime(filePath string, relativeDate time.Time) (time.Time, time.Time, error) {
|
||||
split := strings.Split(filePath, config.Config.FileName.TimeSplit)
|
||||
if len(split) != 2 {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
startTime, err := ParseTime(split[0], relativeDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
stopTime, err := ParseTime(split[1], relativeDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
return startTime, stopTime, nil
|
||||
}
|
||||
|
||||
func ParseTime(s string, relativeDate time.Time) (time.Time, error) {
|
||||
re := regexp.MustCompile(`(\d{2}).?(\d{2}).?(\d{2})`)
|
||||
matches := re.FindStringSubmatch(s)
|
||||
if len(matches) != 4 {
|
||||
return time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
tm, err := time.Parse("15.04.05", fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Date(relativeDate.Year(), relativeDate.Month(), relativeDate.Day(),
|
||||
tm.Hour(), tm.Minute(), tm.Second(), 0, time.Local), nil
|
||||
}
|
Reference in New Issue
Block a user