补测试:重排对「本程序自己录的弹幕」零影响;多分段跨段冲突被一并解决

核实「弹道重排会不会影响正常录播的合并」时补的两个用例:

- 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 —— 这是重排唯一会改变输出的场景,
  方向是变好,不是回归。
This commit is contained in:
tamakyi
2026-09-25 18:28:52 +08:00
parent 9c94a72c4d
commit e44dfa3a4a
2 changed files with 147 additions and 0 deletions
+54
View File
@@ -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) {
+93
View File
@@ -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
}