合并时把弹幕坐标系统一到视频分辨率;修「改设置会删掉正在合并的临时文件」
用户手动合并一场竖屏录播(视频 720x1280,弹幕文件 960x540、字号 30)时发现: 字号改大以后文字变大了,但头像还是小小的,而且越往下的弹幕头像和文字错得越远。 原因:弹幕文件里的 PlayResX/PlayResY 是录制当时的画面尺寸,和现在的视频对不上时, libass 会按「视频高 ÷ PlayResY」缩放文字和它的坐标(这里 1280/540 ≈ 2.37), 而头像叠加用的是视频像素坐标(头像直径 = 字号×4/5、行位置 = 字号×弹道号), 两者不在同一个坐标系里。实测同一帧(720x1280 竖屏): 修复前:文字墨迹高 54px、头像直径 24px,第 4 行文字在 y=226 而它的头像在 y=92 修复后:文字墨迹高 54px、头像直径 56px,头像与对应文字行一一对齐 做法是合并前探测视频分辨率,不一致就把 PlayRes 改成视频宽高:跟随弹幕文件时字号 按高度比换算(占画面的比例不变),用户填了 danmuFontSize 就按成片像素直接用 —— 因为用户看到的本来就是成片上的像素。只在有视频时做,透明头像层模式不变; 探测不到分辨率就保持原样。坐标系一致时(1920x1080 配 1920x1080)完全不改动, 所以正常录播的合并结果和以前一样。验证方式是拿一份「原生就对」的弹幕文件做对照, 自动统一后的成片与它在文字行位置、头像直径和位置上逐项相同。 顺带修一个真 bug(上一轮引入):checkConfig() 里清了合成临时文件,而改设置会重新 读取配置并调用 checkConfig(),于是合并途中改任何设置都会把正在用的分块字幕/临时 成片删掉,合并当场失败(日志:清理上次留下的临时文件 acfunlive_chunk_single.ass → Unable to open ...)。清理挪到只在启动时执行的 cleanLeftoverTempFiles()。 新增 config_test.go(改设置不能删掉在用的临时文件,反向验证过:把清理塞回 checkConfig 会让它失败)和 unifyDanmuSpace / 分辨率解析的用例。
This commit is contained in:
@@ -208,6 +208,12 @@ func burnDanmu(jsonlFile, videoFile, outFile string, offset float64) error {
|
||||
lPrintf("弹幕字号按设置使用 %d(弹幕文件里是 %d)", config.DanmuFontSize, meta.FontSize)
|
||||
meta.FontSize = config.DanmuFontSize
|
||||
}
|
||||
// 弹幕文件的坐标系和视频对不上时,统一到视频坐标系(见 unifyDanmuSpace)
|
||||
if burnMode {
|
||||
if vw, vh := probeVideoSize(ffmpegFile, videoFile); vw > 0 && vh > 0 {
|
||||
unifyDanmuSpace(&meta, vw, vh, config.DanmuFontSize)
|
||||
}
|
||||
}
|
||||
// 弹道按最终字号统一重排,避免还原弹幕或改字号后同弹道追尾
|
||||
relayoutLanes(records, meta)
|
||||
|
||||
@@ -329,6 +335,57 @@ func earliestLane(lanes []laneState) int {
|
||||
return best
|
||||
}
|
||||
|
||||
// 把渲染坐标系统一到视频分辨率。
|
||||
// 弹幕文件里的 PlayResX/PlayResY 是录制当时的画面尺寸,老文件常和现在的视频对不上
|
||||
// (例如 960x540 的弹幕文件配 720x1280 的竖屏视频)。libass 会按「视频高 / PlayResY」
|
||||
// 缩放文字和它的坐标,而头像叠加用的是视频像素坐标,两者不在同一个坐标系里 ——
|
||||
// 字号越大越明显:头像显得小,而且每往下一行错得越多(看起来和文字分开了)。
|
||||
// 这里把 PlayRes 改成视频宽高:跟随文件时字号按高度比换算(保持占画面的比例),
|
||||
// 用户显式填了字号就直接用(他看的是成片上的像素)。返回是否做过调整。
|
||||
func unifyDanmuSpace(meta *danmuMeta, vw, vh, fontSizeOverride int) bool {
|
||||
if vw <= 0 || vh <= 0 || (vw == meta.PlayResX && vh == meta.PlayResY) {
|
||||
return false
|
||||
}
|
||||
oldX, oldY, oldFs := meta.PlayResX, meta.PlayResY, meta.FontSize
|
||||
if fontSizeOverride > 0 {
|
||||
meta.FontSize = fontSizeOverride
|
||||
} else {
|
||||
meta.FontSize = int(math.Round(float64(oldFs) * float64(vh) / float64(oldY)))
|
||||
}
|
||||
if meta.FontSize < 1 {
|
||||
meta.FontSize = 1
|
||||
}
|
||||
meta.PlayResX, meta.PlayResY = vw, vh
|
||||
lPrintf("弹幕文件的坐标系是 %dx%d(字号 %d),视频是 %dx%d,改按视频坐标系渲染,字号 %d",
|
||||
oldX, oldY, oldFs, vw, vh, meta.FontSize)
|
||||
return true
|
||||
}
|
||||
|
||||
// 用 ffmpeg 探测视频分辨率,探测不到返回 0, 0
|
||||
func probeVideoSize(ffmpegFile, path string) (int, int) {
|
||||
cmd := exec.Command(ffmpegFile, "-hide_banner", "-i", path)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
// 没指定输出文件时 ffmpeg 会返回非 0,但流信息已经打印出来了
|
||||
_ = cmd.Run()
|
||||
|
||||
for _, line := range strings.Split(buf.String(), "\n") {
|
||||
// 只看视频流,跳过封面图(mjpeg 之类的 attached pic)那几行
|
||||
if !strings.Contains(line, "Video:") || strings.Contains(line, "attached pic") {
|
||||
continue
|
||||
}
|
||||
if m := videoSizeRe.FindStringSubmatch(line); len(m) == 3 {
|
||||
w, _ := strconv.Atoi(m[1])
|
||||
h, _ := strconv.Atoi(m[2])
|
||||
return w, h
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// 逗号后紧跟的「数字x数字」就是分辨率,前面的编码器名里也有 0x… 这种十六进制,靠逗号区分
|
||||
var videoSizeRe = regexp.MustCompile(`, (\d{2,5})x(\d{2,5})[, \[]`)
|
||||
|
||||
// 用 ffmpeg 探测媒体文件的时长(秒),探测不到返回 0
|
||||
func probeDuration(ffmpegFile, path string) float64 {
|
||||
cmd := exec.Command(ffmpegFile, "-hide_banner", "-i", path)
|
||||
|
||||
Reference in New Issue
Block a user