tamakyi
|
3c17503a21
|
转码的源文件不再限死在录播/成片目录,可以填任意路径
为什么
转码页的多选框只列 /api/files 的内容,也就是录播目录和成片目录两处;后端的
resolveTranscodeInput 走 resolveFile,也只认 record/merge 两个目录名。结果是
录播原片被合并且清掉之后(或者录播目录本来就空),转码页里只剩合并结果可挑,
磁盘上别处的视频一个都进不来。
怎么改
- transcodeJobReq 加 Path 字段,与 dir/name 二选一;新增 resolveTranscodeJob()
把「填了路径就按路径找(认绝对路径和从资源管理器复制来的带引号路径,走
resolveMergeInput)、否则回落 dir+name」收在一处,两条路最后统一过
mergeVideoExts 白名单 —— 路径这条能指向磁盘上任何文件,不挡的话手滑把 .jsonl
丢进来要等 ffmpeg 跑起来才报错。三个调用点和 runTranscode 全部改走它。
- transcodeProbe 回显绝对路径,页面拿它当任务身份:同名文件在不同目录里不会再
被认成一个,重复加入也因此挡得住。
- 转码卡片改成两栏:左边保留原来的文件列表多选,右边加「一行一个路径」的输入框
和「浏览…」按钮(复用已有的目录选择器,可多选、可跨文件夹跨盘攒,选完只追加不
覆盖)。openPick 多了个可选的第 5 参数用来指定起始目录,浏览转码文件时从成片
目录开始(录播目录可能是空的)。
- 报错前缀改用 transcodeJobReq.label():按路径进来时 Name 是空的,直接拼 j.Name
会变成「:参数不合法」。
验证
gofmt -l 干净;go vet 干净;Windows 与 GOOS=linux 编译均通过。
go test ./... 全绿;go test -race -count=1 ./... 27.2s 全绿;顶层用例 209 个
(含子测试 226),本次新增 3 个:-run 单独跑过 TestResolveTranscodeJobByPath /
TestResolveTranscodeJobRejectsBadPath / TestTranscodeProbeHandlerAcceptsPath。
反向验证 44/44:把 resolveTranscodeJob 的路径分支退回 resolveFile 后,上面两个
用例确实变红。
转码接口冒烟 40/40(新增 11 项按路径的:探测回显路径、估算、不存在 400、非视频
后缀 400、指向文件夹 400、真排队转出成片)。
页面 jsdom 冒烟 49/49 且无 JS 报错(新增 12 项:路径框存在、空框不给加入、手填能
进列表并带上绝对路径、探测请求按 path 发、重复加入不重复、浏览弹窗打开/追加不
覆盖/关闭/清空)。
|
2026-09-27 11:10:00 +08:00 |
|
tamakyi
|
a409c5b4dd
|
新增「转码」分区与手动合并逐任务配置,修掉 PC 端图表文字被拉扁
一、PC 端图表文字被拉伸(bug)
.spark 折线图把 viewBox 写死成 "0 0 300 48" 又带 preserveAspectRatio="none":
移动端元素宽≈300px 时缩放≈1 看不出问题,PC 上卡片宽约 1030px,横轴被放大 3.4 倍
而纵轴仍是 48px,图里的 <text> 被整体拉扁。现在按元素实际宽度算 viewBox 宽度
(取不到时回落 300),并监听 resize 重画;preserveAspectRatio 保留不影响 1:1 显示。
二、新增「转码」分区(transcode.go,第七个 tab)
原来只有「录播 + 弹幕 → 成片」一条路,已有成片想压小、换 HEVC、降分辨率只能手搓命令行。
现在从文件列表多选加入,每个文件一行独立配置:编码器、码率控制(跟随源片 / 目标码率 /
质量 CRF·ICQ / 源片百分比)、编码速度、输出帧率、分辨率(只列不高于源片的档)、
音频(拷贝 / 128k / 192k / 去掉),并实时预估产物大小与整批合计。
- POST /api/transcode/probe | estimate | transcode,都在既有的口令鉴权之后
- 目标码率模式按「码率 × 时长」出准数;质量模式用单调系数表插值并标注是粗估
- 落盘沿用合并那套:先在源文件所在目录生成,完成后再 placeOutput 到输出目录,
失败或中断删临时文件,不留半成品
- 与合并共用同一张进度卡片和任务位(mergeSlot),串行排队,可暂停 / 停止
三、手动合并改成「一次排多个任务」
原来一次只能合一场。现在多选视频后按「文件名里的录制时间 + 主体名」自动归组
(相邻两段间隔 2 小时内算同一场),每组一个任务卡,各自配弹幕、偏移、输出名和
合成参数(码率 / 帧率 / 字号 / 编码器),可拆组、可移除;服务端串行跑,逐个回传结果。
请求体从 {videos,danmu,...} 扩成 {jobs:[{...}]},老的单任务写法继续可用。
四、审阅中一并修掉 / 加固的点
- 容器总码率含音频,直接当视频码率用会错两处:预估体积时又加一遍音轨(算成双份),
「跟随源片」时把音轨码率也编进视频里。现在分开记「视频流码率」与「容器总码率」,
转码侧按「视频流码率 → 容器码率 − 音轨 → 体积 ÷ 时长 − 音轨」三级取值;
合并的自动码率仍按老口径(容器含音频),单独用 streamOrContainerKbps 锁住
- 源片本来没有音轨时,预估不再凭空加 128 kbps 音频
- 目标码率加上界(200 Mbps),单位写错的数在开跑前就被挡住(合并的逐任务码率同理)
- 探测结果加缓存(ffmpeg + 路径 + 体积 + 修改时间),体积估算跟着输入框重发时不再
每次都起一个 ffmpeg;文件一变(录制中追加、换了文件)缓存自然失效
- 只有音轨 / 只有封面图的文件、以及正在写入的文件,排队时就被挡住并说清原因
- probeMedia / parseMediaInfo 合并了原先重复的两套解析(probeDuration 与
probeVideoStream 各起一次 ffmpeg 解析同一段输出),burnDanmu 现在一次探测拿全
- 前端:估算参数非法时把服务端给的原因显示在合计行(原来静默不动,像是没改成功);
估算请求加序号,慢的旧回包不再覆盖新结果;转码下拉刷新时保留已勾选项;
跑完的行标「完成 / 失败」并淡化;删掉没有对应元素、从未使用的 CSS 规则
验证
- gofmt / go vet 干净;go build(windows)与 GOOS=linux go build 均通过
- go test ./... 20.9s 全过;go test -race ./... 26.1s 全过(182 → 206 个用例,
transcode_test.go 新增 20 个)
- 反向验证 C:\OtherFiles\acf-test\reverse_check.py 从 37 项加到 43 项,全过
- 51899 隔离实例 + 真 ffmpeg 冒烟 29 项全过:401 鉴权、probe 的 hasVideo/hasAudio、
ts 容器(不带每流码率)探测到的视频码率 = 容器 429 − 音轨 127、
HEVC 按 0.65 折算、参数非法 400 带原因、只有音轨的文件排队即被拒、
输出目录不存在被拒、两任务队列真转成功并落到指定目录、缩放成片高度确实是 240、
日志里带「第 n/N 个」和参数摘要
- 页面 jsdom 冒烟 37 项全过、无 JS 报错(分组 / 拆组 / 逐任务参数 / 转码行渲染 /
跳过无视频轨文件 / 估算失败提示 / 结果徽标)
|
2026-09-27 10:36:29 +08:00 |
|
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
|
c55fe3539d
|
口令失败记录加上硬上限,并且不再傻等到满 delay
审查发现这张表的容量可以被外部放大:IP 来自 r.RemoteAddr,是请求方能左右的输入。
1. 原来为什么会被写满
清理逻辑只在 len > 1000 时才触发一次,而且只删「10 分钟没动静」的。
换着来源持续撞口令时,每条记录的 last 都是刚刚更新的,于是「虽然超了
1000 条但一条都不符合删除条件」,表现就是表不停往上涨。
实测:3000 个不同来源各失败一次之后,表里 3000 条,一条没删。
2. 现在怎么收敛
- 加硬上限 authFailMaxEntries(1 万条)。到上限立刻扫一次,
只靠「每 256 条扫一次」的话两个周期之间涌进来的会把表顶破。
- 扫表先删过期项,还多就按「最久没动静」排序淘汰。
- 淘汰时留一成余量(淘汰到九成),这样满表之后还能再收一千来个
新来源才需要排下一次,不会变成「每来一个就排一遍全表」。
- 同一来源的失败次数封顶。延迟早就封顶了(2 秒),数字继续涨没有意义。
3. 延迟等待改成可以被断开
原来在 handler 里直接 time.Sleep(最多 2 秒)。并发撞口令时每个请求各占一个
goroutine 干等两秒,很容易把服务拖住。现在用 timer + r.Context():
客户端一断开立刻返回,不再占着连接。
已验证:
- 新增 5 个用例:
TestAuthFailTableHasHardCap(2 万个新来源撞完,表必须被压在 1 万以内)、
TestAuthFailExpiredEntriesGetSwept(过期项扫干净)、
TestAuthFailSweepsPeriodically(定期扫是自动的,不用调用方记着)、
TestAuthFailCountCapped(同来源次数封顶、延迟封顶)、
TestAwaitAuthDelayStopsOnCancel(上游取消后立刻返回,不能傻等满 2 秒)。
- 反向验证:把淘汰目标临时改成 authFailMaxEntries*1000(等于没上限),
TestAuthFailTableHasHardCap 立刻报「应压在 10000 以内,实际 20000」。
- gofmt / go vet / go build / GOOS=linux go build / go test / go test -race 全过。
|
2026-09-26 03:00:37 +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
|
aa47dd342d
|
修:选了「自定义」却看不到码率框(焦点还在下拉上,列表不重建)
页面为了让定时刷新不冲掉用户正在填的内容,列表在「焦点还在列表里」时不重建。
但原生 select 选完一项后焦点会留在下拉上,于是选了「自定义」以后列表一直不重建,
那个码率框要等用户点到别处才出现 —— 看起来就是「选了自定义根本没地方填」。
- 选到「自定义」时当场把码率框建出来并聚焦(不用等服务端回来和下一次重建),
切走时当场收掉。
- 顺带把「不重建」的条件收紧成「只有正在列表里的文本框里打字时才跳过」:
下拉和勾选都是改完立即提交的,之前它们也会一直卡住重建,
连带「录播中/弹幕中」的徽标也要点到别处才刷新。
- 框里的初始值:填过自定义就用上次那个数(切走再切回来不会被改成当前档位),
没填过才用现在实际在用的那一档。
验证方式也换了:回归脚本里改用「点开下拉 + 键盘选最后一项」这种真实操作
(Playwright 的 select_option 不保留焦点,正是它让这个 bug 漏了过去),
新增「选完立刻就有框」「焦点直接落在框里」「切走即收掉」「切回带着上次的值」四条断言;
verify_streamer_br.py 33 项、verify_qualities.py 全过,verify_settings.py /
verify_danmu_match.py 无回归,go test ./...、gofmt、go vet 干净。
|
2026-09-25 21:14:59 +08:00 |
|
tamakyi
|
237fffa8fe
|
录制档位改成「最高/第二高/最低/自定义」四种相对偏好,没开播也能先设
上一版的下拉里列的是「这次读到的实际档位」,没读到档位表(没开播)时下拉里就只剩
一项,想预先设「我要最高档」或「最低档」都做不到。改成按「实际档位里的第几档」来选:
- 新增 `qualityPref`:top(最高档)/ second(第二高,默认)/ lowest(最低档)/
custom(取不超过给定码率的最高档,都比它高时退回最低档)。四种偏好任何时候都
在列表里,开播后自动对应到实际档位;读到档位表后把对应结果标在选项后面
(`第二高(蓝光 4M)`、`自定义(≤2500K = 高清)`)。
- `bitrate` 只在 custom 时有意义。`resolveQualityBitrate(tiers, pref, custom)` 统一
解析:相对偏好没有档位表时返回 0(取流退回最高档),custom 返回填的上限。
解析只在取流时做,所以主播换了档位表,偏好会跟着一起变。
- 老配置兼容:没有 qualityPref 时,`bitrate > 0` 当成 custom(就是原来的「码率上限」,
行为不变),`bitrate == 0` 当成默认的第二高;在 checkConfig 里补齐(不写盘,下次
保存自然带上)。`setBitrate` 相应换成 `setQuality(uid, pref, custom)`。
- 页面:选 custom 时旁边出现码率框,初始值就是**当前实际在用的那一档**(不会一切过去
就掉到最低档),回车或失焦即提交,填错给提示并回填服务端的值;从 custom 切到别的
档位不带 bitrate,服务端保留原值,改回来还是原来那个数。
- 设置区同步:新主播的默认档位也用同一套四项(`defaultQualityPref`),只有选自定义
才露出码率上限框(切过去会补一个能用的 3000,免得新主播默默用最低档)。
顺带修一个脚本抓到的真 bug:档位名传成了档位对象(`esc()` 里会 `.replace`),只要
档位表非空就会抛异常,而 refresh() 整段包在 try/catch 里 —— 表现是整个主播列表
变成空白且没有任何报错。现在取的是 `qs[i].name`。
测试:qualities_test.go 重写 TestResolveQualityBitrate(10 种偏好/码率组合)并新增
TestMigrateQualityPref、TestUpgradeQualityPrefs、TestSetQuality;verify_streamer_br.py
改成带迷你后端的假服务(POST 会真正落到假状态上),30 项断言全过;verify_qualities.py
25 项(含设置区与 config.json 往返)全过;verify_settings.py / verify_pick.py /
verify_danmu_match.py 无回归;go test ./...、gofmt、go vet 干净。
|
2026-09-25 20:42:22 +08:00 |
|
tamakyi
|
7b853c1a22
|
档位下拉:指定过的码率不在档位表里时,文案不再重复括号
老配置里常见的是 bitrate 5000 这种「码率上限」,它不是任何档位名,之前会渲染成
「已指定:5000K(5000K)」。现在写成「已指定:≤5000K」,读到档位表后带上档位名
(「已指定:≤5000K(蓝光 4M)」),和 >0 时「取不超过它的最高档」的语义也对得上。
|
2026-09-25 20:05:53 +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
|
eda83dd1e3
|
主播列表的码率改为直播实际档位下拉,没选过时默认第二档
原来是一个手填的码率上限(Kbps),现在改成直播的实际档位(蓝光 8M / 高清…):
- 读到档位后,主播那一行渲染成下拉,选完立即生效;正在录制时换档会自动重录。
- 没手动选过(bitrate 0)的主播按**自上往下第二档**定下来(如
蓝光 8M / 蓝光 4M / 高清 就选蓝光 4M,只有一档就用那一档),不会一上来就吃满
最高档;已经手动选过的永远不动。
- 档位要开播后读直播源列表才知道,读到以后(录制开始时、或页面点「档位」按钮)
缓存进 config.json 的 qualities,所以没开播也能看和改;启动时会把「有缓存但还
没定过码率」的主播按第二档补齐。
- 从来没读过档位的行仍然是手填输入框,旁边多了个「档位」按钮去直播间接一次。
后端:新增 `GET /api/streamer/qualities?uid=…`(同一主播 10 秒内只读一次),
`/api/status` 的每个主播多了 `qualities`(按码率从高到低)和 `qualityName`;
抽出 `newStreamInfo()`/`fetchStreamQualities()`,`getStreamURL()` 取流时顺手缓存
档位,并在 bitrate 为 0 时改用刚定下来的默认档位(否则还是最高档)。
档位相关的纯逻辑放在 config.go:`sortQualitiesDesc` / `defaultQualityBitrate` /
`qualityNameFor` / `sameQualities` / `setStreamerQualities` / `applyCachedDefaultQualities`。
测试:qualities_test.go 7 个用例(排序、默认第二档的 5 种档位数、档位名匹配、
写配置时不覆盖手动选择、启动补齐、JSON 存取往返);浏览器实测 5 组断言全过
(下拉选中当前档位、换档服务端立即变、无档位时保留输入框+按钮、启动默认第二档、
手填仍可用),verify_streamer_br.py / verify_pick.py / verify_settings.py 无回归;
go test ./...、gofmt、go vet 干净。
|
2026-09-25 19:42:47 +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
|
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
|
eb250bb842
|
web UI:手动合并的视频文件也能点选,一次可选多个
之前只有弹幕框挂了「浏览…」,视频框只能手填,或者用那个原生多选下拉
(要按 Ctrl、点一下就把手填的内容整段覆盖、手机上根本没法用)。原因是
视频框是「一行一个、可能多个」的形态,而浏览弹窗当时只支持单选。
改成同一个弹窗支持多选:
- 「浏览…」多带一个多选开关,视频框用它打开;列表里点文件名即勾选、
再点取消,条目左边有勾选框,底部显示「已选 N 项」并能一键清空。
勾选状态由页面保管、换目录不清,所以可以进别的文件夹继续勾、跨盘攒。
- 选用是**追加**到框里:手填的行和之前选好的行都留着,重复的不加第二遍。
双击某个文件=把这一个一起追加进去并关窗(和单选的「双击即选用」一致)。
- 填入时录播目录下的文件只写文件名(跟着保存目录走),别处的写绝对路径;
单选的弹幕框也按同一规则,顺手让框里干净些。
- 多选框取第一行当起点,不再把整段多行文本当路径去解析。
顺带修一个连带问题:手动合并的视频/弹幕框不进配置,refresh 也不会回填它们,
之前选完却会打上「未保存」标记 —— 顶栏那句「有未保存的修改」永远亮着,
点保存也清不掉。现在只对设置表单里的框打标记。
verify_pick.py 扩到 48 项断言(勾选/取消/跨目录累积/清空/追加不覆盖/填文件名
还是绝对路径/手机端点选/配置全程没动)全部通过;verify_settings.py 与
verify_streamer_br.py 重跑无回归;go test ./...、gofmt、go vet 干净。
|
2026-09-25 16:43:21 +08:00 |
|
tamakyi
|
7a396a8852
|
web UI:目录和文件既能点「浏览」逐级选,也能手工填
浏览器拿不到本地绝对路径,所以「交互式选择」做成后端列目录 + 弹窗点选:
- 新增 GET /api/browse:path 为空时用程序所在目录,kind=file 连文件一起列,
hidden=1 带上点开头的隐藏项;文件夹优先排序,上限 2000 项。打不开时仍返回
200 并带上 error 和 parent —— 页面据此自动向上退,不会把用户卡在一个打不开
的路径里(框里填的是文件路径时,就是这样落到它所在的目录的)。权限没放宽,
和其它管理接口一样走口令。
- 文件保存目录、成片移动到、ffmpeg 路径、手动合并的弹幕文件这四个框加了
「浏览…」按钮,输入框本身照旧可以直接填或粘贴;弹窗里的地址栏也能粘贴路径
回车跳转,点选和手填随时切换。默认不列隐藏项,需要时勾「显示隐藏项」。
- 文件夹单击进入(手机上也能用),文件单击选中、双击直接选用;连点同一项、
以及刚跳转过后的 350ms 内忽略点击,双击文件夹不会连进两层。
- 选完只标记「未保存」并填进输入框,不自动保存,和文本框的规则保持一致。
- 补上 README 里的用法和 doc/webapi.md 里的接口说明。
新增 browse_test.go(12 个用例);仓库外的 verify_pick.py 30 项断言全过,
go test ./...、gofmt、go vet、构建均干净。
|
2026-09-25 16:30:01 +08:00 |
|
tamakyi
|
0dfa224bac
|
web UI:没提交的文本框不再被状态刷新刷回原值
页面每 3 秒拉一次 /api/status 回填表单,而回填只跳过「正在聚焦」的元素,
所以文本框改完点一下别处(或按回车,输入框不在 form 里根本不会提交),
几秒后就被服务端的旧值盖回去,看起来像「改了又自己恢复」——
改合成码率时的现象就是输入框里明明是 6500、过了 3 秒自己变回 1500。
服务端日志里一条 6500 的保存记录都没有,全是前端盖的。
- 表单字段加上「未提交」标记,定时刷新不再覆盖;提交成功才清掉
- 回车等于点保存设置(主播码率框回车即提交,新增 uid 回车即添加)
- 有没提交的内容时顶栏提示「有未保存的修改」,保存按钮右上角标红点,
离开页面前再问一次
- 保存失败保留用户填的内容,不再静默回滚,提示里带上服务端给的原因
- 开关、下拉的立即保存用服务端值填还没提交的字段,避免一个框填错
(如成片目录不存在)把开关也一起打回去
- 主播列表里的码率框:重建列表时保留用户改过还没提交的值,填错立刻
回填服务端值
|
2026-09-25 15:50:34 +08:00 |
|
tamakyi
|
a2f27974b2
|
web UI 按用途分成六个分区,整体改版并跟随系统深浅色
需求是「界面比较杂乱,做一下分类,UI 也现代化一些」。原来所有设置挤在一张
「设置」卡片里,从保存目录一路排到管理口令,加上通知推送的两个渠道,一屏
拉不到底,找东西全靠翻。
1) 分成六个分区,顶部一条导航切换
- 监控:添加主播 + 主播列表 + 运行日志(默认打开的就是这里)
- 录制:保存目录、录播/弹幕和成片子文件夹、录播格式、默认码率上限、ffmpeg 路径
- 合并:自动合并的三个开关、成片输出目录和合成码率、手动合并
- 通知:推送哪些事件、QQ 机器人渠道、MeoW 渠道
- 文件:磁盘容量 + 文件列表
- 设置:外网访问、管理口令
- 分区只靠 class 切显示,所有元素始终在 DOM 里,所以原来按 id 取值的
那一大堆逻辑一行都不用改;「监控」是默认页,因为看录制状态是最高频的操作
2) 地址栏带上 #分区名
- 用 replaceState 写,不往历史里塞记录,但刷新、收藏、把链接发给别人
都能落到同一个分区;也顺便记住了上次看的是哪一页(localStorage 兜底)
3) 大改样式(都在同一个模板里,没有引入任何外部依赖)
- 颜色、圆角、阴影收进 CSS 变量,卡片改成柔和阴影 + 大圆角,输入框加了
hover 和 focus 光晕,按钮分主次两级
- 勾选框改成开关样式(隐藏原生 input + 兄弟节点画轨道,所以 el.checked
和 :focus 判断照旧能用);主播列表的「弹幕/录播」改成小圆片
- 把原来那个行内的小 #msg 文案换成右下角提示条:成功 2.6 秒、失败 6 秒
自动消失,最多同时留 4 条,报错信息不会再一闪而过
- 顶栏常驻状态摘要(录制中 N / 直播中 N / 空闲),「保存设置」也挪到顶栏,
一直可见——文本框可能在别的分区里,按钮跟着分区走会找不到
- 跟随系统深浅色(prefers-color-scheme),配套改了 color-scheme 元信息;
日志面板在两种模式下都是深色终端风格
- 手机上标题居中、右侧按钮收成两个字(「保存」),分区导航横向滚动;
顶栏用 1fr auto 1fr 的网格,两侧等宽才是真的居中
- 文件列表里的文件名原来被 max-width:200px 截成「2026-09-24 20-00-00 Xsp…」,
文件名本来就是用来认文件的,改成不截断、允许换行
4) 顺手修的两个真问题
- 口令里有中文时,页面彻底用不了:HTTP 请求头只接受 ISO-8859-1,浏览器
遇到非 ASCII 的 X-Auth-Token 会直接抛 TypeError
(Failed to read the 'headers' property…),请求根本发不出去,页面只显示
「无法连接到服务器」,用户既进不去也没法在页面上把口令改回来。
现在页面统一把口令 encodeURIComponent 之后再放进头里,服务端
authTokenCandidates() 把原样和解码后的都作为候选逐个定长比较
(不提前返回,避免响应时间泄露是哪一种对上了),所以中文口令能用,
curl / 脚本按文档原样发中文口令也照样能过
- 删「正在写入」的文件返回 500 而不是 409,见上一个提交
5) 文档
- README 的「使用方法」补分区表、保存规则(开关立即生效 / 文本框点保存)、
顶栏状态和深浅色说明;各处「设置区」改成对应的分区名
- doc/webapi.md 补口令编码那段,说明脚本侧按原样填即可
已验证:新增 webui_test.go 5 个用例(ASCII 口令、含 % 的口令、中文口令的
编码与原样两种写法、Bearer 与 ?token= 三个来源、以及错口令/空口令/口令未配
一律拒绝、候选顺序与非 Bearer 的 Authorization 不算数),全量 39 个用例通过。
真机用 Edge 无头浏览器跑了两轮共 40 项断言,覆盖:自动登录、六个分区逐一点开
(只显示该分区、页签高亮、hash 跟着变)、六个分区 + 深色 + 三个移动端截图、
开关点了立刻落盘并能点回来、顶栏保存写到配置文件、在别的分区保存不会把隐藏
表单的值清空、刷新后仍在原分区、MeoW 红字提示的显示与隐藏、合并中按钮禁用
并改文案、口令弹窗的失败与重试,桌面/深色/移动端三处控制台零报错。
中文口令另做了端到端验证:配置里放中文口令能直接进、换口令后接口继续可用、
重开页面用新口令能进、旧口令被拒;把客户端编码去掉后同一个测试立刻失败,
确认这个修复是必需的而不是想当然。Windows/Linux 双平台构建、go vet、
gofmt 全干净。51880 的上游原版全程未动。
|
2026-09-25 14:19:44 +08:00 |
|
tamakyi
|
be8ec5f953
|
通知推送拆成两个渠道:QQ 机器人和 MeoW,各自带开关和测试连通
用户要求两件事:① QQ bot 推送要能测连通、并加一个开关;② 照 MeoW 的接口
文档(https://www.chuckfang.com/MeoW/api_doc.html)加一个推送渠道,同样要有
测试和开关。确认过的三个决定:QQ bot 就是现有 webhook;事件勾选两个渠道
共用、开关按渠道分开;MeoW 只填一个昵称。
1) 通知推送改成「共享事件勾选 + 每渠道一个开关」
- notify.go: notifySetting 扩成两个渠道,新增 qqReady()/meowReady();
sendNotify 按渠道各自起后台 goroutine 分发,事件没勾选、或两个渠道都
没启用时什么都不做
- config.go: 新增 notifyEnabled(QQ 机器人渠道开关)、meowEnabled、
meowNickname、meowMsgType、meowHtmlHeight、meowImgUrl
- 关掉开关只是暂停推送,地址、昵称这些配置都留着,随时能再打开
2) 老配置的兼容(重要)
- notifyEnabled 是普通 bool,老 config.json 里没有这个字段会解成 false,
升级后会把已经配好的推送静默关掉
- 解法:loadConfig() 里先置 true 再 json.Unmarshal(Unmarshal 只覆盖文件里
出现的字段),默认值里也写 true;文件里显式写 false 的仍然会关掉
- 已加回归测试 TestNotifyEnabledBackwardCompat 覆盖三种写法
3) MeoW 渠道(新增 meow.go)
- 按文档走 POST {昵称},title/msg/url/imgUrl 放 json body,
msgType/htmlHeight 是 query 参数(文档明确说这两个不属于 JSON 参数)
- 文档自己前后矛盾:响应格式那节写 msg,示例写 message,两个字段都解析
- 空字段一律不放进 body:body 里的 title 优先级高于路径参数,
传空串会把 MeoW 的默认标题顶掉
- 业务状态码翻成中文(400 参数错、403 内容被拦、404 昵称未注册、
429 频控、500 服务器错误),失败信息直接显示在页面上
- msgType 为 html 时渲染成 HTML(首行加粗、链接可点、内容转义),
高度默认 200、范围 40-2000;事件正文复用 webhook 的那份 text
4) MeoW 的错误昵称会封 IP,所以加了保护(踩出来的)
- 实测用不存在的昵称打真实接口,返回的提示是「一天之内累计3次输错对方
昵称您的IP将被封禁」。而开播+开始录制+下播+合并完成一天轻松触发 4 次,
昵称配错就会把用户 IP 封了
- meow.go: 新增 meowBadNick 记录,pushMeow 收到 404 就记下这个昵称;
sendNotify 和测试接口命中记录时直接跳过/拒绝并说明原因,
改掉昵称或重启程序即恢复
- 只有 404 触发拦截:429 是频控、400 是参数问题,都应该继续尝试
- 已加测试 TestMeowBadNickGuard(404 要拦、429/400 不能拦、不影响别的昵称)
5) 测试连通
- /api/notify/test 合并成一个接口,用 channel 区分 qq / meow,
每个渠道的配置下面各有一个「测试连通」按钮
- 用页面上当前填的值直接测、不用先保存,留空的项自动用已保存的值补齐;
测试不受渠道开关和事件勾选影响(本来就是为了打开之前先确认配置)
- 成功返回 {ok, detail},detail 就是页面上的结论:QQ 渠道显示 webhook 的
HTTP 状态码和耗时,MeoW 渠道显示它返回的 status 和耗时;
webhook 返回 200 只代表地址口令没问题,能不能收到要看机器人那边,
这点也写在结果里
- /api/status 新增 meowBlocked,页面上红字显示「当前推不出去:…」
(昵称没填、格式不对、或已被判过未注册)
6) 页面
- 通知区分成「事件勾选 / 渠道一 QQ 机器人 / 渠道二 MeoW」三块,
每块用一条带标题的细线隔开,两个渠道各带开关和测试按钮
- 消息类型选 text 时自动收起 HTML 高度那一行
另外:README 的通知推送一节重写(两个渠道的对比表、MeoW 的频控与封禁提醒、
测试连通的说明),配置文件详解补上新字段;doc/webapi.md 补 /api/notify/test
和 meowBlocked。doc/momoko-acfunlive-notify.py 本来就处理 event="test",不用改。
已验证:新增 notify_test.go,15 个用例全部通过(URL 拼接与转义、昵称校验、
消息类型与高度归一、HTML 渲染与转义、接口成功/失败/空字段处理、6 种业务状态码
的文案、404 拦截保护、老配置兼容、测试接口各分支),其中 TestSendNotifyFanOut
是端到端分发测试:两个渠道都开→各收到 1 条、关掉任一个→只剩另一个、
两个都关→0 条、事件没勾→0 条。真机冒烟两轮:页面控件齐全、老配置默认开启、
QQ 渠道实测本地 webhook 收到完整推送(含 Authorization 和 X-Auth-Token 头与群号)、
空值回退到已保存配置、6 种非法输入全 400、开关能落盘且关开关不清地址、
MeoW 真实接口返回的 404 被正确翻成中文。全量 34 个测试通过,
Windows/Linux 双平台构建、go vet、gofmt 全干净。
|
2026-09-25 13:42:49 +08:00 |
|
tamakyi
|
ed4e47e305
|
init
|
2026-09-25 13:16:54 +08:00 |
|