Files
acfunlive-remake/layout_test.go
T
tamakyi e44dfa3a4a 补测试:重排对「本程序自己录的弹幕」零影响;多分段跨段冲突被一并解决
核实「弹道重排会不会影响正常录播的合并」时补的两个用例:

- 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 —— 这是重排唯一会改变输出的场景,
  方向是变好,不是回归。
2026-09-25 18:28:52 +08:00

185 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import "testing"
// 还原自 .ass 的弹幕:原格式的弹道是按「只有内容」的宽度占位的,渲染用的是
// 「昵称(uid):内容 + 头像」的宽度,占位更长 → 同弹道会追尾。重排后必须分开。
func TestRelayoutSeparatesRestoredOverlap(t *testing.T) {
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
content := "哈哈哈哈哈哈哈哈"
// 两条原本同弹道、间隔 2.5 秒:按内容宽度(8×60=480)算,2.5 秒时前一条已经让位;
// 按昵称头像宽度(头像48+间隔10+文本720)算,前一条要到约 2.9 秒才让位
records := []danmuRecord{
{Time: 0, UID: 101, Name: "甲", Content: content, Lane: 0, Width: 480},
{Time: 2.5, UID: 202, Name: "乙", Content: content, Lane: 0, Width: 480},
}
relayoutLanes(records, meta)
if records[0].Lane == records[1].Lane {
t.Fatalf("两条追尾的弹幕重排后仍在同一条弹道 %d", records[0].Lane)
}
// 宽度也要按昵称头像版重算,否则滚动速度还是错的
want := textWidth(speakerText("甲", 101, content), meta.FontSize)
if records[0].Width != want {
t.Errorf("宽度应重算为 %d,实际 %d", want, records[0].Width)
}
}
// 录制时存下来的弹道本来就是按同一套公式算的,重排应当原样保留
func TestRelayoutKeepsLiveLayout(t *testing.T) {
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 48}
var records []danmuRecord
for i := 0; i < 40; i++ {
records = append(records, danmuRecord{
Time: float64(i) * 0.4, UID: int64(100 + i), Name: "观众", Content: "测试弹幕内容",
})
}
relayoutLanes(records, meta)
first := make([]int, len(records))
for i, r := range records {
first[i] = r.Lane
}
relayoutLanes(records, meta)
for i, r := range records {
if r.Lane != first[i] {
t.Fatalf("第 %d 条重排两次结果不一致:%d → %d", i, first[i], r.Lane)
}
}
}
// 输入顺序乱了也要按时间重排,否则弹道占用顺序错乱
func TestRelayoutSortsByTime(t *testing.T) {
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 48}
shuffled := []danmuRecord{
{Time: 5, UID: 1, Name: "甲", Content: "第三条"},
{Time: 1, UID: 2, Name: "乙", Content: "第一条"},
{Time: 3, UID: 3, Name: "丙", Content: "第二条"},
}
sorted := []danmuRecord{
{Time: 1, UID: 2, Name: "乙", Content: "第一条"},
{Time: 3, UID: 3, Name: "丙", Content: "第二条"},
{Time: 5, UID: 1, Name: "甲", Content: "第三条"},
}
relayoutLanes(shuffled, meta)
relayoutLanes(sorted, meta)
for i := range sorted {
if shuffled[i].Lane != sorted[i].Lane {
t.Errorf("第 %d 条弹道不一致:%d vs %d", i, shuffled[i].Lane, sorted[i].Lane)
}
}
}
// 字号越小,同一条弹幕在屏幕上停留时占用的时间越短,弹道占用也更宽松
func TestRelayoutWidthScalesWithFontSize(t *testing.T) {
big := []danmuRecord{{Time: 0, UID: 7, Name: "甲", Content: "短弹幕"}}
small := []danmuRecord{{Time: 0, UID: 7, Name: "甲", Content: "短弹幕"}}
relayoutLanes(big, danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60})
relayoutLanes(small, danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 30})
if big[0].Width <= small[0].Width {
t.Errorf("字号 60 的宽度 %d 应大于字号 30 的 %d", big[0].Width, small[0].Width)
}
if big[0].Width != small[0].Width*2 {
t.Errorf("宽度应随字号等比缩放:60 → %d,30 → %d", big[0].Width, small[0].Width)
}
}
// 多分段(断流重连)拼接时,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) {
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
texts := []string{"短", "中等长度的一条弹幕", "这是一条很长的弹幕内容用来占位置", "colong"}
var records []danmuRecord
t0 := 0.0
for i := 0; i < 600; i++ {
records = append(records, danmuRecord{
Time: t0,
UID: int64(1000 + i%7),
Name: "观众" + string(rune('A'+i%26)),
Content: texts[i%len(texts)],
Lane: 0,
Width: 0,
})
t0 += 0.05 + float64(i%5)*0.07 // 0.05 ~ 0.33 秒一条
}
relayoutLanes(records, meta)
show := int64(danmuShowSeconds * 1e9)
resX := int64(meta.PlayResX)
type state struct{ emerge int64 }
last := map[int]state{}
for i, r := range records {
if r.Lane < 0 || r.Lane >= laneNum {
t.Fatalf("第 %d 条弹道越界:%d", i, r.Lane)
}
total := int64(avatarSize(meta.FontSize)+avatarGap(meta.FontSize)) + int64(r.Width)
if total <= 0 {
t.Fatalf("第 %d 条宽度没算出来:%d", i, r.Width)
}
rel := int64(r.Time * 1e9)
emerge := rel + total*show/(resX+total)
if prev, ok := last[r.Lane]; ok && rel < prev.emerge {
t.Fatalf("第 %d 条和上一条挤在同弹道 %d:出现 %v 早于上一条让位 %v", i, r.Lane, r.Time, float64(prev.emerge)/1e9)
}
last[r.Lane] = state{emerge}
}
}