205 lines
7.1 KiB
Go
205 lines
7.1 KiB
Go
package files
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"testing"
|
||
)
|
||
|
||
// box 构造一个 MP4 box:[size(4)][type(4)][payload]。
|
||
func box(typ string, payload []byte) []byte {
|
||
b := make([]byte, 8+len(payload))
|
||
binary.BigEndian.PutUint32(b[0:4], uint32(8+len(payload)))
|
||
copy(b[4:8], typ)
|
||
copy(b[8:], payload)
|
||
return b
|
||
}
|
||
|
||
// stcoBox 构造一个含单个 chunk 偏移的 stco:[ver+flags(4)][count(4)=1][offset(4)]。
|
||
func stcoBox(offset uint32) []byte {
|
||
p := make([]byte, 12)
|
||
binary.BigEndian.PutUint32(p[4:8], 1) // entry_count
|
||
binary.BigEndian.PutUint32(p[8:12], offset)
|
||
return box("stco", p)
|
||
}
|
||
|
||
// buildMoovEndMP4 构造一个 moov 在末尾、stco 指向 mdat 内某偏移的最小合法 MP4。
|
||
// 返回 (mp4, mdatPayloadAbsOffset)。
|
||
func buildMoovEndMP4(mdatPayload []byte) ([]byte, uint32) {
|
||
ftyp := box("ftyp", []byte("isom\x00\x00\x02\x00"))
|
||
mdat := box("mdat", mdatPayload)
|
||
mdatAbs := uint32(len(ftyp) + 8) // mdat payload 紧跟 mdat 头(8 字节)
|
||
// moov→trak→mdia→minf→stbl→stco(offset=mdatAbs)
|
||
stbl := box("stbl", stcoBox(mdatAbs))
|
||
minf := box("minf", stbl)
|
||
mdia := box("mdia", minf)
|
||
trak := box("trak", mdia)
|
||
moov := box("moov", trak)
|
||
out := append([]byte(nil), ftyp...)
|
||
out = append(out, mdat...)
|
||
out = append(out, moov...)
|
||
return out, mdatAbs
|
||
}
|
||
|
||
func TestFaststartMP4MovesMoovAndFixesOffsets(t *testing.T) {
|
||
marker := []byte("THE-REAL-CHUNK-DATA-HERE")
|
||
// mdat payload:前面填充 + marker,stco 指向 marker 的绝对偏移。
|
||
pad := bytes.Repeat([]byte{0xAB}, 40)
|
||
mdatPayload := append(append([]byte(nil), pad...), marker...)
|
||
_, mdatAbs := buildMoovEndMP4(mdatPayload)
|
||
markerAbs := mdatAbs + uint32(len(pad)) // marker 在原文件里的绝对偏移
|
||
|
||
// 原 stco 指向 mdatAbs(mdat payload 头)。这里把 stco 改成指向 marker 以便断言。
|
||
in2, _ := buildMoovEndMP4Marker(mdatPayload, markerAbs)
|
||
// 校验前置:原文件里 markerAbs 处确实是 marker。
|
||
if !bytes.Equal(in2[markerAbs:markerAbs+uint32(len(marker))], marker) {
|
||
t.Fatalf("setup: 原文件 markerAbs 处非 marker")
|
||
}
|
||
|
||
out, changed := faststartMP4(in2)
|
||
if !changed {
|
||
t.Fatalf("changed = false, want true(moov 在末尾应被搬动)")
|
||
}
|
||
if len(out) != len(in2) {
|
||
t.Fatalf("size 不守恒: in=%d out=%d", len(in2), len(out))
|
||
}
|
||
// 输出顺序:ftyp, moov, mdat。
|
||
boxes, ok := parseTopLevelBoxes(out)
|
||
if !ok || len(boxes) != 3 || boxes[0].typ != "ftyp" || boxes[1].typ != "moov" || boxes[2].typ != "mdat" {
|
||
t.Fatalf("输出顶层顺序错: %+v ok=%v", boxes, ok)
|
||
}
|
||
// 取出输出 moov 里的 stco 偏移,应指向输出文件里仍是 marker 的位置。
|
||
newOff := readSingleStcoOffset(t, out[boxes[1].start:boxes[1].end])
|
||
if int(newOff)+len(marker) > len(out) {
|
||
t.Fatalf("新偏移越界: %d", newOff)
|
||
}
|
||
got := out[newOff : int(newOff)+len(marker)]
|
||
if !bytes.Equal(got, marker) {
|
||
t.Fatalf("新 stco 偏移 %d 指向 %q, want %q(偏移修正错误)", newOff, got, marker)
|
||
}
|
||
// 新偏移 = 原偏移 + moovSize。
|
||
moovSize := boxes[1].end - boxes[1].start
|
||
if int(newOff) != int(markerAbs)+moovSize {
|
||
t.Fatalf("新偏移 = %d, want 原 %d + moovSize %d", newOff, markerAbs, moovSize)
|
||
}
|
||
}
|
||
|
||
func TestFaststartMP4NoopWhenAlreadyFaststart(t *testing.T) {
|
||
// moov 在 mdat 前 → 已 faststart,应原样返回。
|
||
ftyp := box("ftyp", []byte("isom\x00\x00\x02\x00"))
|
||
stbl := box("stbl", stcoBox(100))
|
||
moov := box("moov", box("trak", box("mdia", box("minf", stbl))))
|
||
mdat := box("mdat", bytes.Repeat([]byte{1}, 64))
|
||
in := append(append(append([]byte(nil), ftyp...), moov...), mdat...)
|
||
out, changed := faststartMP4(in)
|
||
if changed {
|
||
t.Fatalf("已 faststart 不应改动")
|
||
}
|
||
if !bytes.Equal(out, in) {
|
||
t.Fatalf("应原样返回")
|
||
}
|
||
}
|
||
|
||
func TestInspectMP4LayoutDetectsMoovAtEnd(t *testing.T) {
|
||
in, _ := buildMoovEndMP4(bytes.Repeat([]byte{0xCD}, 100))
|
||
readAt := func(off, n int64) ([]byte, error) {
|
||
if off+n > int64(len(in)) {
|
||
n = int64(len(in)) - off
|
||
}
|
||
return in[off : off+n], nil
|
||
}
|
||
l, ok := inspectMP4Layout(int64(len(in)), readAt)
|
||
if !ok {
|
||
t.Fatalf("inspect 失败")
|
||
}
|
||
if !l.needsFaststart {
|
||
t.Fatalf("moov 在末尾应 needsFaststart=true")
|
||
}
|
||
if !l.moovIsLast {
|
||
t.Fatalf("moov 是最后一个 box 应 moovIsLast=true")
|
||
}
|
||
if l.ftypStart != 0 || l.moovEnd != int64(len(in)) {
|
||
t.Fatalf("range 不对: %+v (len=%d)", l, len(in))
|
||
}
|
||
}
|
||
|
||
func TestInspectMP4LayoutNoFaststartWhenMoovFirst(t *testing.T) {
|
||
ftyp := box("ftyp", []byte("isom\x00\x00\x02\x00"))
|
||
moov := box("moov", box("trak", box("mdia", box("minf", box("stbl", stcoBox(100))))))
|
||
mdat := box("mdat", bytes.Repeat([]byte{1}, 100))
|
||
in := append(append(append([]byte(nil), ftyp...), moov...), mdat...)
|
||
readAt := func(off, n int64) ([]byte, error) {
|
||
if off+n > int64(len(in)) {
|
||
n = int64(len(in)) - off
|
||
}
|
||
return in[off : off+n], nil
|
||
}
|
||
l, ok := inspectMP4Layout(int64(len(in)), readAt)
|
||
if !ok || l.needsFaststart {
|
||
t.Fatalf("moov 在前应 needsFaststart=false, got ok=%v %+v", ok, l)
|
||
}
|
||
}
|
||
|
||
// 流式拼接(ftyp + patched moov + 中段)必须与全量 faststartMP4 输出逐字节一致。
|
||
func TestStreamingAssemblyMatchesFullRewrite(t *testing.T) {
|
||
in, _ := buildMoovEndMP4(bytes.Repeat([]byte{0xEE}, 256))
|
||
readAt := func(off, n int64) ([]byte, error) { return in[off : off+n], nil }
|
||
l, ok := inspectMP4Layout(int64(len(in)), readAt)
|
||
if !ok || !l.needsFaststart || !l.moovIsLast {
|
||
t.Fatalf("setup: %+v ok=%v", l, ok)
|
||
}
|
||
// 流式版本:ftyp + patched moov + in[ftypEnd:moovStart]
|
||
ftyp := append([]byte(nil), in[l.ftypStart:l.ftypEnd]...)
|
||
moov := append([]byte(nil), in[l.moovStart:l.moovEnd]...)
|
||
if !patchChunkOffsets(moov, l.moovEnd-l.moovStart) {
|
||
t.Fatalf("patch 失败")
|
||
}
|
||
var streaming []byte
|
||
streaming = append(streaming, ftyp...)
|
||
streaming = append(streaming, moov...)
|
||
streaming = append(streaming, in[l.ftypEnd:l.moovStart]...)
|
||
|
||
full, changed := faststartMP4(in)
|
||
if !changed {
|
||
t.Fatalf("full 应 changed")
|
||
}
|
||
if !bytes.Equal(streaming, full) {
|
||
t.Fatalf("流式拼接与全量重写不一致: streaming=%d full=%d", len(streaming), len(full))
|
||
}
|
||
}
|
||
|
||
func TestFaststartMP4NoopOnNonMP4(t *testing.T) {
|
||
for _, data := range [][]byte{
|
||
nil,
|
||
[]byte("not an mp4 at all"),
|
||
{0, 0, 0, 4}, // size 太小
|
||
} {
|
||
if out, changed := faststartMP4(data); changed || !bytes.Equal(out, data) {
|
||
t.Fatalf("非 MP4 应原样不动: changed=%v", changed)
|
||
}
|
||
}
|
||
}
|
||
|
||
// buildMoovEndMP4Marker 同 buildMoovEndMP4,但 stco 指向给定绝对偏移。
|
||
func buildMoovEndMP4Marker(mdatPayload []byte, stcoOffset uint32) ([]byte, uint32) {
|
||
ftyp := box("ftyp", []byte("isom\x00\x00\x02\x00"))
|
||
mdat := box("mdat", mdatPayload)
|
||
mdatAbs := uint32(len(ftyp) + 8)
|
||
stbl := box("stbl", stcoBox(stcoOffset))
|
||
moov := box("moov", box("trak", box("mdia", box("minf", stbl))))
|
||
out := append([]byte(nil), ftyp...)
|
||
out = append(out, mdat...)
|
||
out = append(out, moov...)
|
||
return out, mdatAbs
|
||
}
|
||
|
||
func readSingleStcoOffset(t *testing.T, moov []byte) uint32 {
|
||
t.Helper()
|
||
idx := bytes.Index(moov, []byte("stco"))
|
||
if idx < 0 {
|
||
t.Fatalf("moov 里找不到 stco")
|
||
}
|
||
// stco 头后:type(4) 已在 idx;payload 从 idx+4 起 = ver+flags(4)+count(4)+offset(4)
|
||
off := idx + 4 + 4 + 4
|
||
return binary.BigEndian.Uint32(moov[off : off+4])
|
||
}
|