Commit Graph
2 Commits
Author SHA1 Message Date
tamakyi c26eb4c648 合并任务排队串行,不再互相顶掉进度和杀错 ffmpeg
审查发现:全局只有一份任务进度状态,但没有任何东西拦住两个合并同时跑。

1. 为什么必须串行
   jobStart() 原来是无条件重置全部字段的。多个主播同时下播(很常见),
   或者自动合并跑着的时候在页面上点一次手动合并,就会有两个任务同时跑:
   后启动的把前一个的输出名、进度、已渲染时长全部清零;更糟的是 jobStop()
   只认最后注册的那个 ffmpeg,点「停止」杀的是别人家任务的进程,
   而自己那个继续跑;两个任务还会互相读到 stopped 标记而提前退出。
   界面上看只是一个进度条乱跳,背后是合并结果可能张冠李戴。

2. 怎么串起来的
   加一把 mergeSlot 锁占任务位:
   - 手动合并用 TryLock,抢不到直接 409 并告诉用户现在谁在跑 —— 它是一次
     HTTP 请求,不能让人干等几十分钟。
   - 自动合并用 Lock 排队等待,它本来就是后台任务,丢掉白录一场。
   jobStart 改成返回 bool:已经有任务在跑时返回 false 且一个字段都不动。
   这样即使以后有调用方忘了先拿任务位,也只是失败而不会互相覆盖。

3. 顺带修「panic 之后卡片一直转」
   原来 jobFinish() 写在 autoMerge / mergeHandler 的正常返回路径上,任务内部
   一旦 panic(外部 safeGo 的 recover 兜在外层)它就永远不会执行,任务会一直
   停在 active,界面上的进度条再也不动,ffmpeg 也失联了。
   现在统一走 runMergeExclusive():任务位用 defer 释放,panic 翻译成 error,
   保证 jobFinish 一定能跑到。

4. 之前 ban 掉的 jobActive() 接回来了
   它本来就该是这道护栏,写了却没人调用,现在用于判断要不要提示「在排队」
   以及拒绝新任务时报出正在忙的任务名。jobState 的锁改成 RWMutex 供它读。

已验证:
- 新增 4 个用例:TestJobStartRejectsWhenBusy(被拒时 Must 一个字段都不改,
  包括进度必须还是原任务的)、TestMergeSlotTryLockExclusive、
  TestMergeSlotSerializes(4 个并发任务必须全跑完且不重不漏)、
  TestMergeExclusiveReleasesOnPanic(panic 后必须拿到任务位 + 翻出错误)。
- 反向验证:把 jobStart 的占用判断临时改成 `if false && job.active`,
  TestJobStartRejectsWhenBusy 立刻报「应当返回 false」。
- 现有测试跟着走真实链路(progress_test 加了 startJobForTest 统一先收尾、
  manualmerge_test 走 execManualMerge 独占执行)—— 改之前这些用例单独跑过、
  连着跑会失败,正是因为前一个用例把任务状态留在了 active。
- gofmt / go vet / go build / GOOS=linux go build / go test / go test -race 全过。
2026-09-26 02:41:29 +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