Files
biliup/app/ui/plugins/bilibili.tsx
T
Noah 67e49354ad fix(recorder): stop stream-gears losing the tail, default to mesio (#1733)
* fix(stream-gears): write the cached GOP on disconnect, flush before the segment hook

The httpflv loop kept the current GOP in `flv_tags_cache` and only wrote it
when the next keyframe arrived. On EOF, a 30 s read timeout or a parse error
the function returned and the cache was dropped, so every disconnect,
reconnect and stream end lost the last 1-10 s. The loop now runs in an inner
function; whatever way it exits, the complete tags in the cache are written
before the original result (including the error) is returned.

`FlvFile` / `TsFile` renamed the `.part` file and ran the segment hook before
the old `BufWriter` was flushed, so the hook (FileValidator, upload) could see
a truncated file, and a failed final flush (disk full) was swallowed. Both now
go through `finish()`: flush and check the error, then rename, then run the
hook. `create_new` returns the flush error; `Drop` logs it with `error!`.

Tests: the rolling-segment test now expects 120 keyframes on disk (was 119)
and checks the size seen by the hook equals the final size; new cases for a
stream that ends cleanly, a read that times out and a parse error; a TS hook
size test; a failed-flush test.

Ref #1726

* feat(recorder): default to mesio when no downloader is configured

Rooms without a `downloader` setting followed the plugin hint, which is
stream-gears for almost every platform. They now use mesio: its FLV output
carries `onMetaData.keyframes` and every segment starts at timestamp 0, so
players can seek; it also records HEVC / Enhanced-FLV and HLS fMP4, which
stream-gears cannot. Only the hint mapping in `core/live.rs` changes; an
explicit `stream-gears` keeps using stream-gears, and plugins that hint
streamlink / yt-dlp are unaffected.

`DownloaderRuntime::from_type` no longer has a catch-all arm. `ffmpeg-external`
and `ffmpeg-internal` used to fall into it and silently run stream-gears; both
now run ffmpeg (external segmenting, same as `ffmpeg`). The arm for
streamlink / yt-dlp / ytarchive, which need the live stream and are built in
`downloader_runtime`, is listed explicitly and logs a warning.

WebUI, README, sample configs and CHANGELOG now say mesio is the default and
list the behaviour differences: the container follows the source (HLS fMP4 is
saved as .mp4), files are written under their final name without `.part`, and
Douyu Wangsu reconnects leave a 2.6-5.8 s overlap between segments.

Ref #1726

* feat(mesio): pass segment duration and size to the segment callback

mesio's segment-complete callback reports the duration and byte size of each
closed file, but biliup only logged them and built `SegmentInfo` with the path
and index. `SegmentInfo` gains `duration_secs` / `size_bytes` (None for the
other downloaders) and mesio fills them in. The end-to-end mesio test checks
the reported size equals the file size on disk.

Ref #1726

* fix(recorder): parse segment_time the same way in every downloader

`segment_time` was read three different ways. stream-gears indexed
`parts[1]` / `parts[2]` directly, so "3600" or "01:00" panicked the download
task (and an unparsable hour silently became 1); mesio returned 0 for anything
that was not HH:MM:SS and silently recorded without time splits; ffmpeg got
the raw string and accepted both forms.

`parse_segment_time` follows ffmpeg's duration syntax (`[HH:]MM:SS[.frac]`
or plain seconds; minutes and seconds below 60 when colons are used), so all
three downloaders now agree. stream-gears and mesio share
`DownloadConfig::segment_time_limit`: 0 means no time split for both (mesio
already did this; stream-gears used to split at every keyframe), and a value
that still cannot be parsed logs a warning and records without time splits
instead of panicking or being ignored silently. The time-range clamp uses the
same parser, so "3600" is shortened near the window end like "01:00:00".

Ref #1726

* feat(webui): enter file_size as a number with an MB/GB unit

The config still stores bytes. The control converts with 1024 (the same
sizes Windows Explorer shows), keeps an untouched value byte-exact even
when it is not a round MB/GB, and re-picks the unit when the form value
is replaced from outside.

In the streamer override, clearing a value that was already set now
saves null (no size split for that streamer); before, Semi dropped the
emptied key and the old value stayed. Clearing something typed in the
same session still falls back to inheriting the global setting.

* fix(webui): keep the file_size unit beside the number in narrow modals
2026-09-24 06:24:17 +00:00

285 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect } from 'react'
import { Form, Select, useFormApi } from '@douyinfe/semi-ui'
import PlatformPanel from './PlatformPanel'
type Props = {
entity: any
list: any
initValues?: Record<string, any>
bare?: boolean
}
const Bilibili: React.FC<Props> = props => {
const { entity, list, initValues, bare } = props
const formApi = useFormApi()
useEffect(() => {
if (initValues) {
Object.entries(initValues).forEach(([key, value]) => {
formApi.setValue(key, value)
})
}
}, [initValues, formApi])
return (
<>
<PlatformPanel header="哔哩哔哩" itemKey="bilibili" bare={bare}>
<Form.Select
allowCreate={true}
filter
field="bili_qn"
extraText={
<div style={{ fontSize: '14px' }}>
录制画质,默认原画。
<br />
刚开播时通常只有原画,会先录原画;下载插件为 ffmpeg / streamlink
时之后每次分段会重新取流并切到所选画质,stream-gears / mesio 整场沿用首次取到的流。
<br />
所选画质不存在时录 B 站返回的最接近的次档画质;开启「免登录原画」时录最高画质。
</div>
}
label="画质等级(bili_qn)"
placeholder="25000(原画)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
rules={[
{
pattern: /^\d*$/,
message: '请仅输入纯数字',
},
]}
showClear={true}
>
<Select.Option value={30000}>30000(杜比)</Select.Option>
<Select.Option value={20000}>20000(4k)</Select.Option>
<Select.Option value={10000}>10000(原画)</Select.Option>
<Select.Option value={401}>401(蓝光-杜比)</Select.Option>
<Select.Option value={400}>400(蓝光)</Select.Option>
<Select.Option value={250}>250(超清)</Select.Option>
<Select.Option value={150}>150(高清)</Select.Option>
<Select.Option value={80}>80(流畅)</Select.Option>
<Select.Option value={0}>0(最低画质)</Select.Option>
</Form.Select>
<Form.Switch
field="bilibili_danmaku"
extraText="录制哔哩哔哩弹幕,默认关闭。弹幕保存为与录像同名的 XML 文件,并随录像分段一起分段。"
label="录制弹幕(bilibili_danmaku)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Switch
field="bilibili_danmaku_detail"
extraText={
<div style={{ fontSize: '14px' }}>
弹幕文件中额外记录发送者昵称、UID,并保存醒目留言、上舰、礼物记录。默认关闭。
<br />
需先开启「录制弹幕」。<strong>实验性功能</strong>:可能与弹幕转 ASS 工具不兼容。
</div>
}
label="录制详细弹幕(bilibili_danmaku_detail)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Switch
field="bilibili_danmaku_raw"
extraText={
<div style={{ fontSize: '14px' }}>
额外保存 B 站服务器返回的原始弹幕数据,供有技术能力的用户做统计分析。默认关闭。
<br />
需先开启「录制弹幕」。<strong>实验性功能</strong>:开启后弹幕文件每 5
分钟才写入一次,且体积可能非常大。
</div>
}
label="录制完整弹幕(bilibili_danmaku_raw)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Input
field="user.bili_cookie"
extraText={
<div style={{ fontSize: '14px' }}>
按占位符格式填入 B 站登录 Cookie,推荐使用「
<a
href="https://github.com/biliup/biliup-rs"
title="「biliup-rs」 Github 项目主页"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--semi-color-link)' }}
>
biliup-rs
</a>
」获取。未登录时部分直播间只能录到较低画质。
</div>
}
placeholder="SESSDATA=none;bili_jct=none;DedeUserID__ckMd5=none;DedeUserID=none;"
label="哔哩哔哩 Cookie 文本(bili_cookie)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Select
field="user.bili_cookie_file"
label="哔哩哔哩 Cookie 文件(bili_cookie_file)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
optionList={list}
extraText="从已登录的 B 站账号中选择,只支持「biliup-rs」生成的 Cookie 文件。与上方「Cookie 文本」同时填写时,以 Cookie 文本为准。"
showClear={true}
/>
<Form.Select
field="bili_protocol"
extraText={
<div style={{ fontSize: '14px' }}>
直播流协议,默认 stream(FLV)。
<br />
选 hls_fmp4 时,开播后会先等待下方「hls_fmp4 转码等待时间」,仍拿不到 fmp4 流则本场回退为
FLV。
<br />
默认的下载插件 mesio 支持 hls_fmp4(保存为 .mp4);<strong>stream-gears 不支持 hls_fmp4</strong>,选了
stream-gears 的需改为 mesio、ffmpeg 或 streamlink。
</div>
}
label="直播流协议(bili_protocol)"
placeholder="stream(flv,默认)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
>
<Select.Option value="stream">stream(flv,默认)</Select.Option>
<Select.Option value="hls_fmp4">hls_fmp4</Select.Option>
</Form.Select>
<Form.Input
field="bili_liveapi"
extraText="获取直播流地址时优先使用的 API,默认官方 API。填入反代地址可获取指定区域(大陆或海外)的直播流。"
label="哔哩哔哩直播主要API(bili_liveapi)"
style={{ width: '100%' }}
placeholder="https://api.live.bilibili.com"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
rules={[
{
pattern:
/^https?:\/\/(?:[\w-]+(?::[\w-]+)?@)?([\w-]+\.)+[\w-]+(?::\d+)?(?:\/[\w-/.]*)?$/,
message: '请输入有效的API地址,必须以 http:// 或 https:// 开头',
},
]}
/>
<Form.Input
field="bili_fallback_api"
extraText={
<div style={{ fontSize: '14px' }}>
上方主要 API 不可用、受区域限制或没有所选协议的流时,改用此 API 重新获取。默认官方 API。
<br />
<strong>海外机器玩法</strong>:主要 API 填能取到大陆直播流的反代,回退 API 保持官方;直播流协议选
hls_fmp4,下载插件选 streamlink,直播 CDN 填 cn-gotcha204,ov-gotcha05。大主播即可走 cn204 的
fmp4 流稳定录制,没有 fmp4 流的小主播自动回退到 ov05 的 FLV 流。
</div>
}
label="哔哩哔哩直播回退API(bili_fallback_api)"
style={{ width: '100%' }}
placeholder="https://api.live.bilibili.com"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
rules={[
{
pattern:
/^https?:\/\/(?:[\w-]+(?::[\w-]+)?@)?([\w-]+\.)+[\w-]+(?::\d+)?(?:\/[\w-/.]*)?$/,
message: '请输入有效的API地址,必须以 http:// 或 https:// 开头',
},
]}
/>
<Form.TagInput
field="bili_cdn"
extraText="优先使用的 CDN 节点,默认不指定(使用 B 站返回的第一个节点)。可填多个,按先后顺序匹配;都匹配不上时仍用第一个节点。"
label="直播CDN(bili_cdn)"
placeholder="例: cn-gotcha204,ov-gotcha05。用英文逗号分隔以批量输入,失焦/Enter保存"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
rules={[
{
validator: (rule, value) => {
value = value ?? []
return Array.isArray(value) && value.every(item => /^(cn|ov)-gotcha\d+$/.test(item))
},
message: '例: cn-gotcha204,ov-gotcha05',
},
]}
/>
<Form.Switch
field="bili_cdn_fallback"
extraText={
<div style={{ fontSize: '14px' }}>
默认关闭。开启后会先探测选中的流地址能否下载,不可用时自动改用同一协议、同一画质下的其他 CDN
节点。
<br />
例:海外机器优选 ov-gotcha05,但该节点一直拉不到流,会自动回退到 ov-gotcha07。
</div>
}
label="CDN 回退(bili_cdn_fallback)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Switch
field="bili_anonymous_origin"
extraText={
<div style={{ fontSize: '14px' }}>
默认关闭。开启后改从 master playlist 接口获取 hls_fmp4 原画流,<strong>不登录也能录到原画</strong>。
<br />
仅在直播流协议为 hls_fmp4 时生效;特殊类型直播间(如付费直播)未填 Cookie 时不走此通道,按普通方式取流。
</div>
}
label="免登录原画(bili_anonymous_origin)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.InputNumber
field="bili_hls_transcode_timeout"
extraText="直播流协议为 hls_fmp4 时,从 B 站记录的开播时间起最多等待这么多秒让 fmp4 流生成;超时仍没有则本场回退为 FLV 流。单位:秒,默认 60。"
label="hls_fmp4 转码等待时间(bili_hls_transcode_timeout)"
style={{ width: '100%' }}
placeholder="60"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
</PlatformPanel>
</>
)
}
export default Bilibili