1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user