diff --git a/docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md b/docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md index 56c8c63e..0023a722 100644 --- a/docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md +++ b/docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md @@ -325,6 +325,12 @@ go test ./plugin/mp4/... -count=1 # 阶段 B —— fMP4 旁路(trailer 完全跳过) +> **执行修订(2026-05-21)**:执行阶段 B 时核实发现 —— monibuca 当前 fMP4 muxer **不写 init `moov` box**(`WriteInitSegment` 的 fragment 分支只写 `ftyp`;`WriteMoov`/`MakeMoov` 在 fMP4 路径零调用),fMP4 产物为 `[ftyp][moof][mdat]...[mfra]`,**缺 `moov`,非合法 fMP4**。下方「fMP4 moov 已在头部」的前提不成立。 +> +> 阶段 B 真实工作量比本节设想大:需先新增 **B0 —— 补全 fMP4 muxer 写 init moov**(`muxer.go` fragment 路径的实质功能开发),再做 B1 bypass。 +> +> **本次执行范围(2026-05-21)**:仅完成 **B2(`box.go` typed-nil panic 修复)** —— 该 panic 是独立 bug,已修复(commit `3bd2a09e`,分支 `feature/trailer-fmp4-bypass`)。**B0 / B1 / B3 / B4(fMP4 完整实现 + 验收)暂停** —— 阶段 A 已把 trailer 磁盘 IO 降 99%+,fMP4 边际收益(`O(moov)`→0)较小,补全 muxer + fMP4 兼容性验证成本不低,留作后续专项。 + ## 原理 fMP4(`muxer` 的 `FLAG_FRAGMENT`)文件结构为 `[ftyp][moov(init)][moof][mdat][moof][mdat]...[mfra]`,`moov` 录制一开始即写在头部。`record stop` 时: diff --git a/plugin/mp4/pkg/box/box.go b/plugin/mp4/pkg/box/box.go index 0d1c5b8c..a7ad9c43 100644 --- a/plugin/mp4/pkg/box/box.go +++ b/plugin/mp4/pkg/box/box.go @@ -93,11 +93,27 @@ func CreateMemoryBox(typ BoxType, mem gomem.Memory) *MemoryBox { } } +// isNil 安全判断 IBox 是否为 nil。直接对接口值调 reflect.Value.IsNil() 在 +// 「纯 nil 接口」(zero Value)和「值类型」上会 panic;本函数规避之: +// 纯 nil 接口返回 true,可空 Kind(指针/接口/slice 等)的 nil 值返回 true, +// 其余(如值类型 struct)返回 false。 +func isNil(i IBox) bool { + if i == nil { + return true + } + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return v.IsNil() + } + return false +} + func CreateContainerBox(typ BoxType, children ...IBox) *ContainerBox { size := uint32(BasicBoxLen) realChildren := make([]IBox, 0, len(children)) for _, child := range children { - if reflect.ValueOf(child).IsNil() { + if isNil(child) { continue } size += uint32(child.Size()) @@ -185,7 +201,7 @@ func (b *FullBox) HeaderSize() uint32 { return FullBoxLen } func WriteTo(w io.Writer, box ...IBox) (n int64, err error) { var n1, n2 int64 for _, b := range box { - if reflect.ValueOf(b).IsNil() { + if isNil(b) { continue } n1, err = b.HeaderWriteTo(w) diff --git a/plugin/mp4/pkg/box/box_test.go b/plugin/mp4/pkg/box/box_test.go new file mode 100644 index 00000000..fd4398f2 --- /dev/null +++ b/plugin/mp4/pkg/box/box_test.go @@ -0,0 +1,49 @@ +package box + +import ( + "bytes" + "testing" +) + +// TestIsNil 验证 isNil 对纯 nil 接口、typed-nil 指针、正常 box 的判定。 +func TestIsNil(t *testing.T) { + var pureNil IBox + if !isNil(pureNil) { + t.Error("纯 nil 接口应判为 nil") + } + var typedNil *ContainerBox + if !isNil(typedNil) { + t.Error("typed-nil *ContainerBox 应判为 nil") + } + if isNil(CreateContainerBox(TypeFREE)) { + t.Error("正常 box 不应判为 nil") + } +} + +// TestCreateContainerBox_SkipsNilChildren 验证 CreateContainerBox 跳过 nil 子 box +// 且不 panic(旧实现对纯 nil 接口调 reflect.Value.IsNil() 会 panic)。 +func TestCreateContainerBox_SkipsNilChildren(t *testing.T) { + var pureNil IBox + var typedNil *ContainerBox + real := CreateContainerBox(TypeFREE) + + c := CreateContainerBox(TypeMOOV, pureNil, typedNil, real) + if len(c.Children) != 1 { + t.Fatalf("应只保留 1 个有效子 box,实际 %d", len(c.Children)) + } +} + +// TestWriteTo_SkipsNil 验证 WriteTo 跳过 nil 子 box 不 panic。 +func TestWriteTo_SkipsNil(t *testing.T) { + var pureNil IBox + var typedNil *ContainerBox + var buf bytes.Buffer + + n, err := WriteTo(&buf, pureNil, typedNil) + if err != nil { + t.Fatalf("WriteTo: %v", err) + } + if n != 0 { + t.Fatalf("全 nil 子 box 应写 0 字节,实际 %d", n) + } +}