* feat(downloader): 直播预览 hub 与写盘旁路(httpflv / hls / mesio) 每路录制任务一个 PreviewHub(寿命 = 整场 execute(),跨分段与断流重试),下载器开始拉流时 attach 一个写入端,在写盘点旁边把同一份媒体字节 push 给它:httpflv 在关键帧写出循环、 stream-gears/hls 在分片 chunk 循环、mesio 在条目进入修复管线前。fMP4 分片流与 HEVC 序列头 标为不可预览;ffmpeg / streamlink / yt-dlp / 边录边传明确不可预览并给出原因。 写入端热路径只做三件事:GOP 缓冲追加、try_recv 订阅请求、broadcast::send。没有 .await、 没有锁等待、没有 ?;订阅请求由写入端在下一次推送时回应,快照(文件头 + 序列头 + 当前 GOP) 与随后的实时分块严格衔接。GOP 快照上限 8 MB,广播缓冲 256 槽 × 64 KB 且无订阅者时不占用, 每路订阅者数由信号量限制;慢订阅者收到 Lagged 后由端点断开让其重连。 Part of #1708 * feat(api): GET /v1/streamers/{id}/live 与列表的 preview 字段 新增 chunked 视频流端点:先发关键帧对齐的快照再接实时分块,Content-Type 按容器为 video/x-flv 或 video/mp2t,Cache-Control: no-store。房间不存在 / 未录制 404,下载器不能 旁路 415(带原因),连接数超限 429(每路 4、进程 16),尚未开始拉流或正在重连 503 + Retry-After。慢客户端 Lagged 或写入端换代时结束响应,由播放器重连。注册在 router() 内, --auth 时与 /v1/streamers 同一道登录校验。 GET /v1/streamers 每项新增 preview: { available, format: "flv" | "mpegts" | null, reason }, 未录制为 null。 Part of #1708 * feat(preview): fMP4 分片流也能预览——init segment 快照、按分片广播、解出 codecs mesio 的 HLS 会话拿到 fMP4 分片时不再标为不可预览:init segment(ftyp + moov)作为文件头 进快照,每个 moof + mdat 分片作为一个关键帧分块广播,新订阅者拿到 init + 最近一个完整分片, 端点 Content-Type 为 video/mp4,列表字段 preview.format 为 "fmp4"。 从 init segment 的 moov/trak/…/stsd 解出 RFC 6381 编码串(avc1.PPCCLL、hvc1.…、mp4a.40.N、 av01 / vp09 / ac-3 / ec-3 / opus / flac)放进 preview.codecs,供前端 MediaSource.isTypeSupported() 校验后 addSourceBuffer;解析只在 attach 后收到 init 时做一次,不在逐分块路径上。 stream-gears 的 HLS 路径没有下载 #EXT-X-MAP 初始化分片,遇到非 TS 分片时标为不可预览并说明原因。 按 ForgQi 在 #1708 的要求把 fMP4 纳入阶段二。 Part of #1708 * feat(ui): 直播间卡片的「预览」弹层——复用录制流的内置播放器 Player.tsx 加 isLive / muted / autoplay / type / codecs 与 onEnded / onError:flv 与 mpegts 走 mpegts.js(直播模式:不攒缓冲、追帧、自动清理 SourceBuffer),fmp4 走一个轻量 MediaSource 播放器(fetch ReadableStream → appendBuffer 串行队列,首个缓冲对齐播放位置,落后 > 3 s 追到 末尾,定期 remove 30 s 之前的缓冲),都以 Artplayer customType 挂载保持 UI 一致; type 未知时用一次 GET 读 Content-Type 再起播。MediaSource.isTypeSupported 不通过时报错并显示 封面 + 原因,不转圈。 StreamerCard 录制中加「预览」图标按钮(不可预览时禁用,tooltip 显示后端给的原因),封面可点 即打开预览;弹层(LivePreview.tsx)关闭即卸载播放器、断开连接。服务端断开(掉队 / 换直链) 自动重连 3 次,之后交给用户手动重试;不可恢复错误 Toast 提示一次。零新依赖。 Part of #1708 * feat(ui): 历史记录页「实时监视」Tab——多路同屏监视器 <LiveMonitor /> 是独立组件:所有正在录制的直播间以小窗网格展示,可预览且在同屏路数内的 直接播放(默认静音),超出的显示封面、点击替换最早开始的一路(先进先出),下载器不能旁路 的显示封面与原因。同屏路数默认 DEFAULT_MONITOR_TILES = 4(可选 1/2/4/6/9,localStorage 记住), 每路小窗占对应直播间的一个预览连接。正在播放的列表由「用户选择 + 当前可预览房间 + 路数」 纯推导,不在 effect 里 setState。 历史记录页改为两个 Tab:「录制文件」(原表格)与「实时监视」,keepDOM=false 切走即卸载全部 播放器。要把监视器挪到别的页面只需换挂载点。 Part of #1708 * test(preview): 让旁路测试不依赖调度时机 rolling 测试先把订阅请求 poll 进写入端队列再开始写盘,源数据按 2 ms 一块限速送入而不是瞬间灌完 (瞬间灌完时订阅者必然掉队,那是慢订阅者测试覆盖的场景);慢订阅者测试只断言 「收到 + 被告知跳过 = 全部」与写入端不等待,不再对读取任务在负载下的吞吐做假设。 * fix(ui): mpegts.js 的 MSE 解码失败给出可读提示 典型来源是 TS 的 PES 没按访问单元对齐(如 Twitch 的 HLS),mpegts.js 拆出的帧不完整、浏览器拒绝 解码;原文 "Failed to execute 'appendBuffer'…" 对用户没有信息量,改为说明原因并强调录制不受影响。 * fix(preview): fMP4 与 TS 的快照从关键帧起,而不是从分段边界起 B 站 hls_fmp4 的分段(约 1 s)各含多个 moof/mdat 分片、且分段边界不对齐关键帧(实测 10 次冷启动 只有 1 次首帧是 I 帧):写入端现在把每个分段按 moof/mdat 切开,读 init 里的视频轨 ID 与 trex 默认 flags、trun 的 first_sample_flags / 首样本 flags / tfhd 默认 flags,视频首帧为同步样本的 分片才作为 GOP 起点。TS 分片起点则嗅探第一个视频 PES 的 NAL 类型(IDR/SPS → 起点,只有普通片 → 追加到上一 GOP,说不准 → 按起点处理)。FLV 路径本来就按关键帧 tag 切,未变。 单测覆盖:分片切分(前导 styp、非同步 / 同步 / trex 默认 / 无视频轨)、跨分段边界的快照、 TS 嗅探(跨包的 PES、音频 PES 在前、非 TS 输入);另有 #[ignore] 的 B 站真实抓样核对。 * feat(danmaku): 预览播放器的实时弹幕——录制中的弹幕客户端旁路一份到 SSE danmaku crate 的 RecorderConfig 新增可选的 live_tx(tokio broadcast):每解出一条事件,在写 XML 之前 send 一份(不等待,无订阅者即丢弃),XML 录制路径不变。DownloadTask 按 stream.danmaku 是否存在建一条 256 槽的广播,传给 danmaku_client;新增 GET /v1/streamers/{id}/danmaku(SSE, 每条 event 为 danmaku / gift / super_chat / guard_buy,data 为 {kind, text, name, color, ts}, 进场消息不进;掉队跳过不断开;进程上限 32 路,超出 429;无弹幕客户端的平台 415)。 /v1/streamers 的 preview 字段新增 danmaku: bool,前端据此启用 / 禁用弹幕开关。 Part of #1708 * feat(ui): 预览弹层装载实时弹幕(开关控制),监视器默认关 Player.tsx 新增 danmaku: { url, enabled }:装 artplayer-plugin-danmuku(新依赖,Artplayer 官方弹幕 插件,5.3.0),开关打开时用 EventSource 连 /v1/streamers/{id}/danmaku,每条事件 emit 进弹幕层 (普通弹幕滚动、礼物 / 醒目留言 / 上舰置顶描边),关闭时断开 SSE 并隐藏弹幕层,不重建播放器。 弹层标题栏加「弹幕」Switch,默认开、记在 localStorage(biliup.preview.danmaku);平台没有弹幕 客户端时禁用并 tooltip 说明。监视器工具栏加同名开关,默认关(biliup.monitor.danmaku), 对全部小窗生效。localStorage 偏好统一走 useSyncExternalStore(app/lib/use-local-pref.ts), 同屏路数也改用它。 Part of #1708 * fix(preview): FLV 广播缓冲扩到 1024 槽,装得下 CDN 的整 GOP 突发 斗鱼 CDN 把整个 GOP(~4 s、~320 个 tag、~2 MB)一次突发送达,httpflv 解析器 1 ms 内推完; 刚建立的订阅者还没读到第一个实时分块,256 槽的缓冲就已被覆盖,收到 Lagged 被断开,重连后 又赶上下一个突发——冷启动 10 次只有 1 次播起来。FLV 分块是单个 tag,小而多,缓冲按格式分开: FLV 1024 槽(实测均值 ~10 KB → ~10 MB),TS / fMP4 沿用 256 槽(分块大而少,16 MB 上限)。 回归测试:请求在关键帧回应后紧跟 600 个 tag 同步推完,订阅者仍逐个读到。 Part of #1708 * fix(ui): 弹幕开关禁用时说明是平台不支持还是未开对应 *_danmaku 配置 * feat(danmaku): 多个直播间的实时弹幕可复用一条 SSE(GET /v1/danmaku?ids=…) 浏览器对同一主机的 HTTP/1.1 并发连接只有 6 个:监视器同屏 N 路视频已占 N 个,弹幕若每路再开 一条 SSE,N=4 时就把列表轮询也饿死。新增 /v1/danmaku?ids=1,3,9 把若干路合成一条,事件 JSON 带 id 供前端分发;没有弹幕 / 未录制的 id 静默跳过,一个都没有时 404;一条最多 16 路,占 1 个 连接许可。原 /v1/streamers/{id}/danmaku 保留,走同一实现(事件同样带 id)。 Part of #1708 * feat(ui): 弹幕连接由页面持有并复用,监视器路数按 HTTP 协议限制 浏览器对同一主机 HTTP/1.1 只有 6 个并发连接,监视器 4 路视频 + 每路一条弹幕 SSE 就把列表轮询 饿死;9 路时也只有 6 路能播(实测)。改为 DanmakuFeed:页面(弹层 / 监视器)持有一条 /v1/danmaku?ids=… 连接,各播放器按 id 订阅;构造无副作用、open() 后有订阅者才连,StrictMode 下重复构造无害。同屏路数选项在 HTTP/1.1 下为 1 / 2 / 4,页面经 h2 / h3 加载时放开 6 / 9。 小窗弹幕字号 15px,弹层 22px。 Part of #1708 * fix(ui): 解码失败后关闭弹层必须断开拉流连接 <video>.error 非空时 mpegts.js destroy() 里清 SourceBuffer 的 remove() 抛 InvalidStateError, 后面的 transmuxer.close() 跑不到,连接留在服务端占着该路的预览许可直到刷新页面——真实 Twitch TS 房间(mpegts.js 放不了)连开 4 次弹层第 4 次就 429。销毁前先把 MediaSource 从元素上摘下 (load() 使其 closed,mpegts.js 对 closed 的 MediaSource 跳过清理),destroy 仍抛则直接关 内部 transmuxer 兜底。 Part of #1708 * fix(ui): mpegts.js 换用可中止的自定义 loader,弹层关闭一定断开拉流 上一提交把 MediaSource 从元素上摘下只避免了 destroy() 抛异常,连接仍未断开:mpegts.js 自带的 FetchStreamLoader 在 Chrome 上处于 buffering 状态时 abort() 不调 AbortController.abort(), 而是等下一个 read() 回来再 reader.cancel()——解码出错后读取链已断,那一次永远等不到,连接留在 服务端占着该路许可(真实 Twitch TS 房间:关闭弹层后用 4 条 curl 探,1 条 429;再开一次 2 条 429, 直到浏览器退出才释放)。改为 customLoader:每次 open() 一个 AbortController,abort() 直接调它, 其余与原 loader 一致(直播不需要 Range)。 Part of #1708 * fix(ui): 忽略 Artplayer 在实例销毁后的自动重连,杜绝孤儿拉流连接 真正的泄漏来源:Artplayer 收到 video:error 后每隔 1 s 重设 art.url 最多 5 次(RECONNECT_TIME_MAX), 不检查自己是否已 destroy。弹层在解码失败时立刻卸载播放器,1 s 后 Artplayer 仍在已销毁的实例上 重设 url → customType 再建一个 mpegts.js 实例、再拉一路流,没有任何人持有它——每次打开泄漏 最多 5 条连接,直到刷新页面。mpegts / fMP4 两个 customType 入口对 art.isDestroy 直接返回; fMP4 入口同时复用上一次的 dispose(同一实例重设 url 时先清掉旧的 MediaSource)。 mock 上用循环发送的 Twitch 坏样本复现:修前每开一次多 1 条连接(1→2→3),修后开关 4 次始终 0。 Part of #1708 * fix(ui): 自动播放重试前确认播放器还活着 * fix(preview): 最后一个订阅者走后释放广播缓冲 tokio broadcast 的缓冲只在被新数据覆盖时丢旧分块,而没有订阅者时写入端不再 send——最后一个 订阅者走后,缓冲里最后 N 个分块就一直挂着(FLV 1024 槽约 10 MB / 路;7 路真实短测中 RSS 在 一轮预览后从 95 MB 停在 117 MB)。写入端发现订阅者归零且缓冲里有数据时换一条新通道,旧缓冲 整个释放;之后的 subscribe() 拿新通道,对订阅者透明。 Part of #1708 * feat(preview): 直播预览取流方式配置(relay / direct)与浏览器直连的能力判定、直链端点 后端与浏览器不在一台机器时,浏览器直接向 CDN 拉一路能省服务器出口带宽(ForgQi,#1712 review 2)。 全局配置新增 preview_transport:relay(默认,复用录制流经 biliup 中转)/ direct(浏览器直连 CDN), JSON 配置里可选字段,不需要迁移。/v1/streamers 的 preview 新增 direct: { capable, reason },按 review 1 实测的平台表判定:B 站 / 抖音 / 虎牙 FLV 能直连;斗鱼一 token 一连接、直连会挤掉录制; Twitch CDN 无 ACAO;HLS(TS / fMP4)本版本回落——mpegts.js 放不了 m3u8,直连需 hls.js;其它平台 未验证按不能。新增 GET /v1/streamers/{id}/live-url:当前录制中那条流的直链、容器、平台与过期时间 估计(B 站 expires / 抖音 expire 十进制 Unix 秒,虎牙 wsTime 十六进制;斗鱼的相对秒数不算), Cache-Control: no-store,与 /live 同在登录校验之内。DownloadTask 随每次 check_stream 刷新直链。 Part of #1708 * feat(ui): 直播预览取流方式——全局设置下拉,direct 模式下浏览器直连 CDN、失败自动回落中转 空间配置 → 全局设置新增「直播预览取流方式(preview_transport)」:经 biliup 中转(默认)/ 浏览器直连 CDN,文案说明两者区别与适用场景。LivePreviewPlayer 按(房间, 配置, 能力)派生取流方式:direct 且 平台能直连时先 GET /v1/streamers/{id}/live-url 拿直链(https 页面下 http 直链换 https),mpegts.js 以 cors / 不带凭据拉 CDN;出错或断开时重取一次直链再播,仍失败回落中转;平台不能直连的房间在 direct 模式下直接走中转。播放器左上角角标:「直连 CDN」或「已回落中转:<原因>」。监视器小窗共用 同一组件。异步结果与回落都带 sourceKey,配置切换时不在 effect 里同步 setState。 Part of #1708 * fix(ui): 直连模式下弹层脚注说明取流方式 * feat(preview): 直连模式向平台另取一条直链,不复用录制那条;斗鱼由此也能直连 ForgQi(#1712):走直连就不要复用链接,直接获取新链接。/v1/streamers/{id}/live-url 改为对该房间的 平台插件再调一次 check_stream,把新拿到的 raw_stream_url(新 token)给浏览器;平台返回未开播或 出错时 503,前端回落中转。实测新 token 的斗鱼 / 虎牙直链各读 25 s 稳定,同时录制那条连接无重试 ——review 1 观察到的「斗鱼一 token 一连接」「虎牙第二条连接只给 GOP 缓存即断」都是同一直链两条 连接的问题,另取一条就绕开了。能力表:B 站 / 抖音 / 斗鱼 / 虎牙 FLV 能直连;Twitch 无 ACAO、HLS 需 hls.js 仍回落。Monitor 新增 PluginFor 消息按房间 URL 取插件。 Part of #1708 * fix(ui): 直连播够 60 s 后再断按直链到期处理,重取直链继续直连 斗鱼 CDN 的 token 每 5 分钟切一次连接(录制那条同样如此,日志里每 5:01 一次 StreamEnded → 重连), B 站直链 1 h 过期。之前直连只允许重取一次,第二次到期就回落中转;现在区分「刚起播就失败」 (重取一次仍失败 → 回落)与「播了一阵才断」(视为到期,照常再取)。 Part of #1708 * feat(preview): HLS 直连——直链是 m3u8 时走 hls.js(按需加载),B 站 hls_fmp4 等也能直连 ForgQi(#1712):HLS 直连也支持一下。新依赖 hls.js ^1.7(动态 import,只在直连模式打开 HLS 房间时 才下载那一个 chunk,首屏包不变)。live-url 的 format 按新直链后缀给 flv / hls;direct_capability 只看平台的跨域放行,不再因容器排除——B 站 .m4s 分片的 CDN 同样 ACAO: *。Player 新增 hls 分支: 跟到最后 3 个分片、后向缓冲 30 s、分片请求不带凭据;清单 / 分片 404、CORS、解码等致命错误报给 LivePreviewPlayer 走重取直链 / 回落中转,清单变为非直播视为断开。Twitch 仍因无 ACAO 回落。 Part of #1708 * feat(preview): live-url 同一房间 5 秒去抖,播放失败后的重取带 fresh=1 绕过 每次打开预览都向平台 API 取一条新直链(带 cookie 时消耗登录态配额,B 站接口有频控)。同一房间 5 s 内的重复请求(StrictMode 双调用、连点、同一房间多个小窗)复用上一次结果并标 cached: true, 不再打平台;前端播放失败后的重取传 ?fresh=1 一定拿新 token(抖音某节点 404、直链到期都靠它)。 缓存插入时顺手清掉一分钟前的条目,表不会长。 Part of #1708
219 lines
6.2 KiB
TypeScript
219 lines
6.2 KiB
TypeScript
// Fetcher implementation. // The extra argument will be passed via the `arg` property of the 2nd parameter.// In the example below, `arg` will be `'my_token'`
|
||
export const API_BASE = process.env.NEXT_PUBLIC_API_SERVER ?? '';
|
||
export async function sendRequest<T>(url: string, { arg }: { arg: T }) {
|
||
const res = await fetch(API_BASE + url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(arg),
|
||
});
|
||
await handleResponse(res);
|
||
return res.json();
|
||
}
|
||
|
||
export const fetcher = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||
const res = await fetch(API_BASE + input, init);
|
||
await handleResponse(res);
|
||
return res.json();
|
||
};
|
||
|
||
export const proxy = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||
const res = await fetch(API_BASE + input, init);
|
||
await handleResponse(res);
|
||
return res;
|
||
};
|
||
|
||
export async function requestDelete<T>(url: string, { arg }: { arg: T }) {
|
||
const res = await fetch(`${API_BASE}${url}/${arg}`, { method: 'DELETE' });
|
||
await handleResponse(res);
|
||
return res;
|
||
}
|
||
|
||
export async function put<T>(url: string, { arg }: { arg: T }) {
|
||
const res = await fetch(`${API_BASE}${url}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(arg),
|
||
});
|
||
await handleResponse(res);
|
||
return res;
|
||
}
|
||
|
||
async function handleResponse(res: Response) {
|
||
// 如果未登录,统一跳转
|
||
if (res.status === 401) {
|
||
// 可选:清理本地状态/缓存
|
||
// localStorage.removeItem('token') 等
|
||
|
||
// 跳转登录(带回跳)
|
||
const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
|
||
window.location.href = `/login?next=${returnTo}`;
|
||
// 抛错让 SWR 知道失败(别返回 json)
|
||
throw new Error('Unauthorized');
|
||
}
|
||
|
||
if (!res.ok) {
|
||
// 尽量返回服务端错误信息
|
||
const text = await res.text().catch(() => '');
|
||
throw new Error(text || `HTTP ${res.status}`);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
type Credit = {
|
||
username: string;
|
||
uid: number;
|
||
};
|
||
|
||
export interface StudioEntity {
|
||
id: number;
|
||
template_name: string;
|
||
user_cookie: string;
|
||
copyright: number;
|
||
copyright_source: string;
|
||
tid: number;
|
||
tid_v2?: number | null;
|
||
cover_path: string;
|
||
title: string;
|
||
description: string;
|
||
dynamic: string;
|
||
tags: string[];
|
||
dtime: number;
|
||
// interactive: number;
|
||
mission_id?: number;
|
||
dolby: number;
|
||
hires: number;
|
||
no_reprint: number;
|
||
is_only_self: number;
|
||
up_selection_reply: number;
|
||
up_close_reply: number;
|
||
up_close_danmu: number;
|
||
charging_pay: number;
|
||
credits: Credit[];
|
||
uploader: string;
|
||
extra_fields?: string;
|
||
}
|
||
|
||
/** 正在录制的直播间能否在页面内预览(复用正在录制的那一路流,见 GET /v1/streamers/{id}/live) */
|
||
export interface LivePreviewInfo {
|
||
/** 当前下载器能否提供预览;为 true 时 format 仍可能是 null(刚开始拉流、容器未定) */
|
||
available: boolean;
|
||
/** 视频流的容器,与 /live 响应的 Content-Type 一致:flv / mpegts 走 mpegts.js,fmp4 直接 MediaSource */
|
||
format: 'flv' | 'mpegts' | 'fmp4' | null;
|
||
/** fmp4 时从 init segment 解出的 RFC 6381 编码串(如 avc1.64001f,mp4a.40.2),其它容器为 null */
|
||
codecs: string | null;
|
||
/** 不可预览的原因(ffmpeg / streamlink 子进程落盘、HEVC FLV 等) */
|
||
reason: string | null;
|
||
/** 这一路有没有实时弹幕(平台实现了弹幕客户端),有则 /v1/streamers/{id}/danmaku(SSE)可用 */
|
||
danmaku: boolean;
|
||
/** 浏览器能否直连 CDN 拉这一路(全局配置 preview_transport = direct 时用;不能则回落中转并显示原因) */
|
||
direct: DirectCapability;
|
||
}
|
||
|
||
export interface DirectCapability {
|
||
capable: boolean;
|
||
/** 不能直连的原因;capable 为 true 时为 null */
|
||
reason: string | null;
|
||
}
|
||
|
||
/** GET /v1/streamers/{id}/live-url:正在录制的那条流的 CDN 直链 */
|
||
export interface LiveUrlInfo {
|
||
url: string;
|
||
/** 浏览器该用哪个播放器:flv → mpegts.js,hls → hls.js(TS / fMP4 分片都行) */
|
||
format: 'flv' | 'hls' | null;
|
||
platform: string;
|
||
/** 过期时间估计(Unix 秒),直链里没有可识别的过期参数时为 null */
|
||
expires_at: number | null;
|
||
direct: DirectCapability;
|
||
}
|
||
|
||
/** 直播预览的取流方式(全局配置 preview_transport),空值视同 relay */
|
||
export type PreviewTransport = 'relay' | 'direct';
|
||
|
||
export interface LiveStreamerEntity {
|
||
id: number;
|
||
url: string;
|
||
remark: string;
|
||
filename: string;
|
||
split_time?: number;
|
||
split_size?: number;
|
||
filename_prefix?: string;
|
||
upload_id?: number;
|
||
upload_streamers_id?: number | null;
|
||
status?: string;
|
||
upload_status?: string;
|
||
/** 录制中最近一个滑动窗口的写盘速率(字节/秒);未录制 / 尚无采样 / 下载器不支持时为 null */
|
||
live_bytes_per_sec?: number | null;
|
||
/** 录制中的直播间封面地址;图片请走 /v1/streamers/{id}/cover 代理 */
|
||
live_cover_url?: string | null;
|
||
/** 录制中的主播头像地址;图片请走 /v1/streamers/{id}/avatar 代理 */
|
||
live_avatar_url?: string | null;
|
||
/** 录制中的预览能力;未录制为 null */
|
||
preview?: LivePreviewInfo | null;
|
||
statusTag?: React.ReactNode;
|
||
format?: string;
|
||
time_range?: string | Date[];
|
||
excluded_keywords?: string[];
|
||
preprocessor?: Record<'run', string>[];
|
||
segment_processor?: Record<'run', string>[];
|
||
downloaded_processor?: Record<'run', string>[];
|
||
postprocessor?: (Record<'run' | 'mv', string> | 'rm')[];
|
||
opt_args?: string[];
|
||
override?: Record<string, any>;
|
||
}
|
||
|
||
export interface BiliType {
|
||
id: number;
|
||
children: BiliType[];
|
||
name: string;
|
||
desc: string;
|
||
}
|
||
|
||
export interface User {
|
||
id: number;
|
||
name: string;
|
||
value: string;
|
||
platform: string;
|
||
}
|
||
|
||
export interface BiliArchive {
|
||
aid: number;
|
||
bvid: string;
|
||
title: string;
|
||
cover: string;
|
||
reject_reason: string;
|
||
reject_reason_url: string;
|
||
duration: number;
|
||
desc: string;
|
||
state: number;
|
||
state_desc: string;
|
||
dtime: number;
|
||
ptime: number;
|
||
ctime: number;
|
||
}
|
||
|
||
export interface BiliArchivePage {
|
||
from_page: number;
|
||
page_size: number;
|
||
total: number;
|
||
total_pages: number;
|
||
fetched_pages: number;
|
||
archives: BiliArchive[];
|
||
}
|
||
|
||
export interface FileList {
|
||
key: number;
|
||
name: string;
|
||
updateTime: number;
|
||
size: number;
|
||
}
|
||
|
||
export interface StreamerInfo {
|
||
id: number;
|
||
name: string;
|
||
url: string;
|
||
title: string;
|
||
/** Unix 时间戳(秒),由后端 ts_seconds 序列化 */
|
||
date: number;
|
||
live_cover_path: string;
|
||
}
|