From e44dfa3a4a63db2911eb9f2bff41b50195e8c6bb Mon Sep 17 00:00:00 2001 From: tamakyi Date: Fri, 25 Sep 2026 18:28:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E6=B5=8B=E8=AF=95=EF=BC=9A=E9=87=8D?= =?UTF-8?q?=E6=8E=92=E5=AF=B9=E3=80=8C=E6=9C=AC=E7=A8=8B=E5=BA=8F=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E5=BD=95=E7=9A=84=E5=BC=B9=E5=B9=95=E3=80=8D=E9=9B=B6?= =?UTF-8?q?=E5=BD=B1=E5=93=8D=EF=BC=9B=E5=A4=9A=E5=88=86=E6=AE=B5=E8=B7=A8?= =?UTF-8?q?=E6=AE=B5=E5=86=B2=E7=AA=81=E8=A2=AB=E4=B8=80=E5=B9=B6=E8=A7=A3?= =?UTF-8?q?=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核实「弹道重排会不会影响正常录播的合并」时补的两个用例: - recordjsonl_test.go:用真正的录制写入器 danmuWriter 造 500 条(密集,平均 0.22 秒一条),再按合并流程读回 jsonl 重排 —— 弹道变化 0 条、宽度变化 0 条。录制侧 写文件完全不经过 relayoutLanes,只有合并渲染前会重排,所以磁盘上的 .ass/.nick.ass/.jsonl 一个字节都不变,单段自动合并的成片和以前完全一致。 (顺带记下录制时确实会丢弹幕:500 条里 1 条因为 100 条弹道全满被丢弃, jsonl 里根本没有它,重排也不会把它变出来。) - layout_test.go:多分段(断流重连)拼接时 remapJsonlTimes 只改时间、保留各段 自己算的弹道,两段都从 0 号弹道起排,时间压缩到同一根轴后必然互相压。新增用例 先断言这种拼法本来就撞,再断言重排后冲突为 0 —— 这是重排唯一会改变输出的场景, 方向是变好,不是回归。 --- layout_test.go | 54 ++++++++++++++++++++++++++ recordjsonl_test.go | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 recordjsonl_test.go diff --git a/layout_test.go b/layout_test.go index 8380e3e..91faddc 100644 --- a/layout_test.go +++ b/layout_test.go @@ -87,6 +87,60 @@ func TestRelayoutWidthScalesWithFontSize(t *testing.T) { } } +// 多分段(断流重连)拼接时,remapJsonlTimes 只改时间、保留各段自己算的弹道, +// 两段各自都从 0 号弹道开始排,时间压缩到同一条轴之后必然互相压。重排按拼接后 +// 的时间轴统一分配,这种跨段冲突也会一并消掉。 +func TestRelayoutFixesConcatenatedSegmentConflict(t *testing.T) { + meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 48} + mk := func(base float64) []danmuRecord { + var out []danmuRecord + for i := 0; i < 12; i++ { + out = append(out, danmuRecord{ + Time: base + float64(i)*0.4, + UID: int64(3000 + i), + Name: "观众", + Content: "断流前后各自的弹幕", + }) + } + return out + } + // 两段各自从 0 秒开始排(录制时就是这样),拼接后第二段紧接第一段 + first := mk(0) + relayoutLanes(first, meta) + second := mk(4.8) + relayoutLanes(second, meta) + records := append(append([]danmuRecord{}, first...), second...) + + // 前提:这种拼法本来就撞(第二段的 0 号弹道紧接第一段的 0 号弹道) + if overlaps := countLaneOverlaps(records, meta); overlaps == 0 { + t.Fatal("两段直接拼起来本应有同弹道冲突,用例前提不成立") + } + + relayoutLanes(records, meta) + + if overlaps := countLaneOverlaps(records, meta); overlaps != 0 { + t.Errorf("重排后仍有 %d 条同弹道冲突", overlaps) + } +} + +// 按渲染时的公式统计同弹道追尾条数 +func countLaneOverlaps(records []danmuRecord, meta danmuMeta) int { + show := int64(danmuShowSeconds * 1e9) + resX := int64(meta.PlayResX) + last := map[int]int64{} // 弹道 → 上一条的让位时刻 + over := 0 + for _, r := range records { + total := int64(avatarSize(meta.FontSize)+avatarGap(meta.FontSize)) + int64(r.Width) + rel := int64(r.Time * 1e9) + emerge := rel + total*show/(resX+total) + if prev, ok := last[r.Lane]; ok && rel < prev { + over++ + } + last[r.Lane] = emerge + } + return over +} + // 密集弹幕(弹道信息全是坏的:lane 都是 0、width 都是 0)重排后不应再有追尾, // 也不该超过弹道行数上限 func TestRelayoutNoOverlapOnDenseInput(t *testing.T) { diff --git a/recordjsonl_test.go b/recordjsonl_test.go new file mode 100644 index 0000000..88e0b38 --- /dev/null +++ b/recordjsonl_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/orzogc/acfundanmu" +) + +// 用真正的录制写入器(danmuWriter)产生 jsonl,再按合并的流程重排一遍, +// 验证「本程序自己录下来的弹幕」合出来和以前一模一样(弹道、宽度都不变)。 +// 这也是手动合并与自动合并的分水岭:录制侧写文件完全不经过 relayoutLanes, +// 只有合并渲染前会重排,所以磁盘上的 .ass/.nick.ass/.jsonl 一个字节都不变。 +func TestLiveRecordingPathUnaffected(t *testing.T) { + dir := t.TempDir() + cfg := acfundanmu.SubConfig{Title: "测试直播", PlayResX: 1920, PlayResY: 1080, + FontSize: 48, StartTime: 1700000000000000000} + ass := filepath.Join(dir, "live.ass") + w := newDanmuWriter("liveid", "stream", cfg, ass) + + texts := []string{"来啦", "这首歌好听", "主播加油啊", "打卡打卡", "哈哈哈哈哈哈哈哈哈", + "这条弹幕稍微长一点看看弹道到底怎么排"} + for i := 0; i < 500; i++ { + relMs := int64(i) * 220 // 平均 0.22 秒一条,够密集 + c := &acfundanmu.Comment{Content: texts[i%len(texts)]} + c.SendTime = (cfg.StartTime + relMs*1e6) / 1e6 + c.UserID = int64(2000 + i%9) + c.Nickname = "观众" + string(rune('A'+i%9)) + c.Avatar = "http://example.invalid/a.png" + w.write(c) + } + w.close() + + f, err := os.Open(filepath.Join(dir, "live.jsonl")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 1<<20), 1<<20) + var meta danmuMeta + first := true + var recs []danmuRecord + for sc.Scan() { + if first { + json.Unmarshal(sc.Bytes(), &meta) + first = false + continue + } + var r danmuRecord + if json.Unmarshal(sc.Bytes(), &r) == nil { + recs = append(recs, r) + } + } + type snap struct{ lane, width int } + before := make([]snap, len(recs)) + for i, r := range recs { + before[i] = snap{r.Lane, r.Width} + } + + relayoutLanes(recs, meta) + + laneDiff, widthDiff := 0, 0 + for i, r := range recs { + if r.Lane != before[i].lane { + laneDiff++ + if laneDiff <= 3 { + t.Logf("第 %d 条弹道变了:%d → %d", i, before[i].lane, r.Lane) + } + } + if r.Width != before[i].width { + widthDiff++ + } + } + t.Logf("录制产生 %d 条弹幕(含空内容 %d 条),重排后弹道变化 %d 条、宽度变化 %d 条", + len(recs), countEmpty(recs), laneDiff, widthDiff) + if laneDiff != 0 || widthDiff != 0 { + t.Errorf("正常录制的弹幕重排后不应有任何变化") + } +} + +func countEmpty(recs []danmuRecord) int { + n := 0 + for _, r := range recs { + if r.Content == "" { + n++ + } + } + return n +}