Files
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

14 KiB
Raw Permalink Blame History

acfunlive web API

web API 默认本地端口为 51880

http://localhost:51880/listlive 列出正在直播的主播

http://localhost:51880/listrecord 列出正在下载的直播视频

http://localhost:51880/listdanmu 列出正在下载的直播弹幕

http://localhost:51880/liststreamer 列出设置了开播提醒或自动下载直播的主播

http://localhost:51880/startmirai 利用 Mirai 发送直播通知到指定 QQ 或 QQ 群

http://localhost:51880/addnotifyon/23682490 通知 uid 为 23682490 的主播的开播

http://localhost:51880/delnotifyon/23682490 取消通知 uid 为 23682490 的主播的开播

http://localhost:51880/addnotifyoff/23682490 通知 uid 为 23682490 的主播的下播

http://localhost:51880/delnotifyoff/23682490 取消通知 uid 为 23682490 的主播的下播

http://localhost:51880/addnotifyrecord/23682490 通知 uid 为 23682490 的主播的直播视频下载

http://localhost:51880/delnotifyrecord/23682490 取消通知 uid 为 23682490 的主播的直播视频下载

http://localhost:51880/addnotifydanmu/23682490 通知 uid 为 23682490 的主播的直播弹幕下载

http://localhost:51880/delnotifydanmu/23682490 取消通知 uid 为 23682490 的主播的直播弹幕下载

http://localhost:51880/addrecord/23682490 uid 为 23682490 的主播直播时自动下载其直播视频

http://localhost:51880/delrecord/23682490 取消自动下载 uid 为 23682490 的主播的直播视频

http://localhost:51880/adddanmu/23682490 uid 为 23682490 的主播直播时自动下载其直播弹幕

http://localhost:51880/deldanmu/23682490 取消自动下载 uid 为 23682490 的主播的直播弹幕

http://localhost:51880/addkeeponline/23682490 uid 为 23682490 的主播直播时在其直播间里挂机

http://localhost:51880/delkeeponline/23682490 取消设置在 uid 为 23682490 的主播直播时在其直播间里挂机

http://localhost:51880/delconfig/23682490 删除 uid 为 23682490 的主播的所有设置

http://localhost:51880/getdlurl/23682490 查看 uid 为 23682490 的主播是否在直播,并输出其直播源

http://localhost:51880/addqq/23682490/12345 将 uid 为 23682490 的主播的开播提醒发送到 QQ12345,需要 QQ 机器人已经添加该 QQ 为好友

http://localhost:51880/delqq/23682490/12345 取消将 uid 为 23682490 的主播的开播提醒发送到 QQ12345

http://localhost:51880/cancelqq/23682490 取消将 uid 为 23682490 的主播的开播提醒发送到任何 QQ

http://localhost:51880/addqqgroup/23682490/12345 将 uid 为 23682490 的主播的开播提醒发送到 QQ 群 12345,需要 QQ 机器人已经加入该 QQ 群,最好是管理员,会@全体成员

http://localhost:51880/delqqgroup/23682490/12345 取消将 uid 为 23682490 的主播的开播提醒发送到 QQ 群 12345

http://localhost:51880/cancelqqgroup/23682490 取消将 uid 为 23682490 的主播的开播提醒发送到任何 QQ 群

http://localhost:51880/startrecord/23682490 临时下载 uid 为 23682490 的主播的直播视频

http://localhost:51880/stoprecord/23682490 取消下载 uid 为 23682490 的主播的直播视频

http://localhost:51880/startdanmu/23682490 临时下载 uid 为 23682490 的主播的直播弹幕

http://localhost:51880/stopdanmu/23682490 取消下载 uid 为 23682490 的主播的直播弹幕

http://localhost:51880/startrecdan/23682490 临时下载 uid 为 23682490 的主播的直播视频和弹幕

http://localhost:51880/stoprecdan/23682490 取消下载 uid 为 23682490 的主播的直播视频和弹幕

http://localhost:51880/log 查看 log

http://localhost:51880/quit 退出 acfunlive 运行

http://localhost:51880/help 显示帮助信息

新版 web UI 的管理接口

web UI(-webui,端口看webPort)用的是另一套/api/接口,全部需要带管理口令:请求头X-Auth-Token: <口令>,或者在 URL 上带?token=<口令>(下载链接用后者,因为浏览器跳转带不了请求头)。

口令支持任意字符,包括中文——不过 HTTP 请求头只接受 ISO-8859-1,浏览器遇到非 ASCII 的口令会直接拒绝发请求(报String contains non ISO-8859-1 code point),所以页面发给服务端的X-Auth-Token是百分号编码过的口令(encodeURIComponent)。服务端两种写法都认:先拿原样比,再拿解码后的比,所以手写 curl 或者脚本按原样把中文口令放进请求头、Authorization: Bearer、?token= 里也都能通过。用脚本调这套接口时不用管编码,原样填就行。

GET /api/streamer/qualities?uid=<uid> 去直播间接一次该主播的码率档位(返回{"uid":…,"qualities":[{"name":"蓝光 4M","bitrate":4000},…],"qualityPref":"second","bitrate":0,"qualityBitrate":实际会用到的档位码率,"qualityName":实际会用到的档位名},qualities按码率从高到低)。读到的档位会缓存进config.json(没开播时页面靠它把偏好标成具体档位名),但不会改动用户选的偏好。同一个主播 10 秒内只让读一次,太频繁返回 502 并给中文原因(没开播时也会返回 502,且没开播时服务端本来就要重试三次,页面上会先给一句正在读取码率档位…)。

POST /api/streamer里的qualityPref是按第几档来选,而不是写死码率:top(最高档)、second(第二高,默认)、lowest(最低档)、custom(取不超过bitrate的最高档,都比它高时退回最低档)。四种偏好任何时候都能设,开播后自动对应到实际档位。bitrate只在custom时有意义:已存在的主播不传就保持原值(页面从最高档切到别的档位时不会把填过的自定义码率清掉)。GET /api/status里每个主播的qualities是缓存的档位表,qualityPref/bitrate是用户选的,qualityBitrate/qualityName是按偏好解析出来的结果(还没读到档位表时,前三种偏好解析为 0、custom解析为填的上限)。

跟手动合并有关的是下面三个: GET /api/merge/danmu?video=<视频文件名或路径> 按视频名找同目录下的同名弹幕文件,给页面「选好视频自动匹配弹幕」用。依次试.jsonl、.ass、.nick.ass,同目录没有就拿文件名去录播目录再找一次;返回{"danmu":"…"},找不到时是空字符串。值里录播目录/成片目录下的文件只写文件名,别处的写绝对路径,和浏览弹窗的填法一致。

POST /api/merge/info 预览一次手动合并的计划,不落盘。

{
    "videos": ["2026-09-25 01-00-00 主播 标题.mp4", "D:/别处/随手放的视频.mp4"],
    "danmu": "2026-09-25 01-00-00 主播 标题.ass",
    "offset": 3                // 可选,不填按文件名里的录制时间推算
}

返回分段列表(名字、体积、时长、是否正在写入)、总时长、弹幕条数、弹幕时间范围、要用的弹幕文件和来源(jsonl 弹幕数据 / 同名的 jsonl 弹幕数据 / 由 xxx.ass 还原)、推算出的偏移和原因、最终输出文件名、以及提示(warnings)。参数不合法一律 400,并在响应体里给中文原因。

POST /api/merge 真正开始合并,请求体和上面一样,另外可以带outName指定输出文件名(不合法会被清理成安全的文件名,重名自动加后缀)。成功后立刻返回{"outName":"…"},合并在后台跑。已经有一个手动合并任务在跑时返回 409。合并完成也会触发一次推送(和自动合并同一个mergeDone事件):手动合并没有 uid,会拿成片或视频的文件名去对已有的主播名,只有一个能对上时才在文案里带上主播名,对不上就用手动合并完成。

合并状态通过GET /api/status查看:mergeBusy(是否在跑)、mergeOutName(当前成片名)、mergeErr(失败原因)、mergeDoneAt(上次完成时间)。合并期间成片会出现在GET /api/files里且inUse为 true,此时POST /api/file/remove会返回 409 拒绝删除。

GET /api/job 返回转换任务(自动合并和手动合并共用一份)的实时进度,页面每秒轮一次它来画监控分区顶部的进度卡片和全局细进度条;空闲时返回active: false,但会保留上一次任务的结束信息(doneAt、message、err)方便界面把结果多显示一会儿。字段:

字段 含义
kind / outName 任务类型(手动合并 / 自动合并)和成片文件名
phases / phaseIdx / phasePercent 阶段名列表、当前阶段序号、当前阶段内的完成比例
percent 总进度 0~100,按各阶段权重折算(渲染占 0.75)
paused / stopping / pausable 是否已暂停 / 是否正在停止 / 现在能不能暂停
speed / fps / doneSec / totalSec 渲染倍速、帧率、已渲染时长、视频总时长
chunkDone / chunkTotal 分块渲染的进度(第 3/12 块)
avatarDone / avatarTotal 获取头像阶段的用户数进度
inputBytes / estOutBytes / estSource / outBytes 源片总体积、预计成片体积及其来源(bitrate 按码率估 / measured 按已渲染部分外推)、已写出字节
etaSec 预计剩余秒数,算不出来时为 0
samples 渲染速度折线的采样点(约每秒一个,最多 120 个)
message / err / doneAt 结束提示(已完成 / 已停止 / 失败)、失败原因、结束时刻

POST /api/job/control 控制当前任务,请求体{"action":"pause"}、{"action":"resume"}或{"action":"stop"},成功返回{"ok":true}。没有任务在跑时返回 409,未知的 action 返回 400。暂停会挂起 ffmpeg 进程(Windows 用NtSuspendProcess、Linux 用SIGSTOP),CPU 占用归零;不在 ffmpeg 环节时暂停和停止会在下一个检查点生效。停止会丢弃已经渲染的部分:不落成片、清掉分块临时文件,任务以message: "已停止"结束(err为任务已被手动停止,界面据此和真失败区分开)。

POST /api/config里的danmuFontSize控制合并时弹幕文字的字号(像素),头像按同一比例缩放,填 0 表示按画面分辨率自动:字号取画面短边的 5.56%(横屏取高、竖屏取宽),所以 1920x1080 是 60、1280x720 是 40、720x1280 也是 40 —— 竖屏画面窄,若按高度算会得到 71,一条弹幕就铺满整行。显式填写则按填的像素值直接用。渲染前会按最终字号重排弹道:从上游原版.ass还原出来的弹幕只按内容宽度占位,而渲染用的是「昵称(uid):内容 + 头像」的完整宽度,占位时间更长,照搬原弹道会追尾叠字;重排按完整宽度重新分配,同一条弹道上不会再有两条弹幕叠在一起。

mergeBitrate填 0(默认)时按「源片码率 + 500 Kbps」自动计算:先探测视频流的码率,加 500 留给烧进去的弹幕;探测不到就退回质量模式。填大于 0 的值则按该码率固定输出。编码器优先硬件 HEVC(hevc_qsv,同画质比 H.264 省约三成体积,mp4 里打hvc1标记),不支持时回退h264_qsv、libx264,日志里会打印实际用的那一个。

POST /api/notify/test 测试某个推送渠道能不能连通。用传进来的值直接测,留空的项自动用已保存的配置补齐,所以不用先保存设置。测试不受渠道开关和事件勾选影响。

// QQ 机器人(webhook)渠道,channel 不填就按这个
{ "channel": "qq", "url": "http://127.0.0.1:8080/acfunlive/notify", "token": "可选", "group": "可选" }
// MeoW 渠道
{ "channel": "meow", "nickname": "你的昵称", "msgType": "text", "imgUrl": "可选", "htmlHeight": 200 }

成功后返回{"ok":true,"detail":"webhook 返回 HTTP 200,用时 0.31 秒。…"},detail就是页面上显示的那句结论。失败时返回 502 并在响应体里给中文原因(如MeoW 返回 status 404(昵称未注册)),参数不合法返回 400。

MeoW 的status和封禁规则会反映在GET /api/status的meowBlocked字段里:昵称没填、格式不对、或已被 MeoW 判过「未注册」时,这里会给出中文原因,非空就说明 MeoW 渠道当前推不出去。注意 MeoW 一天内累计 3 次推给不存在的昵称会封 IP,所以被判过不存在的昵称会被程序记住并停止重试,改昵称或重启程序才恢复。

GET /api/browse 给页面上的浏览按钮列目录用。参数:path(要展开的目录,不填就用程序当前的工作目录)、kind=file(连文件一起列,挑文件时用)、hidden=1(连以点开头的隐藏项一起列)。页面上的浏览有单选(保存目录、ffmpeg 路径、弹幕文件)和多选(手动合并的视频文件)两种用法,接口每次只负责列一层,多选的勾选状态由页面自己保管,所以可以换着目录勾、跨文件夹攒;填回框里时,录播目录下的文件写文件名、别的文件夹里的写绝对路径。

{
    "path": "D:\\record",                                     // 当前展开的目录,已转成绝对路径
    "parent": "D:\\",                                         // 上级目录,已经到根时是空串
    "roots": [{"name": "C:", "path": "C:\\", "dir": true, "size": 0}],
    "entries": [{"name": "2026-09-25.mp4", "path": "D:\\record\\2026-09-25.mp4", "dir": false, "size": 123}],
    "truncated": false,                                       // 条目太多时只返回前 2000 项
    "error": ""                                               // 打不开时的中文原因
}

文件夹排在文件前面,同类按名字排(不区分大小写)。挑文件时文件夹照样会列出来,不然进不去子目录。error非空时仍是 HTTP 200,entries是空的,但parent会带上——页面据此退到上一级,不会把用户卡在一个打不开的路径里(比如把文件路径当目录展开时)。

这个接口能看到整台机器的目录,但权限和其它管理接口一样,都是拿到口令即全部权限,没有放宽。