From 3bd2a09e441f96cda652329532baece65e83214c Mon Sep 17 00:00:00 2001 From: "richard.li" Date: Thu, 21 May 2026 18:21:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(mp4/box):=20CreateContainerBox/WriteTo?= =?UTF-8?q?=20=E4=BF=AE=20typed-nil=20=E5=AD=90=20box=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit box.go 的 CreateContainerBox / WriteTo 用 reflect.ValueOf(child).IsNil() 判空, 对「纯 nil 接口」(zero Value)和值类型会 panic。加 isNil() helper 安全判定: 纯 nil 接口 / 可空 Kind 的 nil 值返回 true, 其余返回 false。 加 box_test.go 单测覆盖。 trailer 零重写 plan 阶段 B 的 B2;fMP4 完整实现(需先补 muxer init moov) 暂停, 本 panic 修复独立可用。 --- plugin/mp4/pkg/box/box.go | 20 ++++++++++++-- plugin/mp4/pkg/box/box_test.go | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 plugin/mp4/pkg/box/box_test.go 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) + } +} From c9a9566285f56dcd7cfa8cd8fcae069bb5d7ba39 Mon Sep 17 00:00:00 2001 From: "richard.li" Date: Fri, 22 May 2026 09:06:17 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(plan):=20=E6=A0=87=E6=B3=A8=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=20B=20=E6=89=A7=E8=A1=8C=E4=BF=AE=E8=AE=A2=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20fMP4=20muxer=20=E7=BC=BA=20init=20moov?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 执行阶段 B 时发现: monibuca fMP4 muxer 不写 init moov box, 产物 [ftyp][moof][mdat]...[mfra] 非合法 fMP4。plan 阶段 B "moov 已在头部" 前提不成立。本次仅完成 B2(box typed-nil panic 修复), B0/B1/B3/B4 (fMP4 完整实现)暂停, 标注为后续专项。 --- docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md | 6 ++++++ 1 file changed, 6 insertions(+) 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` 时: