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
|
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
|
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
|
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 |
|