264 lines
7.6 KiB
Go
264 lines
7.6 KiB
Go
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("切分完成")
|
||
}
|