Commit Graph
7 Commits
Author SHA1 Message Date
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