Files
acfunlive-remake/split_pad_test.go
tamakyi 3eb4aa3102 让 ffmpeg 的报错能进日志,并堵住三处会直接毁掉整块渲染的坑
背景:线上第 153/648 块失败,三个编码器依次报同一个 "exit status 0xffffffea"
(EINVAL)。可用的线索只有这一句 —— 因为 ffmpeg 自己的输出走的是 os.Stderr,
服务模式下没人看,等于丢掉了。所以这轮先解决"看不到原因",再顺手补几处防御。

一、把 ffmpeg 的报错留在日志里
   runFFmpegWith 给 stderr 挂一个只保留最后 40 行的 writer(进度行用 \r 刷新,
   一并按行处理),失败时把这段附在错误上再由上层 lPrintErrf 写进日志。
   顺带过滤掉每次启动都打印的版本/编译配置那几行(configuration 一行就一千多字符,
   留着会把真正的报错挤出去)。实测效果:报错里直接能看到
   "moov atom not found" / "Error opening input: Invalid data found"。

二、跟编码器无关的失败不再挨个回退
   滤镜图建不起来、输入文件打不开这类问题,换个编码器结果一模一样。
   原来会把同一件事重跑三遍(hevc_qsv → h264_qsv → libx264),既浪费时间
   又在日志里刷三行看不出区别的错误。现在命中这些关键词就直接报出完整错误。

三、头像 split 的份数必须和真正用到的份数一致
   统计头像用量时按整块弹幕算,生成 overlay 时才做可见性裁剪:
   同一 uid 在本块有多条、其中一条整条落在块外的话,split 会多出一个没人接收的输出 pad,
   ffmpeg 直接 EINVAL 退出(报错完全看不出跟弹幕有关)。抽出 danmuVisibleInChunk
   让统计、生成 overlay、写块字幕三处用同一个判断。

四、缓存命中要校验文件完整,下载改成原子写
   原来只看"文件在不在",被写一半的 PNG 会被当成可用缓存塞给 ffmpeg 当输入。
   现在要求 PNG 以 IEND 收尾(16 字节以下直接判无效),下载先写 .part 再改名,
   旧位置搬过来的文件同样要过这道校验。

五、头像直径至少为 1
   有人把 danmuFontSize 填成 1、2 时 avatarSize 会算出 0,而 scale=0:0
   会让 ffmpeg 直接 EINVAL、整块渲染失败。

验证:
- gofmt / go vet / go build 通过;go test 全绿(16.8s);go test -race 全绿(22.8s);
  staticcheck -checks=all 零告警
- 新增 diagnostics_test.go(10 个)与 split_pad_test.go(1 个)用例;
  反向验证扩到 21 项全过
- 隔离实例:e2e.py 37 项、smoke_extra.py 33 项、smoke_late_danmu.py(越界弹幕场景)全过;
  另用一个损坏的 mp4 实测,任务失败时日志里能看到 ffmpeg 的原话,
  且没有出现编码器回退
2026-09-26 12:15:14 +08:00

57 lines
1.8 KiB
Go
Raw Permalink 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 (
"image"
"image/png"
"os"
"path/filepath"
"testing"
)
// 一块里同一个人有多条弹幕、但其中一条整条落在块外(会被裁掉)时,
// 头像的 split 份数必须和真正用到的份数一致 —— 否则 split 会多出一个没人接收的输出 pad,
// ffmpeg 直接以 EINVAL(Windows 上就是 exit status 0xffffffea)退出,
// 报错完全看不出跟弹幕有关,还会连累三个编码器挨个"失败"。
func TestRenderChunkMatchesSplitWithVisibleDanmu(t *testing.T) {
ffmpegFile := getFFmpeg()
if ffmpegFile == "" {
t.Skip("找不到 ffmpeg,跳过")
}
withJobFlags(t, true, false, false)
dir := t.TempDir()
// 头像
av := filepath.Join(dir, "av.png")
f, err := os.Create(av)
if err != nil {
t.Fatal(err)
}
if err := png.Encode(f, image.NewRGBA(image.Rect(0, 0, 16, 16))); err != nil {
t.Fatal(err)
}
f.Close()
// 同一个 uid 两条弹幕:一条在块内可见,另一条时间远在块外
uid := int64(7)
chunk := danmuChunk{start: 0, end: 1, records: []danmuRecord{
{Time: 0.2, UID: uid, Name: "甲", Content: "块内可见的", Lane: 0, Width: 100},
{Time: 30.0, UID: uid, Name: "甲", Content: "整条在块外的", Lane: 1, Width: 100},
}}
ctx := &burnContext{
ffmpegFile: ffmpegFile,
outFile: filepath.Join(dir, "out.mp4"),
meta: danmuMeta{PlayResX: 160, PlayResY: 120, FontSize: 20},
avatars: map[int64]string{uid: av},
dir: dir,
encoder: "libx264", // 直接指定软编,别让 QSV 的兼容性干扰这条用例
}
out := filepath.Join(dir, "chunk.mov")
if err := ctx.renderChunk(chunk, "chunk.ass", out, false, nil); err != nil {
t.Fatalf("同一个人有两条弹幕、其中一条在块外时不该渲染失败:%v", err)
}
if !fileExists(out) {
t.Fatalf("应当产出文件:%s", out)
}
}