Files
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

417 lines
17 KiB
TypeScript
Raw Permalink 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 from 'react'
import styles from '../../styles/dashboard.module.scss'
import SectionTitle from '../../(app)/components/SectionTitle'
import { Form, Select, Space, useFormState } from '@douyinfe/semi-ui'
import { IconUpload, IconDownload } from '@douyinfe/semi-icons'
import { FileSizeField } from '../FileSizeInput'
const Global: React.FC = () => {
// useFormApi 不订阅表单值变化,切换下拉框后条件渲染不会刷新;useFormState 会
const { values } = useFormState()
const isSyncDownloader = values?.downloader === 'sync-downloader'
return (
<>
{/* 全局下载 */}
<div className={styles.frameDownload}>
<SectionTitle icon={<IconDownload size="small" />} title="全局下载设置" />
<Form.Select
label="下载插件(downloader)"
field="downloader"
placeholder="mesio(默认)"
extraText={
<div style={{ fontSize: '14px' }}>
全局默认的下载插件,可在单个主播的覆写设置里另选。可选:
<br />
1. <strong>mesio</strong>(默认):内置 rust-srec 引擎,无需额外安装。进程内下载 FLV / HLS,修复时间戳(每段从
0 开始)、写入关键帧索引(onMetaData.keyframes),支持 HEVC 和 hls_fmp4;不转封装,按源站的容器保存(FLV /
TS,hls_fmp4 存为 .mp4)。详见{' '}
<a
href="https://github.com/hua0512/rust-srec"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--semi-color-link)' }}
>
项目主页
</a>
。
<br />
2. ffmpeg:非 Docker 用户需自行安装 FFmpeg。
<br />
3. streamlink:多线程下载 HLS 分片,也可下载 FLV 直链;需系统中有 streamlink 命令。
<br />
4. sync-downloader(边录边传):录制的同时流式上传,<strong>需先为主播设置上传模板</strong>;不受
pool2 / threads / segment_time 控制,固定 3 线程上传,请确保上传带宽充足;非 Docker 用户需自行安装
FFmpeg。详见 Wiki{' '}
<a
href="https://github.com/biliup/biliup/wiki/%E8%BE%B9%E5%BD%95%E8%BE%B9%E4%BC%A0%E5%8A%9F%E8%83%BD"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--semi-color-link)' }}
>
边录边传功能
</a>
。
<br />
5. ytarchive:仅适用于 YouTube 直播。
<br />
6. stream-gears:内置,无需额外安装,可防 FLV 流花屏;不支持 HEVC 编码和 hls_fmp4 流,FLV
时间戳沿用源站的原值(不从 0 开始)。
</div>
}
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
>
<Select.Option value="streamlink">streamlink(hls多线程下载)</Select.Option>
<Select.Option value="ffmpeg">ffmpeg</Select.Option>
<Select.Option value="stream-gears">stream-gears</Select.Option>
<Select.Option value="sync-downloader">sync-downloader(边录边传)</Select.Option>
<Select.Option value="ytarchive">ytarchive(仅适用于 Youtube Live)</Select.Option>
<Select.Option value="mesio">mesio(默认)</Select.Option>
</Form.Select>
{isSyncDownloader ? (
<>
<Form.Input
field="sync_save_dir"
label="边录边传额外保存本地目录(sync_save_dir)"
placeholder=""
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
rules={[
{
pattern: /^[^*|?"<>]*$/,
message: '路径中不能包含Windows不允许的字符 * | ? " < >',
},
{
pattern: /^(?![a-zA-Z]:).*$/,
message: '以字母开头时,第二个字符不能是中文冒号',
},
{
pattern: /^[^:]*$|^[a-zA-Z]:[\/\\][^:]*$/,
message: '冒号只能出现在第二个字符位置,且后面必须连接斜杠',
},
{
pattern: /^(?!.*?\.{3,})(?!.*?\.{2}(?![\/\\])).*$/,
message: '点号最多只能连续出现两次,且后面必须连接斜杠',
},
{
pattern: /^(?!.*\/\\)(?!.*\\\/).*$/,
message: '不允许连接正反斜杠',
},
{
pattern: /^(?!.*([\\]{3,}|[\/]{2,})).*$/,
message: '反斜杠最多只能连续出现两次,正斜杠最多只能连续出现一次',
},
]}
stopValidateWithError={true}
/>
</>
) : null}
<FileSizeField
label="视频分段大小(file_size)"
extraText={
<div style={{ fontSize: '14px' }}>
录像单文件大小上限,超过后开始写下一个文件。下载回放时无法使用。留空表示不按大小分段。
<br />
按 1024 进制换算:1 GB = 1024 MB = 1073741824 字节,与 Windows 资源管理器显示的大小一致。配置文件里存的仍是字节数。
</div>
}
field="file_size"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Input
field="segment_time"
extraText={
<div style={{ fontSize: '14px' }}>
录像单文件时间限制,超过此时长触发文件分割。
<br />
格式:&apos;00:00:00&apos;(时:分:秒)
</div>
}
label="视频分段时长(segment_time)"
placeholder="01:00:00"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
rules={[
{
pattern: /^[^:]*$/,
message: '请使用英文冒号',
},
{
pattern: /^[0-9:]*$/,
message: '只接受数字和英文冒号',
},
{
pattern: /^$|^[0-9]{2,4}:[0-5][0-9]:[0-5][0-9]$/,
message: '分或秒不符合规范',
},
]}
stopValidateWithError={true}
/>
<Form.Input
field="filename_prefix"
extraText={
<div style={{ fontSize: '14px' }}>
全局文件名模板。可被单个主播文件名模板覆盖。可用变量如下
<br />
{'\u007B'}streamer{'\u007D'}: 录播备注(必须保留)
<span style={{ margin: '0 20px' }}></span>
{'\u007B'}title{'\u007D'}: 直播标题
<br />
%Y-%m-%d %H_%M_%S: 开始录制时的 年-月-日 时_分_秒
</div>
}
label="文件名模板(filename_prefix)"
placeholder="{streamer}%Y-%m-%dT%H_%M_%S"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.Switch
field="segment_processor_parallel"
extraText={<div style={{ fontSize: '14px' }}>开启后无法保证分段后处理先后执行顺序</div>}
label="视频分段后处理并行(segment_processor_parallel)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.InputNumber
field="filtering_threshold"
extraText={
<div style={{ fontSize: '14px' }}>
小于此大小的视频文件将会被过滤删除。
<br />
单位:MB
</div>
}
label="碎片过滤(filtering_threshold)"
suffix={'MB'}
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.InputNumber
field="delay"
label="下播延迟检测(delay)"
extraText={
<div style={{ fontSize: '14px' }}>
当检测到主播下播后,延迟一定时间再次检测确认,避免特殊情况提早启动上传导致分稿件。
<br />
单位:秒
<br />
默认延迟时间为 0 秒
</div>
}
placeholder="0"
suffix="s"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.InputNumber
field="event_loop_interval"
extraText={
<div style={{ fontSize: '14px' }}>
单个主播检测间隔时间,单位:秒。比如虎牙有10个主播,每个主播会间隔10秒检测
<br />
单位:秒
</div>
}
label="直播事件检测间隔(event_loop_interval)"
suffix="s"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.InputNumber
field="pool1_size"
extraText="负责下载事件的线程池大小,用于限制最大同时录制数。"
label="下载线程池大小(pool1_size)"
placeholder={5}
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.Select
field="preview_transport"
label="直播预览取流方式(preview_transport)"
extraText={
<div style={{ fontSize: '14px' }}>
<div>
<strong>经 biliup 中转</strong>(默认):页面里的预览复用正在录制的那一路流,不向直播平台多拉一路。
浏览器与 biliup 在同一台机器或同一内网时选这个,不多占 CDN 带宽。
</div>
<div>
<strong>浏览器直连 CDN</strong>:biliup 向平台另取一条直链(新 token,不影响录制那条),浏览器自己去
CDN 拉,媒体流量不经过 biliup,适合 biliup 部署在异地服务器、浏览器远程访问的情况,省服务器出口带宽。
只有 CDN 放行跨域的平台能直连(B 站 / 抖音 / 斗鱼 / 虎牙,FLV 与 HLS 都行);Twitch 等按 Origin 白名单放行
的平台在直连模式下自动回落中转并在播放器角标标出原因。
</div>
</div>
}
placeholder="经 biliup 中转(relay)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
>
<Form.Select.Option value="relay">经 biliup 中转(relay)</Form.Select.Option>
<Form.Select.Option value="direct">浏览器直连 CDN(direct)</Form.Select.Option>
</Form.Select>
</div>
<Space />
{/* 全局上传 */}
<div className={styles.frameUpload}>
<SectionTitle icon={<IconUpload size="small" />} title="全局上传设置" />
<Form.Select
field="submit_api"
label="提交接口(submit_api)"
extraText="B站投稿提交接口,默认为自动选择。"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
>
<Form.Select.Option value="app">安卓APP(app)</Form.Select.Option>
<Form.Select.Option value="b-cut-android">BCut安卓APP(b-cut-android)</Form.Select.Option>
<Form.Select.Option value="web">网页(web)</Form.Select.Option>
</Form.Select>
<Form.Select
field="uploader"
label="上传插件(uploader)"
extraText="全局默认上传插件选择。"
placeholder="biliup-rs"
noLabel={true}
style={{ width: '100%', display: 'none' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
initValue="Noop"
>
<Form.Select.Option value="bili_web">bili_web</Form.Select.Option>
<Form.Select.Option value="biliup-rs">biliup-rs</Form.Select.Option>
<Form.Select.Option value="Noop">Noop(即不上传,但会执行后处理)</Form.Select.Option>
</Form.Select>
<Form.Select
field="lines"
label="上传线路(lines)"
extraText="b站上传线路选择,默认为自动模式,可手动切换为alia, bda2, bldsa, tx, txa, estx, akbd"
placeholder="AUTO(自动,默认)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
>
<Form.Select.Option value="AUTO">AUTO(自动,默认)</Form.Select.Option>
<Form.Select.Option value="alia">alia(海外-阿里云)</Form.Select.Option>
<Form.Select.Option value="bda2">bda2(大陆-百度云)</Form.Select.Option>
<Form.Select.Option value="bldsa">bldsa(大陆-B站自建)</Form.Select.Option>
<Form.Select.Option value="tx">tx(大陆-腾讯云)</Form.Select.Option>
<Form.Select.Option value="txa">txa(海外-腾讯云)</Form.Select.Option>
<Form.Select.Option value="estx">estx(大陆-B站自建)</Form.Select.Option>
<Form.Select.Option value="akbd">akbd(大陆-B站自建)</Form.Select.Option>
</Form.Select>
<Form.InputNumber
field="threads"
placeholder={3}
extraText="单文件并发上传数,未达到带宽上限时,增大此值可提高上传速度(不要设置过大,部分线路限制为8,如速度不佳优先调整上传线路)"
label="上传并发(threads)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.InputNumber
field="max_upload_limit"
placeholder={8}
extraText="录播上传次数上限,防止因意外情况如B站接口抽风、录播本身损坏导致录播反复上传浪费宽带或被B站风控(注:限制是记录在程序上下文中的,重启程序会重置上传次数限制;且为了保证尽量不改动老用户使用逻辑,默认将此值设置为一个较大的值,一般推荐设置为2-3)"
label="上传重试次数限制(max_upload_limit)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
showClear={true}
/>
<Form.InputNumber
field="pool2_size"
extraText={
<div style={{ fontSize: '14px' }}>负责上传事件的线程池大小。根据实际带宽设置。</div>
}
placeholder={3}
label="上传线程池大小(pool2_size)"
style={{ width: '100%' }}
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
<Form.Switch
field="use_live_cover"
extraText={
<div style={{ fontSize: '14px' }}>
使用直播间封面作为投稿封面。此封面优先级低于单个主播指定的自定义封面,保存于cover文件夹下,上传后自动删除。
<br />
目前支持平台:哔哩哔哩,克拉克拉,Twitch,YouTube。
</div>
}
label="使用直播间封面作为投稿封面(use_live_cover)"
fieldStyle={{
alignSelf: 'stretch',
padding: 0,
}}
/>
</div>
</>
)
}
export default Global