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 +}