owpengram-server/internal/app/files/mp4faststart.go

243 lines
6.7 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 files
import "encoding/binary"
// faststartMP4 把 MP4 的 moov 原子移到 mdat 之前faststart并把 moov 内所有
// stco/co64 chunk 偏移整体加上 moov 的大小(因为 moov 插到 mdat 前会把媒体数据整体后移)。
// 返回 (newData, changed)。非 MP4 / 已 faststart / 任何解析异常时返回 (data, false) 原样,
// 绝不破坏数据——这是上传热路径,宁可不优化也不能把好视频改坏。
//
// 背景TDesktop 的 story 流式播放路径无法处理 moov 在文件末尾的视频av_read_frame
// 报 Invalid data普通 Telegram 客户端上传前会 faststart。telesrv 在落盘前补这一步,
// 让 supports_streaming=true 的承诺对所有客户端成立。不转码、保留原编码(含 HEVC
func faststartMP4(data []byte) ([]byte, bool) {
boxes, ok := parseTopLevelBoxes(data)
if !ok {
return data, false
}
ftypIdx, moovIdx, firstMdatIdx := -1, -1, -1
for i, b := range boxes {
switch b.typ {
case "ftyp":
if ftypIdx < 0 {
ftypIdx = i
}
case "moov":
if moovIdx < 0 {
moovIdx = i
}
case "mdat":
if firstMdatIdx < 0 {
firstMdatIdx = i
}
}
}
// 必须有 ftyp(且在最前)、moov、mdatmoov 已在 mdat 前则已 faststart。
if ftypIdx != 0 || moovIdx < 0 || firstMdatIdx < 0 {
return data, false
}
if moovIdx < firstMdatIdx {
return data, false
}
// 拷出 moov独立底层数组后续就地改偏移不影响原 data
moov := append([]byte(nil), data[boxes[moovIdx].start:boxes[moovIdx].end]...)
moovSize := int64(len(moov))
if !patchChunkOffsets(moov, moovSize) {
return data, false
}
// 重组ftyp + moov + 其余 box除 ftyp/moov按原顺序。
out := make([]byte, 0, len(data))
out = append(out, data[boxes[ftypIdx].start:boxes[ftypIdx].end]...)
out = append(out, moov...)
for i, b := range boxes {
if i == ftypIdx || i == moovIdx {
continue
}
out = append(out, data[b.start:b.end]...)
}
if len(out) != len(data) {
// 长度必须守恒(只搬不改大小);不守恒说明哪里算错,保守放弃。
return data, false
}
return out, true
}
// mp4Layout 是只读 box 头得出的顶层结构信息,用于在不读取整段媒体的前提下判断是否需要
// faststart 以及如何流式重写。
type mp4Layout struct {
needsFaststart bool // moov 在 mdat 之后
moovIsLast bool // moov 是最后一个顶层 box可走流式重写
ftypStart, ftypEnd int64
moovStart, moovEnd int64
}
// inspectMP4Layout 只读顶层 box 头(每个 ≤16 字节,跳过 box 负载)来判定结构,避免为了
// 「检查是否已 faststart」而把整段视频读进内存。readAt(off, n) 读取 [off, off+n) 字节。
// 非 MP4 / 结构异常返回 (·, false)。
func inspectMP4Layout(size int64, readAt func(off, n int64) ([]byte, error)) (mp4Layout, bool) {
l := mp4Layout{moovStart: -1, moovEnd: -1}
mdatStart := int64(-1)
ftypSeen := false
p := int64(0)
for boxes := 0; p+8 <= size; boxes++ {
if boxes > 1024 { // 顶层 box 数量上限,防御异常文件
return mp4Layout{}, false
}
hdr, err := readAt(p, 16)
if err != nil || int64(len(hdr)) < 8 {
return mp4Layout{}, false
}
boxSize := int64(binary.BigEndian.Uint32(hdr[0:4]))
typ := string(hdr[4:8])
switch {
case boxSize == 1:
if len(hdr) < 16 {
return mp4Layout{}, false
}
boxSize = int64(binary.BigEndian.Uint64(hdr[8:16]))
case boxSize == 0:
boxSize = size - p
}
if boxSize < 8 || p+boxSize > size {
return mp4Layout{}, false
}
switch typ {
case "ftyp":
if p != 0 {
return mp4Layout{}, false // ftyp 必须在最前
}
ftypSeen = true
l.ftypStart, l.ftypEnd = p, p+boxSize
case "moov":
if l.moovStart < 0 {
l.moovStart, l.moovEnd = p, p+boxSize
}
case "mdat":
if mdatStart < 0 {
mdatStart = p
}
}
p += boxSize
}
if p != size || !ftypSeen || l.moovStart < 0 || mdatStart < 0 {
return mp4Layout{}, false
}
l.needsFaststart = l.moovStart > mdatStart
l.moovIsLast = l.moovEnd == size
return l, true
}
type boxRef struct {
typ string
start, end int
}
// parseTopLevelBoxes 顺序解析顶层 box要求恰好无缝覆盖整个 data否则视为非法不处理。
func parseTopLevelBoxes(data []byte) ([]boxRef, bool) {
var boxes []boxRef
p := 0
for p+8 <= len(data) {
size := int(binary.BigEndian.Uint32(data[p : p+4]))
typ := string(data[p+4 : p+8])
switch {
case size == 1:
if p+16 > len(data) {
return nil, false
}
size64 := binary.BigEndian.Uint64(data[p+8 : p+16])
size = int(size64)
case size == 0:
size = len(data) - p
}
if size < 8 || p+size > len(data) {
return nil, false
}
boxes = append(boxes, boxRef{typ: typ, start: p, end: p + size})
p += size
}
if p != len(data) {
return nil, false
}
return boxes, true
}
// patchChunkOffsets 递归进入 moov 的容器 box把 stco/co64 的每个偏移 += delta。
func patchChunkOffsets(box []byte, delta int64) bool {
if len(box) < 8 {
return false
}
p := 8 // 跳过自身 box 头moov 用 32 位 size极少 64 位;若 64 位则下面 walk 仍从 8 起会错→由调用方 moov 头守恒保证)
for p+8 <= len(box) {
size := int(binary.BigEndian.Uint32(box[p : p+4]))
typ := string(box[p+4 : p+8])
hdr := 8
switch {
case size == 1:
if p+16 > len(box) {
return false
}
size = int(binary.BigEndian.Uint64(box[p+8 : p+16]))
hdr = 16
case size == 0:
size = len(box) - p
}
if size < hdr || p+size > len(box) {
return false
}
child := box[p : p+size]
switch typ {
case "stco":
if !patchStco(child, delta) {
return false
}
case "co64":
if !patchCo64(child, delta) {
return false
}
case "trak", "mdia", "minf", "stbl", "edts":
if !patchChunkOffsets(child, delta) {
return false
}
}
p += size
}
return true
}
// patchStcostco = [size(4)][type(4)][version+flags(4)][entry_count(4)][offsets 4*count]。
func patchStco(box []byte, delta int64) bool {
if len(box) < 16 {
return false
}
count := binary.BigEndian.Uint32(box[12:16])
off := 16
if int64(off)+int64(count)*4 > int64(len(box)) {
return false
}
for i := uint32(0); i < count; i++ {
v := binary.BigEndian.Uint32(box[off : off+4])
binary.BigEndian.PutUint32(box[off:off+4], uint32(int64(v)+delta))
off += 4
}
return true
}
// patchCo64co64 entries 为 8 字节。
func patchCo64(box []byte, delta int64) bool {
if len(box) < 16 {
return false
}
count := binary.BigEndian.Uint32(box[12:16])
off := 16
if int64(off)+int64(count)*8 > int64(len(box)) {
return false
}
for i := uint32(0); i < count; i++ {
v := binary.BigEndian.Uint64(box[off : off+8])
binary.BigEndian.PutUint64(box[off:off+8], v+uint64(delta))
off += 8
}
return true
}