tamakyi
|
6f3c8096b4
|
修分块渲染的弹幕轨迹:跨块续画 + 文字头像同源,并补失败推送与输出帧率设置
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。
|
2026-09-26 16:34:47 +08:00 |
|
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 |
|
tamakyi
|
a2b7185a7b
|
头像链接落盘 + 并发获取 + 单块上限按命令行实测值收紧
一、头像链接的反查结果落盘(avatar_cache/uid_urls.jsonl,7 天有效期)
老录播的 jsonl 里没有头像链接,合并时要按 uid 向 AcFun 反查;但结果原来只存在内存里,
进程一重启就得把整场 uid 重新查一遍。实测一个 uid 约 1 秒,几百人的场次要几分钟,
日志还跟着打「正在获取头像:已处理 x/y 个用户」,看起来就像在重新下载头像
(实测图片缓存本身是好的:同进程第二轮、重启进程后第三轮都是「新增 0、重写 0」)。
现在查到就落盘(追加写,加载时顺手压掉重复/过期行),重启后直接读记录不再查。
有效期 7 天是为了主播换头像后能自己更新过来。
二、头像获取改成 6 路并发
查链接和下图都要走网络,串行做几百人要好几分钟。每个任务开头仍过闸门,
暂停/停止的响应不受影响(任务走带缓冲的 channel,worker 提前退出也不会卡住投递端)。
三、日志分清楚「复用」和「真下载」
原来的 looked++ 只看「这一条没有链接」,内存记忆命中时也会打印「已按 uid 向 AcFun 反查」,
误导成在重下。现在汇总成一句:链接复用 X / 向 AcFun 查 Y,图片复用缓存 Z / 新下载 W。
四、单块条数上限从 200 收紧到 120(线上事故)
线上一次 4 万条弹幕的合并在第 73 块失败:
"fork/exec ...: The filename or extension is too long"(就是 200 条上限那一版)。
真机实测:一块里每条弹幕来自不同用户时,每条占「-i 头像路径 + scale 行 + overlay 行」
约 241 字符 —— 120 条 28864 字符、200 条 48288 字符,而 Windows 的 CreateProcess 限 32767。
现在上限 120(给中文路径和其它参数留 3KB 余量),并加了一道自检:真超了就报
「ffmpeg 命令过长(N 字符):这一块有 X 条弹幕、Y 个头像」,而不是烂在 exec 里。
顺带试过 -filter_complex_script(滤镜写文件、命令行只留文件名),
实测这个 ffmpeg 构建没有该选项(Unrecognized option),所以走收紧条数这条路。
验证:
- gofmt / go vet / go build / GOOS=linux go build 通过;go test 全绿(11.0s);
go test -race 全绿(15.7s);staticcheck -checks=all 零告警
- 新增 avatar_cache_test.go(11 个)与 command_len_test.go(3 个)用例;反向验证扩到 17 项全过
- 隔离实例:e2e.py 36 项、smoke_extra.py 33 项全过
- 新增 smoke_manyuid.py(300 条密集弹幕 / 300 个不同用户 / 全带头像,就是线上事故的复现场景):
合并成功、分 3 块(120/120/60 条)、日志无「命令过长」;
第二次合并同一场「图片复用缓存 300 / 新下载 0」
|
2026-09-26 11:08:26 +08:00 |
|
tamakyi
|
a24425e16d
|
修第三批 P2:除零、停止/暂停的响应与状态一致性、临时文件、带 BOM 的 ASS
一、弹幕滚动时间不再整数除零
write() 里拿 (PlayResX+length) 做除数,PlayResX 被配成 0 又收到一条空内容弹幕时
分母正好是 0,直接 panic 终止整场录制。抽成 scrollMarks 并给分母为 0 的兜底
(退回半个停留时长,至少让它正常显示出来)。
二、点停止不再要等整个块渲完
jobAttach 原来只看 paused:停止请求若落在 jobGate 之后、ffmpeg 启动过程中,
job.proc 还没登记,jobStop 拿不到进程可杀,用户得等整个块渲完(大块要几分钟)。
现在 attach 时发现任务已请求停止就地把它杀掉,上层拿到 errJobStopped,
卡片显示的是「已停止」而不是「失败」。
三、暂停没挂住就回滚状态
jobPause 先把 job.paused 置 true 再调 suspendProcess,挂起失败时状态已经改了:
界面显示「已暂停」而 ffmpeg 还在跑。失败时回滚(只在 proc 仍是同一个进程时回滚)。
jobResume 同理:恢复不成放回「已暂停」,免得显示在跑却停着。
四、.ass 还原的临时 jsonl 用完就删
还原结果写不进录播目录时会退到系统临时目录的固定名文件,原来从不清理,
每失败一次留一份。新增 danmuSource.tempJsonl 标记 + dropTempDanmu 收尾删除;
只删标记过的临时文件 —— 正常情况下那个路径是用户录播目录里的同名 jsonl,
删了就是丢数据。
五、带 BOM 的 ASS 能整份解析
带 UTF-8 BOM 时首行是 BOM+[Script Info],一个节都进不去,最后只报
「可能不是弹幕文件」,跟真实原因完全对不上。解析时剥掉 BOM。
六、头像下载期间暂停/停止不用再干等
单次超时 30 秒 × 3 次重试,最坏 96 秒才响应暂停/停止。单次超时收到 10 秒
(头像只有几百 KB),并且每次重试前过闸门:暂停在这里等、停止直接放弃,
不再把剩下的重试跑完;停止也不再打「头像不可用」的告警。
验证:
- gofmt -l 空;go vet ./... 空;go build 与 GOOS=linux go build 通过
- go test ./... 全绿(10.9s);go test -race ./... 全绿(14.5s)
- 新增 p2_fixes_test.go 8 个用例;本轮 6 项加上一批 6 项共 12 项反向验证全部通过
(每项临时退回源码 → 对应测试确实失败 → 恢复)
- 51897 隔离实例上完整复测:e2e.py 37 项全过、smoke_extra.py 33 项全过
|
2026-09-26 10:07:30 +08:00 |
|
tamakyi
|
9684346067
|
清掉 5 个确认没人调用的遗留函数
staticcheck 的 U1000 一直报这 5 个,逐个核对过引用(含同名方法、测试、字符串调用、反射):
- runFFmpeg:runFFmpegWith 的薄包装,没有任何调用点
- isLiveOn(uid int):包级函数没人用,实际在跑的始终是 streamer.isLiveOn() 方法
- quitRecord:需要收尾的地方都直接 updateLiveInfo(..., isRecording = false)
- setLiveInfo:整体覆盖 liveInfo,早已被 updateLiveInfo(原地改部分字段)取代
- isRecording(liveID):字段 info.isRecording 到处都在直接用
上一轮那个 jobActive 虽然也是 U1000,但它是「本该接上却漏接」的护栏(接回后
P0 的并发问题才解决),所以这次删之前特意确认这 5 个不是同类情况:它们都有
等价且更合适的现役替代实现。
验证:gofmt -l 空;go vet ./... 空;go build ./... 通过;go test ./... 全绿(11.3s);
staticcheck -checks=all 的 U1000 归零。
|
2026-09-26 09:37:11 +08:00 |
|
tamakyi
|
8e82aa54c6
|
修第二批 P1:合并收尾、分块上限、确定性失败与 ffmpeg 路径归一
一、合并收尾只删真正进了成片的分段
prepareForMerge 中途有 4 处会回退成「只合并第一个分段」,而收尾仍按原分段列表
全删源视频 —— 后半场没进成片却照样被删掉,等于永久丢数据。现在 prepareForMerge
把「实际参与合并的源文件」一并返回(新增 used),收尾只删这些。
二、拼接中断不再在录播目录留几 GB 中间文件
concat 失败分支原来只有「被停止」那条路径删了 acfunlive_chunk_joined.*,
普通失败会把它留在录播目录,而且带着 chunkTempPrefix,界面文件列表里根本看不见。
三、把「被停止」和「拼接失败」分开
停止任务会杀掉 ffmpeg 让它非 0 退出,原来一律报成「拼接失败」,
卡片上显示的是失败而不是已停止;prepareForMerge 那里也统一改用 errors.Is 判断。
四、单块弹幕条数加硬上限(maxDanmuPerChunkHard = 200)
块尾会把「显示时间跨过块尾」的弹幕一并并入,弹幕密集时单块能膨胀到几百条。
一条弹幕在 -filter_complex 里约占 110 字节,几百条足以顶破 Windows 的
32767 字符命令行上限,表现同样是「合并失败且看不出原因」。到上限就把块尾
收到下一条弹幕出现之前,保证被挤到下一块的弹幕不会因为「整条早于块头」被丢弃。
五、确定性失败不再白重试三次
头像超出体积/尺寸上限这类失败重试多少次结果都一样,原来会白白把十几 MB 重新下三遍。
新增 noRetryError(runThrice 用 errors.As 识别)只标记这两个上限,网络类错误照旧重试。
六、ffmpeg 路径统一归一成绝对路径
PATH 里带「.」这类相对条目、或程序目录探测失败时,getFFmpeg 会返回相对路径。
合并时 ffmpeg 工作目录被改成输出目录,相对路径到那边就找不着了;而且 Windows 上
exec 会直接拒绝执行「相对当前目录」找到的可执行文件,报错还跟路径完全看不出关系。
验证:
- gofmt -l 空;go vet ./... 空;go build ./... 与 GOOS=linux go build 通过
- go test ./... 全绿(10.8s);go test -race ./... 全绿(14.7s)
- 新增 chunks_test.go 10 个用例;6 项修复逐条反向验证(临时退回修复 → 对应测试确实失败)
- 51897 隔离实例上新产物实测:e2e.py 24 项全过;smoke_extra.py 34 项全过
(含并发合并返回 409 并报出在跑的任务名、口令失败后正确口令仍立刻可用、
超大头像被拒不误伤正常头像且只尝试 1 次、停止后无临时文件残留)
|
2026-09-26 09:35:51 +08:00 |
|
tamakyi
|
4e8471deea
|
头像下载加上体积与像素上限,并不再每次新建连接
审查发现:CDN 的响应大小是外部输入,而下载头像时是整份读进内存的。
1. 体积上限
downloadAvatar 用的是裸 io.ReadAll,同文件里另一处请求头像链接的代码
(fetchAvatarURL)却规规矩矩用了 LimitReader(256K),两处不对称。
实测:让假服务端无限吐数据,一个头像就吃掉 512MB 内存才去解码。
现在按 avatarMaxBytes(2MB)截断 —— 真实头像最大也就几百 KB,余量很足。
多读一个字节才能分辨「刚好到上限」和「超过了上限」。
2. 像素上限(压缩炸弹)
光限制文件体积不够:几十 KB 的 PNG/GIF 能解压出上亿像素,几 MB 的文件
就能吃掉几 GB。decodeAvatar 解码后按 avatarMaxPixels(2048x2048)再查一次。
头像最终只会被缩到几十像素(跟随字号),根本用不到那么大的原图。
3. 复用连接
downloadAvatar 和 fetchAvatarURL 原来每次调用都新建一个 http.Client,
等于每个头像都重新握手一次,一场直播几百个头像就是几百次。
同项目的 notifyClient / meowClient 早就是包级复用了,这里补上。
已验证:
- 新增 3 个用例:
TestDownloadAvatarCapsResponseBody(假服务端吐 10MB,断言整份没被读完且
报错要写清是体积超限、失败不留文件)、
TestDownloadAvatarAcceptsNormalSize(96x96 正常头像不受影响)、
TestDecodeAvatarCapsPixels(3000x3000 被拦,300x300 放行)。
- 反向验证:去掉 LimitReader 后报「整份响应都被读完了(10.0 MB)」;
去掉像素检查后报「3000x3000 应当被尺寸上限拦住」。
- 一处测试写法修正:最初断言的是服务端「发出」的字节数(含 socket 缓冲,
实测 5~7MB 抖动导致误报),改成断言「是否读满了服务端准备的量」才准确
—— 客户端读到上限就撒手,服务端根本写不完整个响应。连跑三次确认稳定。
- gofmt / go vet / go build / GOOS=linux go build / go test / go test -race 全过。
|
2026-09-26 03:06:54 +08:00 |
|
tamakyi
|
7dd2487cb8
|
合并任务的可视化进度、预计剩余时间与暂停/停止
用户反馈:合并的进度只体现在日志里(ffmpeg 的 frame=/speed= 行),看不出
百分比、当前环节和还要多久,也没有办法中途暂停或放弃。
1. 新增全局任务状态(progress.go)
一份状态同时服务自动合并和手动合并,记录阶段、总进度、速度、帧率、
已处理时长、输入/预计输出/已写出体积、ETA 和速度折线采样点。
总进度按阶段加权:准备 0.02 / 分段拼接 0.08 / 获取头像 0.10 /
画面渲染 0.75 / 拼接与落盘 0.05,渲染阶段按「已渲染 ÷ 总时长」折算。
ETA 在渲染阶段用「剩余时长 ÷ 倍速」,其它阶段按已用时间线性外推。
2. 用 ffmpeg 的 -progress pipe:1 取结构化进度
runFFmpeg 拆出带回调的版本:解析 out_time_us / speed / fps / total_size,
分块渲染时把块内时间折算到整片时间轴,并把已完成块的字节数累计进来。
ffmpeg 自己的统计行仍在 stderr,单独用命令行时不会少东西。
预计成片体积先按「(视频码率 + 音频 192k)× 时长 ÷ 8」估,渲染过 5%
之后换成「已写出字节 ÷ 已完成比例」外推(estimateAudioKbps)。
3. 暂停 / 停止
Windows 用 ntdll 的 NtSuspendProcess / NtResumeProcess(照任务管理器
的「挂起」语义),Linux 用 SIGSTOP / SIGCONT,放在带 build tag 的
jobctl_windows.go / jobctl_unix.go 里。
非 ffmpeg 环节(下载头像、逐块渲染之间)用 jobGate 闸门生效。
停止 = 杀掉当前 ffmpeg 并置位,各层返回 errJobStopped:不再回退其它
编码器、不落成片、清掉分块临时文件,任务以「已停止」结束(而不是失败)。
4. 接口
GET /api/job 给页面轮询任务快照;POST /api/job/control 接受
pause / resume / stop,没有任务时 409、未知动作 400。
5. 界面
监控分区顶部新增「转换任务」卡片:阶段步骤条、总进度条、实时指标、
预计剩余时间、源片与预计成片的体积对比条、渲染速度折线(每秒一个点、
最多两分钟),以及暂停/继续/停止按钮;页面最顶上再加一条全局细进度条,
切到哪个分区都看得到。有任务时每秒轮询,空闲时 4 秒。任务结束后卡片
再停留 20 秒显示结果。
已验证:
- 新增 progress_test.go 12 个用例:阶段权重和为 1、进度按阶段折算、阶段
不回退、ETA、体积预估与实测外推的切换、折线采样、暂停/停止的闸门语义、
控制接口在无任务时报错、speed 字段与 -progress 文本的解析。
测试里把 errJobStopped 和真失败区分开(jobErrText)。
- 端到端(独立实例 51897 + 独立 config,全程没碰仓库里的实例和
acfunlive.exe):跑通完整合并,进度到 100%、预计体积与成片实际大小
相差 0.4%;暂停后进度冻结且 ffmpeg 的 CPU 时间 5 秒内 0 增长;
继续后进度恢复;停止后任务以「已停止」结束、没有半成品成片、
没有残留临时文件、没有残留 ffmpeg 进程。
- 界面(playwright 驱动系统 Edge):步骤条 5 个阶段且只有一个处于进行中、
进度条与顶部细条同步、5 项指标(速度/帧率/已处理时长/已用时间/已写出)、
ETA 文案、体积对比条两段宽度按比例、折线有采样点、界面按钮能暂停/继续/
停止、结束后按钮隐藏且文案是「已停止」、深色主题与 390 宽移动端无横向溢出。
- 过程中修掉三个真缺陷:停止时残留分块临时文件(part 登记晚于渲染)、
界面「继续」按钮点不动(onclick 写死了 pause)、停止被当成失败报红
(现在按 message 区分 已完成/已停止/失败)。
- gofmt / go vet / go build / GOOS=linux go build / go test 全过。
|
2026-09-26 02:05:47 +08:00 |
|
tamakyi
|
cbe5dfa982
|
弹幕字号按画面短边自动定,竖屏成片不再字大到铺满整行
1. 字号基准从「画面高度」改成「画面短边」
以前合并时只用高度比换算字号(字号 × 视频高 ÷ 弹幕文件高)。横屏弹幕文件
(1920x1080、60 号)配竖屏视频(720x1280)时会被放大到 71 号,而画面只有
720 宽,一条 10 字弹幕就铺满整行。横竖屏互换时高度不是约束文字宽度的维度,
短边才是:横屏取高、竖屏取宽。
2. 「弹幕字号」填 0 的含义从「跟随弹幕文件」改成「自动按画面分辨率」
取画布短边的 5.56%(60/1080)。于是 1920x1080 → 60、1280x720 → 40、
960x540 → 30,横屏行为与以前完全一致;720x1280 → 40(以前 60)、
640x1280 → 36。填大于 0 仍是精确像素覆盖。
3. 录制端竖屏档位预设 720x1280 的字号从 60 改成 40
原来横屏三档都是短边的 5.56%,只有竖屏这档是 8.33%,比横屏大 50%。
改完四档口径一致,新录的弹幕文件和成片也对得上。
4. 坐标系和字号拆开
unifyDanmuSpace 只负责把 PlayRes 统一到视频分辨率,字号统一交给
applyFontSize 按最终画布算,避免两处各改一次。
5. .gitignore 加上 /avatar_cache
头像缓存挪到程序目录后,程序正好跑在仓库里,缓存目录会被 git 看见,
git add . 会把那 336 个 PNG 一起提交进去。
已验证:
- 新增 TestShortSide / TestAutoFontSize / TestSubConfigsMatchAutoFontSize /
TestApplyFontSize;TestUnifyDanmuSpace 改成只校验 PlayRes,并且新增断言
确认它不再动字号。
- 反向验证:把短边规则临时退回「按高度」,TestAutoFontSize 报
「720x1280 应为 40,实际 71」「1080x1920 应为 60,实际 107」,
TestApplyFontSize、TestSubConfigsMatchAutoFontSize 同步失败。
- 端到端:造 720x1280 竖屏视频 + 1920x1080/60 号的 jsonl,字号填 0 真跑一次合并,
日志打出「竖屏画布 720x1280,弹幕字号自动取 40」;同一输入再用旧规则跑一次对照,
抽同一时刻的帧量墨迹高度:改前 46px、改后 25px(1.84 倍)。
- gofmt / go vet / go build / GOOS=linux go build / go test 全过。
|
2026-09-26 00:59:17 +08:00 |
|
tamakyi
|
348e3bd675
|
档位改成「自动·第二高 + 实际档位」的下拉;手动合并也推送;头像缓存挪到程序目录
三件事:
1) 主播列表的录制档位
- 一律是下拉,不再有手填输入框。第一项「自动 · 第二高」(值 0)= 按这次读到的
档位排序自上往下取第二高(只有一档就用它,读不到档位表才退回最高档);
后面依次列出实际档位。选最高/最低/中间某一档直接点它,手动选过之后
「自动」仍留在列表里,随时能改回去。
- 语义变化:0 不再被写死成某个具体码率。原来记录档位时会把「没选过」的主播
按第二档定下来(写进 config.json),现在只在取流时解析,所以主播换了档位表
也能跟着走;app 启动时的补齐逻辑(applyCachedDefaultQualities)随之去掉。
- 解析规则统一到 resolveQualityBitrate:0 = 第二高,>0 = 不超过它的最高档,
都比它高时退回最低档;「自动」项会把解析到的档位名带出来
(自动 · 第二高(当前 = 蓝光 4M))。/api/status 和档位接口都多了 qualityBitrate。
- 点「档位」按钮时先给一句「正在读取码率档位…」:主播没开播时服务端要重试三次
才放弃,约 5 秒,之前这段时间界面没有任何反应。
2) 手动合并完成也推送(这就是「MeoW 只在测试时成功」的原因)
- 原来只有自动合并调 sendNotify,手动合并从头到尾没推过,所以事件开着、渠道也通
却一直收不到。现在手动合并完成后同样按 mergeDone 事件推送两个渠道。
- 手动合并没有 uid,会拿成片名(再退到视频名)去对已有主播名,只有一个能对上时
才在文案里带上主播名,对不上就用「手动合并完成」。
3) 头像缓存挪到程序所在目录
- 原来存在「录播目录/avatar_cache」,换一次保存目录(甚至换盘)就要把同一批观众
的头像重新下一遍(一个头像可能 0.5MB)。现在统一放程序目录下的 avatar_cache。
- 旧位置作为只读回退:命中就搬过来(同盘改名,跨盘退回复制),不重复下载;
没被用到的旧文件留在原地,用到哪个搬哪个。
验证:
- 新增 avatar_test.go 两个用例(缓存路径、旧位置搬家)和 notify_test.go 的
TestManualMergeSendsMergeDoneNotify(真跑一次合并,断言 webhook 与 MeoW 各收到
一条 mergeDone,且从文件名认出主播);qualities_test.go 改成新语义,新增
resolveQualityBitrate(7 种档位/码率组合)和 guessUIDByFileName。
- 端到端:把旧位置的头像缓存放在录播目录、头像地址指向连不通的端口,合并仍然成功
且画面里有那张头像(纯红像素 400 个),证明是搬来的而不是重新下载的;文件也从
录播目录搬到了程序目录。
- 浏览器:verify_streamer_br.py 重写成档位下拉的 20 项断言,verify_qualities.py
更新为新语义,两个都全过;verify_settings.py / verify_pick.py / verify_danmu_match.py
无回归;go test ./...、gofmt、go vet 干净。
|
2026-09-25 20:03:24 +08:00 |
|
tamakyi
|
a891094f29
|
合并默认改用 HEVC、码率按「源片 + 500」自动算;手动合并自动匹配同名弹幕
两件事:
1) 合成码率与编码器
- `mergeBitrate` 填 0(默认)时不再是 ICQ 质量模式,改成「源片视频码率 + 500 Kbps」:
烧进去的文字和头像都是高频细节,和源片同码率会糊,所以留一点余量;探测不到源片
码率时才退回质量模式(QSV 用 ICQ 22、软编用 CRF 18),下限 800 Kbps。
- 编码器默认换成硬件 HEVC:候选顺序 hevc_qsv → h264_qsv → libx264,QSV 可用性按
编码器分别探测(原来只探 h264_qsv)。HEVC 在 mp4 里打 `hvc1` tag,Apple 系
(QuickTime / Safari / iPhone)才认 —— ffmpeg 默认写的是 hev1。
- 日志现在会打印编码器优先顺序和实际用的码率。
实测(90 秒真实素材,源片 3217 kbps,密集弹幕):日志「按源片 3217 kbps + 弹幕
余量 500 kbps 取 3717 kbps」,成片 codec=hevc、tag=hvc1、实际 3.71 Mbps。
之前同样是这份素材:h264 + ICQ22 会飙到 7386 kbps(79 MB),改成自动码率后
43.7 MB,体积小了 45% 画质还更稳。
2) 手动合并自动匹配弹幕
- 新增 `GET /api/merge/danmu?video=…`:按视频名找同目录下的同名弹幕,依次试
`.jsonl`、`.ass`、`.nick.ass`;同目录没有就拿文件名去录播目录再找一次。
- 页面在「手填/粘贴视频后失焦」「多选弹窗选完」「从文件列表多选后」「点预览或
开始合并时弹幕框还空着」这几种情况下都会自动匹配并提示;弹幕框里已经有内容时
绝不动它,找不到也保持为空让用户自己选。
- 填回框里的写法与浏览弹窗一致:录播/成片目录下的只写文件名,别处的写绝对路径。
测试:新增 encode_test.go(编码器顺序、参数与 hvc1、自动码率、ffmpeg 输出解析)
和 TestFindDanmuForVideo(7 个子用例:优先级、录播目录回退、绝对路径、找不到返回空);
仓库外的 verify_danmu_match.py 4 组断言全过,verify_pick.py / verify_settings.py 重跑
无回归;go test ./...、gofmt、go vet 干净。
|
2026-09-25 19:23:44 +08:00 |
|
tamakyi
|
e8b455dfe8
|
合并时把弹幕坐标系统一到视频分辨率;修「改设置会删掉正在合并的临时文件」
用户手动合并一场竖屏录播(视频 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 / 分辨率解析的用例。
|
2026-09-25 19:03:21 +08:00 |
|
tamakyi
|
9c94a72c4d
|
合并前重排弹道并支持自定义弹幕字号
用户反馈两件事:弹幕想再小些;某场合并的 1:19:25 处弹幕糊在一起(把成片抽帧
出来一看,是同一弹道上两条弹幕追尾叠字)。
重叠的根因:弹幕从上游原版的 .ass 还原成 jsonl 时,lane 是按「只有内容」的
宽度反推的(原格式的 x/y 就是这么摆的),而渲染用的是「昵称(uid):内容 + 头像」
的完整宽度 —— 后者占位时间长得多,照搬原弹道必然追尾。用用户那份 3011 条的真实
jsonl 验过:按渲染公式统计,重排前 960 条(31.9%)追尾,重排后 0 条。
- 新增 relayoutLanes:渲染前按最终字号统一重排弹道并重算宽度,公式和录制时的
placeLane 完全一致,所以录制下来的 jsonl 重排后弹道不变(已加用例覆盖)。
- 空弹道用 math.MinInt64 占位:placeLane 的条件是「本条出现时间晚于上一条的让位
时刻」,零值下 0 > 0 不成立,第一条弹幕(还原出来的时间可能正好是 0)会占不上
弹道、占用状态丢失,后面又跟着乱排。录制的弹幕时间总大于 0,所以原逻辑没暴露。
- 弹道全满(100 条都占着)时退到最早让出来的那条:录制时是直接不写这条,合并时
丢一条等于凭空少一句弹幕,压一点也比丢了强。
- 新增 danmuFontSize 设置(合并分区,0 = 跟随弹幕文件):覆盖渲染字号,头像按
同一比例缩放;改了字号会在重排时一并生效,不会因为宽度变了又追尾。
验证:layout_test.go 5 个用例;用新 ffmpeg(用户已自行升级到 n9.0.2)在隔离实例上
跑了真合并 —— 字号 95 以上的对照(60px → 42px:抽帧量到的弹幕墨迹高度 48px → 36px)、
按 1:19:00 裁出的 130 秒片段合并前后追尾 25 条 → 0 条;重跑 verify_settings.py
(新增第 8 节覆盖字号字段)、verify_pick.py、verify_streamer_br.py 无回归;
go test ./...、gofmt、go vet 干净。
|
2026-09-25 18:18:30 +08:00 |
|
tamakyi
|
e35a4f7cef
|
合并先在本地做完再落到成片目录;头像支持 gif 和静态 webp
起因是核查合并落盘和头像处理时发现的两处问题:
1) 成片落盘:ffmpeg 原来是直接写目标目录(automerge.go 里那句注释「不再事后再
移动一次」)。成片目录可能是网络盘或外接盘,写到一半失败会在那里留下一个
看起来正常的半成品。现在改成先本地合成、完成后再落盘:
- 输出先写到弹幕文件所在目录(本地)的 acfunlive_chunk_merging_*
- 写完后由 placeOutput 落盘:同盘直接 os.Rename(瞬时),跨盘退回拷贝;
拷贝中途失败留下的是 *.acfunlive.part,一眼看得出不是成品
- 成功或失败都会清掉本地临时文件,失败时两个目录都不留东西
- 临时文件沿用 chunkTempPrefix,不再出现在文件管理的列表里(listFiles 过滤)
- 启动时清理上次残留;删不掉时提示一句 —— 强杀程序不会连带杀掉 ffmpeg
子进程,它可能还在写这个文件,这种情况下次启动会再清一次
2) 头像:downloadAvatar 只注册了 jpeg 和 png 解码器,AcFun 上的 gif 头像必然
解码失败,静态 webp 同样。加上 _ "image/gif"(多帧取第一帧)和
_ "golang.org/x/image/webp"(x/image 本来就是直接依赖,零新增模块)。
动画 webp 两条路都解不开(x/image 报 invalid format,ffmpeg 5.0 也不行),
保持只烧文字。解码失败时错误信息带上 Content-Type,方便排查是哪张图。
在隔离实例上用真 ffmpeg 跑了真合并验证:gif 头像和 webp 头像都进了缓存并烧进
画面(抽帧确认圆形头像);日志里 ffmpeg 的输出是 record 下的临时文件,完成后
才出现在 merged;坏视频合成失败后成片目录没有半成品、录播目录零残留;合并
中途强杀留下的临时文件在下次启动时被清掉。
新增 output_test.go(7 个)和 avatar_test.go(3 个);go test ./...、gofmt、
go vet 干净。
|
2026-09-25 17:34:31 +08:00 |
|
tamakyi
|
ed4e47e305
|
init
|
2026-09-25 13:16:54 +08:00 |
|