Files
video-factory/tools/split_merged/split_merged.go
T
2026-07-15 14:34:57 +08:00

264 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/Eyevinn/mp4ff/mp4"
)
func main() {
if len(os.Args) < 4 {
fmt.Println("用法: split_merged <输入文件> <输出目录> <段数>")
os.Exit(1)
}
inputPath := os.Args[1]
outDir := os.Args[2]
numSegments, err := strconv.Atoi(os.Args[3])
if err != nil || numSegments <= 0 {
fmt.Printf("无效的段数: %s\n", os.Args[3])
os.Exit(1)
}
// 读取合并文件
f, err := mp4.ReadMP4File(inputPath)
if err != nil {
fmt.Printf("读取文件失败: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(outDir, 0755); err != nil {
fmt.Printf("创建输出目录失败: %v\n", err)
os.Exit(1)
}
// 收集每轨道按段划分的 sample 范围
type trakInfo struct {
trak *mp4.TrakBox
samples uint32 // 总 sample 数
perSeg uint32 // 每段 sample 数
}
var traks []trakInfo
for _, trak := range f.Moov.Traks {
stbl := trak.Mdia.Minf.Stbl
if stbl.Stsz != nil {
totalSamples := stbl.Stsz.SampleNumber
perSeg := totalSamples / uint32(numSegments)
traks = append(traks, trakInfo{
trak: trak,
samples: totalSamples,
perSeg: perSeg,
})
fmt.Printf("轨道 %s: 总 %d samples, 每段 %d\n", trak.Mdia.Hdlr.HandlerType, totalSamples, perSeg)
}
}
if len(traks) == 0 {
fmt.Println("未找到可分割的轨道")
os.Exit(1)
}
// 逐段切分
for seg := 0; seg < numSegments; seg++ {
outPath := filepath.Join(outDir, fmt.Sprintf("seg_%d.mp4", seg))
fmt.Printf("切分第 %d 段 -> %s\n", seg, outPath)
outFile := mp4.NewFile()
outFile.Ftyp = f.Ftyp
outFile.Moov = f.Moov // 稍后会修改
// 为每轨道创建仅包含本段 samples 的 stbl
for _, ti := range traks {
stbl := ti.trak.Mdia.Minf.Stbl
startSample := seg * int(ti.perSeg)
endSample := startSample + int(ti.perSeg)
if seg == numSegments-1 {
endSample = int(ti.samples) // 最后一段拿剩余所有 samples
}
fmt.Printf(" 轨道 %s: samples [%d, %d)\n", ti.trak.Mdia.Hdlr.HandlerType, startSample, endSample)
// 拷贝 stsz entry(只保留本段范围)
if stbl.Stsz != nil {
newStsz := &mp4.StszBox{}
if stbl.Stsz.SampleUniformSize > 0 {
newStsz.SampleUniformSize = stbl.Stsz.SampleUniformSize
newStsz.SampleNumber = uint32(endSample - startSample)
} else {
newStsz.SampleSize = make([]uint32, endSample-startSample)
copy(newStsz.SampleSize, stbl.Stsz.SampleSize[startSample:endSample])
newStsz.SampleNumber = uint32(len(newStsz.SampleSize))
}
stbl.Stsz = newStsz
}
// 计算本段 chunk 范围:通过 stsc 找到对应的 chunk
// 直接根据 perSeg chunks 计算。每段 chunks = totalChunks / numSegments
chunkCount := uint32(0)
if stbl.Stco != nil {
chunkCount = uint32(len(stbl.Stco.ChunkOffset))
} else if stbl.Co64 != nil {
chunkCount = uint32(len(stbl.Co64.ChunkOffset))
}
perSegChunks := chunkCount / uint32(numSegments)
startChunk := seg * int(perSegChunks)
endChunk := startChunk + int(perSegChunks)
if seg == numSegments-1 {
endChunk = int(chunkCount)
}
// 重新构建 stco(只保留本段 chunk)
if stbl.Stco != nil {
oldChunks := stbl.Stco.ChunkOffset
stbl.Stco.ChunkOffset = oldChunks[startChunk:endChunk]
// 调整偏移量:减去 mdat 中本段数据的起始位置
baseOffset := oldChunks[startChunk]
for i := range stbl.Stco.ChunkOffset {
stbl.Stco.ChunkOffset[i] -= baseOffset
}
}
if stbl.Co64 != nil {
oldChunks := stbl.Co64.ChunkOffset
stbl.Co64.ChunkOffset = oldChunks[startChunk:endChunk]
baseOffset := oldChunks[startChunk]
for i := range stbl.Co64.ChunkOffset {
stbl.Co64.ChunkOffset[i] -= baseOffset
}
}
// 重新构建 stsc(只保留本段 chunk 的 entries
if stbl.Stsc != nil {
var newEntries []mp4.StscEntry
for _, e := range stbl.Stsc.Entries {
if e.FirstChunk-1 >= uint32(startChunk) && e.FirstChunk-1 < uint32(endChunk) {
newEntries = append(newEntries, mp4.StscEntry{
FirstChunk: e.FirstChunk - uint32(startChunk),
SamplesPerChunk: e.SamplesPerChunk,
SampleDescriptionIndex: e.SampleDescriptionIndex,
})
}
}
stbl.Stsc.Entries = newEntries
}
// 重设 stts(重建,只保留本段 samples 对应的时间)
if stbl.Stts != nil {
newStts := &mp4.SttsBox{}
currentSample := uint32(0)
for i := range stbl.Stts.SampleCount {
count := stbl.Stts.SampleCount[i]
delta := stbl.Stts.SampleTimeDelta[i]
nextSample := currentSample + count
if nextSample <= uint32(startSample) {
currentSample = nextSample
continue
}
if currentSample >= uint32(endSample) {
break
}
overlapStart := uint32(0)
if currentSample < uint32(startSample) {
overlapStart = uint32(startSample) - currentSample
}
overlapEnd := count
if nextSample > uint32(endSample) {
overlapEnd = uint32(endSample) - currentSample
}
if overlapStart < overlapEnd {
newStts.SampleCount = append(newStts.SampleCount, overlapEnd-overlapStart)
newStts.SampleTimeDelta = append(newStts.SampleTimeDelta, delta)
}
currentSample = nextSample
}
stbl.Stts = newStts
}
// 修正 stco 偏移:加上 ftyp+moov+mdat_header 偏移
mdatDataStart := uint32(0) // 稍后在写入时修正
_ = mdatDataStart
}
// 提取本段 mdat 数据
mdatStart := uint32(0)
if len(traks) > 0 && traks[0].trak.Mdia.Minf.Stbl.Stco != nil {
// 使用第一个轨道的第一个 chunk 偏移量作为数据起始位置
mdatStart = traks[0].trak.Mdia.Minf.Stbl.Stco.ChunkOffset[0]
}
// 重构 mdat:从合并文件提取本段数据
outFile.Mdat = &mp4.MdatBox{}
mdatPayloadLen := uint64(0)
// 计算本段 mdat 数据长度
for _, ti := range traks {
stbl := ti.trak.Mdia.Minf.Stbl
if stbl.Stsz != nil {
if stbl.Stsz.SampleUniformSize > 0 {
mdatPayloadLen += uint64(stbl.Stsz.SampleUniformSize) * uint64(stbl.Stsz.SampleNumber)
} else {
for _, s := range stbl.Stsz.SampleSize {
mdatPayloadLen += uint64(s)
}
}
}
}
if mdatPayloadLen > 0 {
segData := make([]byte, mdatPayloadLen)
// 从原始 mdat 拷贝
offset := uint64(mdatStart)
_ = offset
copyPos := uint64(0)
for _, ti := range traks {
stbl := ti.trak.Mdia.Minf.Stbl
if stbl.Stsz != nil {
if stbl.Stsz.SampleUniformSize > 0 {
dataLen := uint64(stbl.Stsz.SampleUniformSize) * uint64(stbl.Stsz.SampleNumber)
copy(segData[copyPos:], f.Mdat.Data[offset:offset+dataLen])
copyPos += dataLen
offset += dataLen
} else {
for _, s := range stbl.Stsz.SampleSize {
copy(segData[copyPos:], f.Mdat.Data[offset:offset+uint64(s)])
copyPos += uint64(s)
offset += uint64(s)
}
}
}
}
outFile.Mdat.SetData(segData)
}
// 更新 mdhd/tkhd/mvhd 时长
outFile.Moov.Mvhd = f.Moov.Mvhd // 拷贝原始 mvhd
// 简化处理:直接使用原始 mvhd 时长
// 重建 Children
outFile.Children = []mp4.Box{outFile.Ftyp, outFile.Moov, outFile.Mdat}
// 修正 stco 偏移:调整到实际文件位置
combinedBase := outFile.Ftyp.Size() + outFile.Moov.Size() + outFile.Mdat.HeaderSize()
for _, ti := range traks {
stbl := ti.trak.Mdia.Minf.Stbl
if stbl.Stco != nil {
for i := range stbl.Stco.ChunkOffset {
stbl.Stco.ChunkOffset[i] += uint32(combinedBase)
}
}
}
if err := mp4.WriteToFile(outFile, outPath); err != nil {
fmt.Printf("写入 %s 失败: %v\n", outPath, err)
continue
}
fmt.Printf(" 完成: %s\n", outPath)
}
fmt.Println("切分完成")
}