修分块渲染的弹幕轨迹:跨块续画 + 文字头像同源,并补失败推送与输出帧率设置
1. 密集弹幕时头像偏离轨迹、弹幕突然加速、之后一段时间屏幕空白
分块渲染的固有问题,三个毛病叠在一起:
- writeChunkAss 把 \move 的起止时间截断到块边界(End=min(t0+10, chunk.end)),
但 x2 写死成 -Width —— 等于把 10 秒的路挤进可见的那零点几秒里,文字直接飞过去。
而头像 overlay 的 x 一直是「固定 /10 秒 + 真实 t0」,于是同一时刻文字已经走到
左边、头像还在半路,看起来就是「头像掉队(移动变慢)」。
- 块尾还会被单块上限收紧成「下一条弹幕出现的时间」,最极端时窗口只剩 0.01 秒。
- 每块只画「本块新出现」的弹幕,跨块的续画没有:块头屏幕上只剩刚出现的几条,
要 10 秒才重新填满 —— 就是「弹幕突然加速,然后一段时间屏幕上什么都没有」。
改法:文字层改成按绝对时间算轨迹(danmuTextX / danmuAvatarX / avatarXExpr 同源),
任何一块里的文字速度都等于真实速度;分块器改成「续画(起播时间在 start-10 ~ start)
+ 本块新出现的」,块尾取「下一条弹幕的起播时间」,这样每条弹幕只在唯一一块里算
新出现,显示期间跨过的后面几块把它接着画,一帧都不缺。
代价是每块要多画一倍多的弹幕,所以顺手把头像一个「绝对路径 + 每人一行 scale」改成
「预缩放 + 短相对路径」(prepareAvatarInputs / scaleAvatarFile),每条从实测 241
字符降到约 130,单块上限才能从 120 抬到 200(200 条 ≈ 26600,仍低于自检阈值 30000
和 Windows 的 32767)。
2. 合并失败没有任何通知
notify.go 新增 mergeFail 事件与 notifyMergeFailure(),接在 autoMerge() 和手动合并的
失败分支上。正常结束、文件里没有弹幕、以及用户自己点的停止都不算失败,不推 ——
最后一种推「合并失败」会误导。配置项 notifyMergeFail 默认开,且和 notifyEnabled 一样
在 Unmarshal 之前预置 true,免得老设置文件升级后被静默关掉。
3. 低帧率录播里的弹幕一格一格跳
新增设置项 mergeFps,默认 0 = 跟随源片(什么都不做)。只在目标高于源片帧率时才加
fps 滤镜,而且必须加在字幕/头像之前 —— 加在之后只是把已经画好的几帧复制一遍,
看起来还是卡。上限 240。
4. 顺带
- -danmuburn 命令行模式不走 initialize(),exeDir 从来没被赋值:头像缓存会落到
「当前工作目录/avatar_cache」,旧版还会把这个相对路径写进滤镜,而 ffmpeg 的工作
目录是弹幕目录,报「打不开 avatar_cache\xxx.png」整块失败。
- cleanTempFiles 见到目录就 continue,预缩放头像那个临时文件夹永远清不掉。
- command_len_test 不再用会过期的「每条 241 字符」估算,改成真跑一遍最坏情况;
探针的头像路径也改成真实的短相对路径(原来量的是早已不存在的长绝对路径)。
- 新增 TestWebuiSettingFieldsMatchInputs:把页面里 JS 的 settingFields/numberFields
抠出来,跟所有 input/select/textarea 的 id 比对,漏登记的设置项会被抓住。
验证
- gofmt / go vet / go test(174 个)/ go test -race / staticcheck / GOOS=linux go build 全过。
- 反向验证 32 项全过:把每个修复临时退回,对应测试确实失败。
- 真实片段实测(2023AcFun 晚会 120 秒高峰段,689 条弹幕,10 秒窗口内最多同时 140 条):
旧 6 块 / 绘制 689 次 / 54.1% 的弹幕文字被加速(中位 2.12x,最差 500x)/
54.2% 有轨迹缺口(中位缺 5.29 秒)/ 78.3s;
新 5 块 / 绘制 984 次 / 加速 0% / 缺口 0% / 98.8s(+26%)。
对照帧里旧版在块尾前 0.1 秒屏幕已经全空,块头后 0.4 秒仍然全空;新版一直是满屏。
- 输出帧率:15fps 源配 mergeFps=60,成片 60fps,1 秒内 60 帧哈希互不相同(不是复制帧);
默认设置下成片仍是 15fps。
This commit is contained in:
@@ -137,6 +137,7 @@ type configData struct {
|
||||
MergeDelDanmu bool `json:"mergeDelDanmu"` // 合并完成后删除弹幕文件
|
||||
MergeOutputDir string `json:"mergeOutputDir"` // 合并完成后成片移动到该文件夹,留空不移动
|
||||
MergeBitrate int `json:"mergeBitrate"` // 合并成片的目标码率(Kbps),0 表示使用默认质量
|
||||
MergeFps int `json:"mergeFps"` // 合并成片的输出帧率,0 表示跟随源片(只在高于源片时生效,源片帧率不会被降下来)
|
||||
DanmuFontSize int `json:"danmuFontSize"` // 合并时弹幕文字的字号(像素,头像随字号等比缩放),0 表示按画面分辨率自动
|
||||
|
||||
// 新添加主播的默认录制档位(留空/非法按第二高)与自定义时的码率上限
|
||||
@@ -154,7 +155,8 @@ type configData struct {
|
||||
NotifyLiveOn bool `json:"notifyLiveOn"` // 开播时推送
|
||||
NotifyLiveOff bool `json:"notifyLiveOff"` // 下播时推送
|
||||
NotifyRecordStart bool `json:"notifyRecordStart"` // 开始录制直播视频时推送
|
||||
NotifyMergeDone bool `json:"notifyMergeDone"` // 自动合并完成时推送
|
||||
NotifyMergeDone bool `json:"notifyMergeDone"` // 合并完成时推送
|
||||
NotifyMergeFail bool `json:"notifyMergeFail"` // 合并失败时推送(用户手动停止不算失败,不推)
|
||||
|
||||
MeowEnabled bool `json:"meowEnabled"` // MeoW 推送的渠道开关
|
||||
MeowNickname string `json:"meowNickname"` // MeoW 昵称,推送地址里的路径参数
|
||||
@@ -224,6 +226,9 @@ const (
|
||||
// 录播视频默认的封装格式。原版(orzogc/acfunlive)用 config.Output 决定扩展名,默认就是 mp4
|
||||
const defaultRecordFormat = "mp4"
|
||||
|
||||
// 合并输出帧率的上限。再高只会让渲染更慢,对弹幕流畅度没有意义
|
||||
const maxMergeFps = 240
|
||||
|
||||
// 支持的录播封装格式。录制用的是 -c copy,只换容器不转码,所以这些都只是封装差别
|
||||
var recordFormats = map[string]struct{}{
|
||||
"mp4": {},
|
||||
@@ -256,11 +261,12 @@ func recordFileExt() string {
|
||||
|
||||
// 默认设置
|
||||
var config = configData{
|
||||
UIDs: uidList{},
|
||||
WebPort: 51880,
|
||||
RecordSubdir: defaultRecordSubdir,
|
||||
MergeSubdir: defaultMergeSubdir,
|
||||
NotifyEnabled: true,
|
||||
UIDs: uidList{},
|
||||
WebPort: 51880,
|
||||
RecordSubdir: defaultRecordSubdir,
|
||||
MergeSubdir: defaultMergeSubdir,
|
||||
NotifyEnabled: true,
|
||||
NotifyMergeFail: true,
|
||||
}
|
||||
|
||||
// Windows 下不能用作文件(夹)名的保留字
|
||||
@@ -322,10 +328,11 @@ func loadConfig() {
|
||||
checkErr(err)
|
||||
|
||||
if json.Valid(data) {
|
||||
// 老设置文件里没有 notifyEnabled 这个字段,默认按「开启」处理,
|
||||
// 老设置文件里没有 notifyEnabled / notifyMergeFail 这两个字段,默认按「开启」处理,
|
||||
// 否则升级后已经配好的推送会被静默关掉。写在 Unmarshal 前面,
|
||||
// 文件里显式写了 false 时仍会被覆盖成 false
|
||||
config.NotifyEnabled = true
|
||||
config.NotifyMergeFail = true
|
||||
err = json.Unmarshal(data, &config)
|
||||
checkErr(err)
|
||||
} else {
|
||||
@@ -335,11 +342,18 @@ func loadConfig() {
|
||||
|
||||
// 命令行合成模式(-danmuburn)也读取设置文件,让 ffmpeg 路径、成片目录等配置生效
|
||||
func loadConfigForDanmuburn() {
|
||||
if *configDir == "" {
|
||||
// 这条路径不走 initialize(),exeDir 得自己定:头像缓存放哪、ffmpeg 去哪找都看它。
|
||||
// 漏掉的话 cacheDir 会变成「当前工作目录/avatar_cache」,换目录跑就会重新下一遍头像,
|
||||
// 更糟的是旧版把头像绝对路径直接写进滤镜,拿到相对路径时 ffmpeg(工作目录是弹幕目录)
|
||||
// 就找不到文件、整块渲染失败
|
||||
if exeDir == "" {
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
*configDir = filepath.Dir(exePath)
|
||||
exeDir = filepath.Dir(exePath)
|
||||
}
|
||||
}
|
||||
if *configDir == "" {
|
||||
*configDir = exeDir
|
||||
}
|
||||
if *configDir == "" {
|
||||
return
|
||||
}
|
||||
@@ -422,6 +436,10 @@ func checkConfig() {
|
||||
lPrintErrf("%s里的danmuFontSize不能小于 0(0 表示按画面分辨率自动)", configFile)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.MergeFps < 0 || config.MergeFps > maxMergeFps {
|
||||
lPrintErrf("%s里的mergeFps只能在 0(跟随源片)到 %d 之间", configFile, maxMergeFps)
|
||||
os.Exit(1)
|
||||
}
|
||||
if config.DefaultBitrate < 0 {
|
||||
lPrintErrf("%s里的defaultBitrate不能小于 0", configFile)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user