Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/superpowers/plans/2026-05-21-trailer-zero-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 时:
Expand Down
20 changes: 18 additions & 2 deletions plugin/mp4/pkg/box/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions plugin/mp4/pkg/box/box_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading