This commit is contained in:
2026-07-15 14:41:49 +08:00
parent 98d4c302d8
commit c6c0daaafe
4 changed files with 24 additions and 286 deletions
+2 -2
View File
@@ -6,6 +6,8 @@ require (
github.com/Eyevinn/mp4ff v0.53.0
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
github.com/gogf/gf/v2 v2.10.2
github.com/golang-jwt/jwt/v5 v5.3.1
golang.org/x/crypto v0.54.0
)
require (
@@ -23,7 +25,6 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
@@ -43,7 +44,6 @@ require (
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
-4
View File
@@ -94,12 +94,8 @@ golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
BIN
View File
Binary file not shown.
+22 -280
View File
@@ -26,7 +26,6 @@ import (
"video-factory/shortdrama/model/dto"
"video-factory/shortdrama/model/entity"
"github.com/Eyevinn/mp4ff/mp4"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
@@ -1849,294 +1848,40 @@ func (s *dramaService) waitForSegmentVideo(ctx context.Context, d *entity.Drama,
return fmt.Errorf("串行等待超时: 任务 %d 第%d段视频在 %d 次轮询后仍未就绪", task.Id, task.SegmentIdx+1, maxAttempts)
}
func mdatDataOffset(f *mp4.File) uint64 {
offset := uint64(0)
for _, child := range f.Children {
if child == f.Mdat {
return offset + f.Mdat.HeaderSize()
}
offset += child.Size()
}
return 0
}
func (s *dramaService) concatVideos(inputs []string, output string) error {
if len(inputs) < 2 {
return fmt.Errorf("需要至少2个输入文件才能合并")
}
// 1. 读取所有 MP4 文件到内存
var files []*mp4.File
// 创建 ffmpeg concat demuxer 文件列表
filelist := output + ".filelist.txt"
var lines []string
for _, path := range inputs {
f, err := mp4.ReadMP4File(path)
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("读取 %s 失败: %w", path, err)
absPath = path
}
files = append(files, f)
escaped := strings.ReplaceAll(absPath, "'", "'\\''")
lines = append(lines, "file '"+escaped+"'")
}
// 2. 计算累积 mdat 负载大小和各文件原始基址
cumPayload := make([]uint64, len(files))
totalPayload := uint64(0)
origBase := make([]uint64, len(files))
for i, f := range files {
cumPayload[i] = totalPayload
totalPayload += uint64(len(f.Mdat.Data))
origBase[i] = mdatDataOffset(f)
if err := os.WriteFile(filelist, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
return fmt.Errorf("创建文件列表失败: %w", err)
}
defer os.Remove(filelist)
// 3. 以第一个文件为蓝本构建输出结构
outFile := mp4.NewFile()
outFile.Ftyp = files[0].Ftyp
outFile.Moov = files[0].Moov
outFile.Mdat = &mp4.MdatBox{}
outFile.Mdat.SetData(make([]byte, totalPayload))
pos := 0
for _, f := range files {
copy(outFile.Mdat.Data[pos:], f.Mdat.Data)
pos += len(f.Mdat.Data)
}
// 4. 按轨道类型初始化合并状态
type mergeState struct {
stbl *mp4.StblBox
chunkBase uint32 // 当前已合并的 chunk 数量(stsc 偏移用)
}
states := make(map[string]*mergeState)
for _, trak := range files[0].Moov.Traks {
hdlr := trak.Mdia.Hdlr.HandlerType
if _, ok := states[hdlr]; ok {
continue
}
stbl := trak.Mdia.Minf.Stbl
nChunks := chunkCount(stbl)
states[hdlr] = &mergeState{stbl: stbl, chunkBase: nChunks}
}
// 5. 合并后续文件
for fi := 1; fi < len(files); fi++ {
f := files[fi]
for _, srcTrak := range f.Moov.Traks {
hdlr := srcTrak.Mdia.Hdlr.HandlerType
st, ok := states[hdlr]
if !ok {
continue
}
srcStbl := srcTrak.Mdia.Minf.Stbl
nChunks := chunkCount(srcStbl)
// stco/co64:偏移调整 = 第一个文件的基址差 + 累积 mdat 偏移
adjust := int64(origBase[0]-origBase[fi]) + int64(cumPayload[fi])
mergeChunkOffsets(st.stbl, srcStbl, adjust)
// stsz:合并采样大小表
if err := mergeSampleSizes(st.stbl.Stsz, srcStbl.Stsz); err != nil {
return err
}
// stts:合并时域采样表
mergeTimeToSample(st.stbl.Stts, srcStbl.Stts)
// stsc:合并样块映射表,FirstChunk 需平移
if srcStbl.Stsc != nil {
for i, e := range srcStbl.Stsc.Entries {
sid := srcStbl.Stsc.GetSampleDescriptionID(i + 1)
if err := st.stbl.Stsc.AddEntry(e.FirstChunk+st.chunkBase, e.SamplesPerChunk, sid); err != nil {
return fmt.Errorf("stsc 合并失败: %w", err)
}
}
}
st.chunkBase += nChunks
}
}
// 6. 修正所有 chunk 偏移(合并后 moov 大小变化)
combinedBase := mdatDataOffset(outFile)
finalAdjust := int64(combinedBase - origBase[0])
for _, st := range states {
adjustChunkOffsets(st.stbl, finalAdjust)
}
// 7. 更新 mvhd 总时长
totalDur := uint64(0)
for _, f := range files {
totalDur += f.Moov.Mvhd.Duration
}
outFile.Moov.Mvhd.Duration = totalDur
// 更新每个 tkhd 时长
for _, trak := range outFile.Moov.Traks {
hdlr := trak.Mdia.Hdlr.HandlerType
totalDur = 0
for _, f := range files {
for _, t := range f.Moov.Traks {
if t.Mdia.Hdlr.HandlerType == hdlr {
totalDur += t.Tkhd.Duration
break
}
}
}
trak.Tkhd.Duration = totalDur
}
// 更新每个 elst 段时长(决定播放器实际播放时长)
for _, trak := range outFile.Moov.Traks {
if trak.Edts == nil || len(trak.Edts.Elst) == 0 {
continue
}
hdlr := trak.Mdia.Hdlr.HandlerType
for _, elst := range trak.Edts.Elst {
for i := range elst.Entries {
totalSegDur := uint64(0)
for _, f := range files {
for _, t := range f.Moov.Traks {
if t.Mdia.Hdlr.HandlerType == hdlr && t.Edts != nil {
for _, e := range t.Edts.Elst {
if len(e.Entries) > i {
totalSegDur += e.Entries[i].SegmentDuration
}
}
}
}
}
elst.Entries[i].SegmentDuration = totalSegDur
}
}
}
// 更新每个 mdhd 媒体时长
for _, trak := range outFile.Moov.Traks {
hdlr := trak.Mdia.Hdlr.HandlerType
totalDur = 0
for _, f := range files {
for _, t := range f.Moov.Traks {
if t.Mdia.Hdlr.HandlerType == hdlr {
totalDur += t.Mdia.Mdhd.Duration
break
}
}
}
trak.Mdia.Mdhd.Duration = totalDur
}
// 8. 重建顶级 Children(保留原始 box 顺序,仅替换 mdat 引用)
outFile.Children = make([]mp4.Box, len(files[0].Children))
for i, child := range files[0].Children {
if _, isMdat := child.(*mp4.MdatBox); isMdat {
outFile.Children[i] = outFile.Mdat
} else {
outFile.Children[i] = child
}
}
// 9. 写入输出文件
return mp4.WriteToFile(outFile, output)
}
// chunkCount 返回 stbl 中的 chunk 数量
func chunkCount(stbl *mp4.StblBox) uint32 {
if stbl.Stco != nil {
return uint32(len(stbl.Stco.ChunkOffset))
}
if stbl.Co64 != nil {
return uint32(len(stbl.Co64.ChunkOffset))
}
return 0
}
// mergeChunkOffsets 将 src 的 stco/co64 追加到 base,每个偏移加上 adjust
func mergeChunkOffsets(base, src *mp4.StblBox, adjust int64) {
if src.Stco != nil {
if base.Stco == nil {
base.Stco = &mp4.StcoBox{}
}
for _, off := range src.Stco.ChunkOffset {
base.Stco.ChunkOffset = append(base.Stco.ChunkOffset, uint32(int64(off)+adjust))
}
}
if src.Co64 != nil {
if base.Co64 == nil {
base.Co64 = &mp4.Co64Box{}
}
for _, off := range src.Co64.ChunkOffset {
base.Co64.ChunkOffset = append(base.Co64.ChunkOffset, uint64(int64(off)+adjust))
}
}
}
// adjustChunkOffsets 对 stbl 中所有 stco/co64 偏移统一加上 adjust
func adjustChunkOffsets(stbl *mp4.StblBox, adjust int64) {
if stbl.Stco != nil {
for i := range stbl.Stco.ChunkOffset {
stbl.Stco.ChunkOffset[i] = uint32(int64(stbl.Stco.ChunkOffset[i]) + adjust)
}
}
if stbl.Co64 != nil {
for i := range stbl.Co64.ChunkOffset {
stbl.Co64.ChunkOffset[i] = uint64(int64(stbl.Co64.ChunkOffset[i]) + adjust)
}
}
}
// mergeSampleSizes 合并 stsz 采样大小表
func mergeSampleSizes(base, src *mp4.StszBox) error {
if src == nil {
return nil
}
if base.SampleUniformSize > 0 {
if src.SampleUniformSize == base.SampleUniformSize {
base.SampleNumber += src.SampleNumber
} else {
sizes := make([]uint32, base.SampleNumber)
for i := range sizes {
sizes[i] = base.SampleUniformSize
}
if src.SampleUniformSize > 0 {
for i := uint32(0); i < src.SampleNumber; i++ {
sizes = append(sizes, src.SampleUniformSize)
}
} else {
sizes = append(sizes, src.SampleSize...)
}
base.SampleUniformSize = 0
base.SampleSize = sizes
base.SampleNumber = uint32(len(sizes))
}
} else {
if src.SampleUniformSize > 0 {
for i := uint32(0); i < src.SampleNumber; i++ {
base.SampleSize = append(base.SampleSize, src.SampleUniformSize)
}
} else {
base.SampleSize = append(base.SampleSize, src.SampleSize...)
}
base.SampleNumber = uint32(len(base.SampleSize))
cmd := exec.Command("ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", filelist,
"-c", "copy",
"-y", output,
)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("ffmpeg concat 失败: %w, 输出: %s", err, string(out))
}
return nil
}
// mergeTimeToSample 合并 stts 时域采样表(相邻相同 delta 的条目合并)
func mergeTimeToSample(base, src *mp4.SttsBox) {
if src == nil || len(src.SampleCount) == 0 {
return
}
if len(base.SampleCount) > 0 {
lastDelta := base.SampleTimeDelta[len(base.SampleTimeDelta)-1]
firstDelta := src.SampleTimeDelta[0]
if lastDelta == firstDelta {
base.SampleCount[len(base.SampleCount)-1] += src.SampleCount[0]
base.SampleCount = append(base.SampleCount, src.SampleCount[1:]...)
base.SampleTimeDelta = append(base.SampleTimeDelta, src.SampleTimeDelta[1:]...)
return
}
}
base.SampleCount = append(base.SampleCount, src.SampleCount...)
base.SampleTimeDelta = append(base.SampleTimeDelta, src.SampleTimeDelta...)
}
func (s *dramaService) downloadFile(ctx context.Context, url, dest string) error {
resp, err := http.Get(url)
if err != nil {
@@ -2175,12 +1920,9 @@ func (s *dramaService) downloadFile(ctx context.Context, url, dest string) error
}
func (s *dramaService) probeVideo(path string) error {
f, err := mp4.ReadMP4File(path)
if err != nil {
return fmt.Errorf("视频文件无效: %w", err)
}
if f.Moov == nil {
return fmt.Errorf("视频文件缺少 moov box")
cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("视频文件无效: %w, ffprobe 输出: %s", err, string(out))
}
return nil
}