package main import ( "math" "path/filepath" "testing" ) // 输出帧率只在目标的**上面**才生效:源片本来就 60fps 时填 30 只会把画面压得更卡, // 源片帧率探测不到(0)时也不动,免得把高帧率源押到某个值 func TestOutputFpsOnlyRaises(t *testing.T) { for _, c := range []struct { target int source float64 want int }{ {0, 30, 0}, // 没填:跟随源片 {60, 0, 0}, // 源片帧率探测不到:不动 {60, 30, 60}, // 15/24/30fps 录播里弹幕一格一格跳,提上去 {60, 60, 0}, // 已经一样高:不动 {60, 59.94, 60}, // 29.97/59.94 这类小数帧率要当成「低一点点」 {30, 60, 0}, // 降帧没意义 {-1, 30, 0}, // 非法值当成没填 } { if got := outputFps(c.target, c.source); got != c.want { t.Errorf("outputFps(%d, %g) = %d,期望 %d", c.target, c.source, got, c.want) } } } // 透明头像层模式没有源片可跟:设置填了就按填的,没填固定 30 func TestLayerFpsFallback(t *testing.T) { if got := layerFps(0); got != defaultLayerFps { t.Errorf("layerFps(0) = %d,期望 %d", got, defaultLayerFps) } if got := layerFps(60); got != 60 { t.Errorf("layerFps(60) = %d,期望 60", got) } // 手工构造的 burnContext 没填这个字段时也不能把 r=0 交给 ffmpeg // (color 源拿 r=0 会直接 EINVAL,症状是 exit status 0xffffffea) ctx := &burnContext{} if got := ctx.layerRate(); got != defaultLayerFps { t.Errorf("没填 layerFps 的 burnContext 应当回落到 %d,实际 %d", defaultLayerFps, got) } ctx.layerFps = 25 if got := ctx.layerRate(); got != 25 { t.Errorf("填了 layerFps 就按填的来,实际 %d", got) } } // 低帧率源设了输出帧率时,成片帧率真的会提上去: // fps 滤镜必须加在字幕/头像之前,弹幕才会按新帧率重绘; // 加在之后只是把已经画好的几帧复制一遍,看起来还是卡 func TestRenderChunkRaisesFrameRate(t *testing.T) { ffmpegFile := getFFmpeg() if ffmpegFile == "" { t.Skip("找不到 ffmpeg,跳过") } withJobFlags(t, true, false, false) dir := t.TempDir() src := filepath.Join(dir, "src.mp4") if err := runFFmpegWith(ffmpegFile, []string{ "-y", "-f", "lavfi", "-i", "color=c=black:s=160x120:d=1:r=15", "-c:v", "libx264", "-pix_fmt", "yuv420p", src, }, dir, nil); err != nil { t.Skipf("生成 15fps 测试视频失败,跳过:%v", err) } chunk := danmuChunk{start: 0, end: 1, records: []danmuRecord{ {Time: 0.1, UID: 7, Name: "甲", Content: "提帧测试", Lane: 0, Width: 100}, }} ctx := &burnContext{ ffmpegFile: ffmpegFile, videoFile: src, outFile: filepath.Join(dir, "out.mp4"), meta: danmuMeta{PlayResX: 160, PlayResY: 120, FontSize: 20}, avatars: map[int64]string{}, dir: dir, encoder: "libx264", outFps: outputFps(30, 15), } if ctx.outFps != 30 { t.Fatalf("15fps 源设 30 应当产出 outFps=30,实际 %d", ctx.outFps) } assName := chunkTempPrefix + "fps_test.ass" if err := writeChunkAss(filepath.Join(dir, assName), ctx.meta, chunk, 0, avatarSize(ctx.meta.FontSize), avatarGap(ctx.meta.FontSize)); err != nil { t.Fatal(err) } out := filepath.Join(dir, "chunk.mp4") if err := ctx.renderChunk(chunk, assName, out, true, nil); err != nil { t.Fatalf("提帧渲染不该失败:%v", err) } info := probeVideoStream(ffmpegFile, out) if math.Abs(info.fps-30) > 0.5 { t.Fatalf("成片帧率应当是 30,实际 %.2f", info.fps) } }