diff --git a/go.mod b/go.mod index 6840aa4..f3c7552 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index bd680ba..e0ec07d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/short_drama.db b/short_drama.db index efa6908..29e0e37 100644 Binary files a/short_drama.db and b/short_drama.db differ diff --git a/shortdrama/service/drama_service.go b/shortdrama/service/drama_service.go index df3fc0f..56ee969 100644 --- a/shortdrama/service/drama_service.go +++ b/shortdrama/service/drama_service.go @@ -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 }