@bililive-tools/manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +91 -0
- package/lib/api.d.ts +1 -0
- package/lib/api.js +21 -0
- package/lib/api.js.map +1 -0
- package/lib/common.d.ts +55 -0
- package/lib/common.js +4 -0
- package/lib/common.js.map +1 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +61 -0
- package/lib/index.js.map +1 -0
- package/lib/manager.d.ts +92 -0
- package/lib/manager.js +256 -0
- package/lib/manager.js.map +1 -0
- package/lib/record_extra_data_controller.d.ts +28 -0
- package/lib/record_extra_data_controller.js +157 -0
- package/lib/record_extra_data_controller.js.map +1 -0
- package/lib/recorder.d.ts +118 -0
- package/lib/recorder.js +2 -0
- package/lib/recorder.js.map +1 -0
- package/lib/streamManager.d.ts +30 -0
- package/lib/streamManager.js +119 -0
- package/lib/streamManager.js.map +1 -0
- package/lib/utils.d.ts +62 -0
- package/lib/utils.js +232 -0
- package/lib/utils.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Intro
|
|
2
|
+
|
|
3
|
+
原项目:https://github.com/WhiteMinds/LiveAutoRecord
|
|
4
|
+
|
|
5
|
+
这是 [biliLive-tools](https://github.com/renmu123/biliLive-tools) 的一个平台插件,支持管理录播
|
|
6
|
+
|
|
7
|
+
# 安装
|
|
8
|
+
|
|
9
|
+
`npm i @bililive-tools/manager`
|
|
10
|
+
|
|
11
|
+
## 支持的平台
|
|
12
|
+
|
|
13
|
+
| 平台 | 包名 |
|
|
14
|
+
| ---- | ----------------------------------- |
|
|
15
|
+
| B站 | `@bililive-tools/bilibili-recorder` |
|
|
16
|
+
| 斗鱼 | `@bililive-tools/douyu-recorder` |
|
|
17
|
+
| 虎牙 | `@bililive-tools/huya-recorder` |
|
|
18
|
+
|
|
19
|
+
# 使用
|
|
20
|
+
|
|
21
|
+
以B站举例
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createRecorderManager } from "@bililive-tools/manager";
|
|
25
|
+
import { provider } from "@bililive-tools/bilibili-recorder";
|
|
26
|
+
|
|
27
|
+
const manager = createRecorderManager({
|
|
28
|
+
providers: [provider],
|
|
29
|
+
savePathRule: "D:\\录制\\{platforme}}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}", // 保存路径,占位符见文档
|
|
30
|
+
autoCheckInterval: 1000 * 60, // 自动检查间隔,单位秒
|
|
31
|
+
autoRemoveSystemReservedChars: true, // 移除系统非法字符串
|
|
32
|
+
biliBatchQuery: false, // B站检查使用批量接口
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 不同provider支持的参数不尽相同,具体见相关文档
|
|
36
|
+
|
|
37
|
+
manager.addRecorder({
|
|
38
|
+
providerId: provider.id,
|
|
39
|
+
channelId: "7734200",
|
|
40
|
+
quality: 10000,
|
|
41
|
+
streamPriorities: [],
|
|
42
|
+
sourcePriorities: [],
|
|
43
|
+
});
|
|
44
|
+
manager.startCheckLoop();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## savePathRule 占位符参数
|
|
48
|
+
|
|
49
|
+
| 值 | 标签 |
|
|
50
|
+
| ----------- | ------ |
|
|
51
|
+
| {platform} | 平台 |
|
|
52
|
+
| {channelId} | 房间号 |
|
|
53
|
+
| {remarks} | 备注 |
|
|
54
|
+
| {owner} | 主播名 |
|
|
55
|
+
| {title} | 标题 |
|
|
56
|
+
| {year} | 年 |
|
|
57
|
+
| {month} | 月 |
|
|
58
|
+
| {date} | 日 |
|
|
59
|
+
| {hour} | 时 |
|
|
60
|
+
| {min} | 分 |
|
|
61
|
+
| {sec} | 秒 |
|
|
62
|
+
|
|
63
|
+
## 事件
|
|
64
|
+
|
|
65
|
+
### RecordStart
|
|
66
|
+
|
|
67
|
+
录制开始
|
|
68
|
+
|
|
69
|
+
### RecordStop
|
|
70
|
+
|
|
71
|
+
录制结束
|
|
72
|
+
|
|
73
|
+
### error
|
|
74
|
+
|
|
75
|
+
错误
|
|
76
|
+
|
|
77
|
+
### RecorderDebugLog
|
|
78
|
+
|
|
79
|
+
录制相关的log
|
|
80
|
+
|
|
81
|
+
### videoFileCreated
|
|
82
|
+
|
|
83
|
+
录制文件开始,如果开启分段,分段时会触发这两个事件,可以用来实现webhook
|
|
84
|
+
|
|
85
|
+
### videoFileCompleted
|
|
86
|
+
|
|
87
|
+
录制文件结束
|
|
88
|
+
|
|
89
|
+
# 协议
|
|
90
|
+
|
|
91
|
+
与原项目保存一致为 LGPL
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getBiliStatusInfoByUIDs<UID extends number>(userIds: UID[]): Promise<Record<string, boolean>>;
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { assert } from "./utils.js";
|
|
3
|
+
const requester = axios.create({
|
|
4
|
+
timeout: 10e3,
|
|
5
|
+
// axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,
|
|
6
|
+
// 但会导致请求报错 "Client network socket disconnected before secure TLS connection was established"。
|
|
7
|
+
proxy: false,
|
|
8
|
+
});
|
|
9
|
+
export async function getBiliStatusInfoByUIDs(userIds) {
|
|
10
|
+
const res = await requester.get("http://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids", {
|
|
11
|
+
params: { uids: userIds },
|
|
12
|
+
});
|
|
13
|
+
assert(res.data.code === 0, `Unexpected resp, code ${res.data.code}, msg ${res.data.message}`);
|
|
14
|
+
const obj = {};
|
|
15
|
+
for (const uid of userIds) {
|
|
16
|
+
const data = res.data.data?.[uid];
|
|
17
|
+
obj[uid] = data?.live_status === 1;
|
|
18
|
+
}
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=api.js.map
|
package/lib/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,IAAI;IACb,kDAAkD;IAClD,8FAA8F;IAC9F,KAAK,EAAE,KAAK;CACb,CAAC,CAAC;AAiBH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAqB,OAAc;IAC9E,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAiB7B,mEAAmE,EAAE;QACrE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;KAC1B,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,yBAAyB,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAE/F,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;QAClC,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,WAAW,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/lib/common.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AnyObject, UnknownObject } from "./utils.js";
|
|
2
|
+
export type ChannelId = string;
|
|
3
|
+
export declare const Qualities: readonly ["lowest", "low", "medium", "high", "highest"];
|
|
4
|
+
export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
|
|
5
|
+
export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
|
|
6
|
+
export type Quality = (typeof Qualities)[number] | (typeof BiliQualities)[number] | (typeof DouyuQualities)[number];
|
|
7
|
+
export interface MessageSender<E extends AnyObject = UnknownObject> {
|
|
8
|
+
uid?: string;
|
|
9
|
+
name: string;
|
|
10
|
+
avatar?: string;
|
|
11
|
+
extra?: E;
|
|
12
|
+
}
|
|
13
|
+
export interface Comment<E extends AnyObject = UnknownObject> {
|
|
14
|
+
type: "comment";
|
|
15
|
+
timestamp: number;
|
|
16
|
+
text: string;
|
|
17
|
+
mode?: number;
|
|
18
|
+
color?: string;
|
|
19
|
+
sender?: MessageSender;
|
|
20
|
+
extra?: E;
|
|
21
|
+
}
|
|
22
|
+
export interface GiveGift<E extends AnyObject = UnknownObject> {
|
|
23
|
+
type: "give_gift";
|
|
24
|
+
timestamp: number;
|
|
25
|
+
name: string;
|
|
26
|
+
count: number;
|
|
27
|
+
price: number;
|
|
28
|
+
text?: string;
|
|
29
|
+
cost?: number;
|
|
30
|
+
color?: string;
|
|
31
|
+
sender?: MessageSender;
|
|
32
|
+
extra?: E;
|
|
33
|
+
}
|
|
34
|
+
export interface Guard<E extends AnyObject = UnknownObject> {
|
|
35
|
+
type: "guard";
|
|
36
|
+
timestamp: number;
|
|
37
|
+
name: string;
|
|
38
|
+
count: number;
|
|
39
|
+
price: number;
|
|
40
|
+
level: number;
|
|
41
|
+
text?: string;
|
|
42
|
+
cost?: number;
|
|
43
|
+
color?: string;
|
|
44
|
+
sender?: MessageSender;
|
|
45
|
+
extra?: E;
|
|
46
|
+
}
|
|
47
|
+
export interface SuperChat<E extends AnyObject = UnknownObject> {
|
|
48
|
+
type: "super_chat";
|
|
49
|
+
timestamp: number;
|
|
50
|
+
text: string;
|
|
51
|
+
price: number;
|
|
52
|
+
sender?: MessageSender;
|
|
53
|
+
extra?: E;
|
|
54
|
+
}
|
|
55
|
+
export type Message = Comment | GiveGift | SuperChat | Guard;
|
package/lib/common.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAU,CAAC;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAU,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ffmpeg from "@renmu/fluent-ffmpeg";
|
|
2
|
+
import { RecorderProvider } from "./manager.js";
|
|
3
|
+
import { SerializedRecorder, Recorder, RecordHandle } from "./recorder.js";
|
|
4
|
+
import { AnyObject } from "./utils.js";
|
|
5
|
+
import utils from "./utils.js";
|
|
6
|
+
export * from "./common.js";
|
|
7
|
+
export * from "./recorder.js";
|
|
8
|
+
export * from "./manager.js";
|
|
9
|
+
export * from "./record_extra_data_controller.js";
|
|
10
|
+
export { utils };
|
|
11
|
+
/**
|
|
12
|
+
* 提供一些 utils
|
|
13
|
+
*/
|
|
14
|
+
export declare function defaultFromJSON<E extends AnyObject>(provider: RecorderProvider<E>, json: SerializedRecorder<E>): Recorder<E>;
|
|
15
|
+
export declare function defaultToJSON<E extends AnyObject>(provider: RecorderProvider<E>, recorder: Recorder<E>): SerializedRecorder<E>;
|
|
16
|
+
export declare function genRecorderUUID(): Recorder["id"];
|
|
17
|
+
export declare function genRecordUUID(): RecordHandle["id"];
|
|
18
|
+
export declare function setFFMPEGPath(newPath: string): void;
|
|
19
|
+
export declare const createFFMPEGBuilder: (input?: string | import("stream").Readable | undefined, options?: ffmpeg.FfmpegCommandOptions | undefined) => ffmpeg.FfmpegCommand;
|
|
20
|
+
export declare function getDataFolderPath<E extends AnyObject>(provider: RecorderProvider<E>): string;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// forked from https://github.com/WhiteMinds/LiveAutoRecord
|
|
2
|
+
import ffmpeg from "@renmu/fluent-ffmpeg";
|
|
3
|
+
import { omit, pick } from "lodash-es";
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
import utils from "./utils.js";
|
|
6
|
+
export * from "./common.js";
|
|
7
|
+
export * from "./recorder.js";
|
|
8
|
+
export * from "./manager.js";
|
|
9
|
+
export * from "./record_extra_data_controller.js";
|
|
10
|
+
export { utils };
|
|
11
|
+
/**
|
|
12
|
+
* 提供一些 utils
|
|
13
|
+
*/
|
|
14
|
+
export function defaultFromJSON(provider, json) {
|
|
15
|
+
return provider.createRecorder(omit(json, ["providerId"]));
|
|
16
|
+
}
|
|
17
|
+
export function defaultToJSON(provider, recorder) {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
return {
|
|
20
|
+
providerId: provider.id,
|
|
21
|
+
...pick(recorder, [
|
|
22
|
+
"id",
|
|
23
|
+
"channelId",
|
|
24
|
+
"owner",
|
|
25
|
+
"remarks",
|
|
26
|
+
"disableAutoCheck",
|
|
27
|
+
"quality",
|
|
28
|
+
"streamPriorities",
|
|
29
|
+
"sourcePriorities",
|
|
30
|
+
"extra",
|
|
31
|
+
"segment",
|
|
32
|
+
"saveSCDanma",
|
|
33
|
+
"saveCover",
|
|
34
|
+
"saveGiftDanma",
|
|
35
|
+
"disableProvideCommentsWhenRecording",
|
|
36
|
+
"liveInfo",
|
|
37
|
+
"uid",
|
|
38
|
+
]),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// 目前是假设使用环境的规模都比较小,不太容易遇到性能问题,所以用 string uuid 作为 id 来简化开发的复杂度。
|
|
42
|
+
// 后面如果需要再高一些的规模,可以优化成分布式 id 生成器,或者其他的异步生成 id 的方案。
|
|
43
|
+
export function genRecorderUUID() {
|
|
44
|
+
return uuid();
|
|
45
|
+
}
|
|
46
|
+
export function genRecordUUID() {
|
|
47
|
+
return uuid();
|
|
48
|
+
}
|
|
49
|
+
let ffmpegPath = "ffmpeg";
|
|
50
|
+
export function setFFMPEGPath(newPath) {
|
|
51
|
+
ffmpegPath = newPath;
|
|
52
|
+
}
|
|
53
|
+
export const createFFMPEGBuilder = (...args) => {
|
|
54
|
+
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
55
|
+
return ffmpeg(...args);
|
|
56
|
+
};
|
|
57
|
+
export function getDataFolderPath(provider) {
|
|
58
|
+
// TODO: 改成 AppData 之类的目录
|
|
59
|
+
return "./" + provider.id;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,MAAM,MAAM,sBAAsB,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,EAAE,IAAI,IAAI,EAAE,MAAM,MAAM,CAAC;AAIlC,OAAO,KAAK,MAAM,YAAY,CAAC;AAE/B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mCAAmC,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,CAAC;AAEjB;;GAEG;AAEH,MAAM,UAAU,eAAe,CAC7B,QAA6B,EAC7B,IAA2B;IAE3B,OAAO,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,QAA6B,EAC7B,QAAqB;IAErB,aAAa;IACb,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,EAAE;QACvB,GAAG,IAAI,CAAC,QAAQ,EAAE;YAChB,IAAI;YACJ,WAAW;YACX,OAAO;YACP,SAAS;YACT,kBAAkB;YAClB,SAAS;YACT,kBAAkB;YAClB,kBAAkB;YAClB,OAAO;YACP,SAAS;YACT,aAAa;YACb,WAAW;YACX,eAAe;YACf,qCAAqC;YACrC,UAAU;YACV,KAAK;SACN,CAAC;KACH,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,kDAAkD;AAClD,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC;AACD,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,EAAE,CAAC;AAChB,CAAC;AAED,IAAI,UAAU,GAAW,QAAQ,CAAC;AAClC,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,UAAU,GAAG,OAAO,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAA+B,EAAE,EAAE;IACxE,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACjC,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAsB,QAA6B;IAClF,yBAAyB;IACzB,OAAO,IAAI,GAAG,QAAQ,CAAC,EAAE,CAAC;AAC5B,CAAC"}
|
package/lib/manager.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Emitter } from "mitt";
|
|
2
|
+
import { ChannelId, Message } from "./common.js";
|
|
3
|
+
import { RecorderCreateOpts, Recorder, SerializedRecorder, RecordHandle, DebugLog } from "./recorder.js";
|
|
4
|
+
import { AnyObject, UnknownObject } from "./utils.js";
|
|
5
|
+
import { StreamManager } from "./streamManager.js";
|
|
6
|
+
export interface RecorderProvider<E extends AnyObject> {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
siteURL: string;
|
|
10
|
+
matchURL: (this: RecorderProvider<E>, channelURL: string) => boolean;
|
|
11
|
+
resolveChannelInfoFromURL: (this: RecorderProvider<E>, channelURL: string) => Promise<{
|
|
12
|
+
id: ChannelId;
|
|
13
|
+
title: string;
|
|
14
|
+
owner: string;
|
|
15
|
+
uid?: number;
|
|
16
|
+
} | null>;
|
|
17
|
+
createRecorder: (this: RecorderProvider<E>, opts: Omit<RecorderCreateOpts<E>, "providerId">) => Recorder<E>;
|
|
18
|
+
fromJSON: <T extends SerializedRecorder<E>>(this: RecorderProvider<E>, json: T) => Recorder<E>;
|
|
19
|
+
setFFMPEGOutputArgs: (this: RecorderProvider<E>, args: string[]) => void;
|
|
20
|
+
}
|
|
21
|
+
declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckLiveStatusAndRecord", "autoCheckInterval", "ffmpegOutputArgs", "biliBatchQuery"];
|
|
22
|
+
type ConfigurableProp = (typeof configurableProps)[number];
|
|
23
|
+
export interface RecorderManager<ME extends UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> extends Emitter<{
|
|
24
|
+
error: {
|
|
25
|
+
source: string;
|
|
26
|
+
err: unknown;
|
|
27
|
+
};
|
|
28
|
+
RecordStart: {
|
|
29
|
+
recorder: Recorder<E>;
|
|
30
|
+
recordHandle: RecordHandle;
|
|
31
|
+
};
|
|
32
|
+
RecordSegment: {
|
|
33
|
+
recorder: Recorder<E>;
|
|
34
|
+
recordHandle?: RecordHandle;
|
|
35
|
+
};
|
|
36
|
+
videoFileCreated: {
|
|
37
|
+
recorder: Recorder<E>;
|
|
38
|
+
filename: string;
|
|
39
|
+
};
|
|
40
|
+
videoFileCompleted: {
|
|
41
|
+
recorder: Recorder<E>;
|
|
42
|
+
filename: string;
|
|
43
|
+
};
|
|
44
|
+
RecordStop: {
|
|
45
|
+
recorder: Recorder<E>;
|
|
46
|
+
recordHandle: RecordHandle;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
|
49
|
+
Message: {
|
|
50
|
+
recorder: Recorder<E>;
|
|
51
|
+
message: Message;
|
|
52
|
+
};
|
|
53
|
+
RecorderUpdated: {
|
|
54
|
+
recorder: Recorder<E>;
|
|
55
|
+
keys: (string | keyof Recorder<E>)[];
|
|
56
|
+
};
|
|
57
|
+
RecorderAdded: Recorder<E>;
|
|
58
|
+
RecorderRemoved: Recorder<E>;
|
|
59
|
+
RecorderDebugLog: DebugLog & {
|
|
60
|
+
recorder: Recorder<E>;
|
|
61
|
+
};
|
|
62
|
+
Updated: ConfigurableProp[];
|
|
63
|
+
}> {
|
|
64
|
+
providers: P[];
|
|
65
|
+
getChannelURLMatchedRecorderProviders: (this: RecorderManager<ME, P, PE, E>, channelURL: string) => P[];
|
|
66
|
+
recorders: Recorder<E>[];
|
|
67
|
+
addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
|
|
68
|
+
removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
|
|
69
|
+
startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
70
|
+
stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
71
|
+
autoCheckLiveStatusAndRecord: boolean;
|
|
72
|
+
autoCheckInterval: number;
|
|
73
|
+
isCheckLoopRunning: boolean;
|
|
74
|
+
startCheckLoop: (this: RecorderManager<ME, P, PE, E>) => void;
|
|
75
|
+
stopCheckLoop: (this: RecorderManager<ME, P, PE, E>) => void;
|
|
76
|
+
savePathRule: string;
|
|
77
|
+
autoRemoveSystemReservedChars: boolean;
|
|
78
|
+
ffmpegOutputArgs: string;
|
|
79
|
+
/** b站使用批量查询接口 */
|
|
80
|
+
biliBatchQuery: boolean;
|
|
81
|
+
}
|
|
82
|
+
export type RecorderManagerCreateOpts<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> = Partial<Pick<RecorderManager<ME, P, PE, E>, ConfigurableProp>> & {
|
|
83
|
+
providers: P[];
|
|
84
|
+
};
|
|
85
|
+
export declare function createRecorderManager<ME extends AnyObject = UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE>(opts: RecorderManagerCreateOpts<ME, P, PE, E>): RecorderManager<ME, P, PE, E>;
|
|
86
|
+
export declare function genSavePathFromRule<ME extends AnyObject, P extends RecorderProvider<AnyObject>, PE extends AnyObject, E extends AnyObject>(manager: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>, extData: {
|
|
87
|
+
owner: string;
|
|
88
|
+
title: string;
|
|
89
|
+
startTime?: number;
|
|
90
|
+
}): string;
|
|
91
|
+
export type GetProviderExtra<P> = P extends RecorderProvider<infer E> ? E : never;
|
|
92
|
+
export { StreamManager };
|
package/lib/manager.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import mitt from "mitt";
|
|
3
|
+
import { omit, range } from "lodash-es";
|
|
4
|
+
import { parseArgsStringToArgv } from "string-argv";
|
|
5
|
+
import { getBiliStatusInfoByUIDs } from "./api.js";
|
|
6
|
+
import { formatDate, removeSystemReservedChars, formatTemplate, } from "./utils.js";
|
|
7
|
+
import { StreamManager } from "./streamManager.js";
|
|
8
|
+
const configurableProps = [
|
|
9
|
+
"savePathRule",
|
|
10
|
+
"autoRemoveSystemReservedChars",
|
|
11
|
+
"autoCheckLiveStatusAndRecord",
|
|
12
|
+
"autoCheckInterval",
|
|
13
|
+
"ffmpegOutputArgs",
|
|
14
|
+
"biliBatchQuery",
|
|
15
|
+
];
|
|
16
|
+
function isConfigurableProp(prop) {
|
|
17
|
+
return configurableProps.includes(prop);
|
|
18
|
+
}
|
|
19
|
+
export function createRecorderManager(opts) {
|
|
20
|
+
const recorders = [];
|
|
21
|
+
let checkLoopTimer;
|
|
22
|
+
const multiThreadCheck = async (manager) => {
|
|
23
|
+
const handleBatchQuery = async (obj) => {
|
|
24
|
+
for (const recorder of recorders) {
|
|
25
|
+
if (recorder.extra.recorderUid == null)
|
|
26
|
+
continue;
|
|
27
|
+
const isLive = obj[recorder.extra.recorderUid];
|
|
28
|
+
if (isLive) {
|
|
29
|
+
await recorder.checkLiveStatusAndRecord({
|
|
30
|
+
getSavePath(data) {
|
|
31
|
+
return genSavePathFromRule(manager, recorder, data);
|
|
32
|
+
},
|
|
33
|
+
banLiveId: tempBanObj[recorder.channelId],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const maxThreadCount = 3;
|
|
39
|
+
// 这里暂时不打算用 state == recording 来过滤,provider 必须内部自己处理录制过程中的 check,
|
|
40
|
+
// 这样可以防止一些意外调用 checkLiveStatusAndRecord 时出现重复录制。
|
|
41
|
+
let needCheckRecorders = recorders.filter((r) => !r.disableAutoCheck);
|
|
42
|
+
let threads = [];
|
|
43
|
+
if (manager.biliBatchQuery) {
|
|
44
|
+
const biliNeedCheckRecorders = needCheckRecorders.filter((r) => r.providerId === "Bilibili" && r.extra?.recorderUid);
|
|
45
|
+
needCheckRecorders = needCheckRecorders.filter((r) => {
|
|
46
|
+
if (r.providerId !== "Bilibili")
|
|
47
|
+
return true;
|
|
48
|
+
if (r.providerId === "Bilibili" && !r.extra?.recorderUid)
|
|
49
|
+
return true;
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
const uids = biliNeedCheckRecorders.map((r) => r.extra?.recorderUid);
|
|
53
|
+
// console.log("uids", uids);
|
|
54
|
+
try {
|
|
55
|
+
if (uids.length !== 0) {
|
|
56
|
+
const biliStatus = await getBiliStatusInfoByUIDs(uids);
|
|
57
|
+
// console.log("biliStatus", biliStatus);
|
|
58
|
+
threads.push(handleBatchQuery(biliStatus));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
manager.emit("error", { source: "getBiliStatusInfoByUIDs", err });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const checkOnce = async () => {
|
|
66
|
+
const recorder = needCheckRecorders.shift();
|
|
67
|
+
if (recorder == null)
|
|
68
|
+
return;
|
|
69
|
+
const banLiveId = tempBanObj[recorder.channelId];
|
|
70
|
+
await recorder.checkLiveStatusAndRecord({
|
|
71
|
+
getSavePath(data) {
|
|
72
|
+
return genSavePathFromRule(manager, recorder, data);
|
|
73
|
+
},
|
|
74
|
+
banLiveId,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
threads = threads.concat(range(0, maxThreadCount).map(async () => {
|
|
78
|
+
while (needCheckRecorders.length > 0) {
|
|
79
|
+
try {
|
|
80
|
+
await checkOnce();
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
manager.emit("error", { source: "checkOnceInThread", err });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
await Promise.all(threads);
|
|
88
|
+
};
|
|
89
|
+
// 用于记录暂时被 ban 掉的直播间
|
|
90
|
+
const tempBanObj = {};
|
|
91
|
+
const manager = {
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
...mitt(),
|
|
94
|
+
providers: opts.providers,
|
|
95
|
+
getChannelURLMatchedRecorderProviders(channelURL) {
|
|
96
|
+
return this.providers.filter((p) => p.matchURL(channelURL));
|
|
97
|
+
},
|
|
98
|
+
recorders,
|
|
99
|
+
addRecorder(opts) {
|
|
100
|
+
const provider = this.providers.find((p) => p.id === opts.providerId);
|
|
101
|
+
if (provider == null)
|
|
102
|
+
throw new Error("Cant find provider " + opts.providerId);
|
|
103
|
+
// TODO: 因为泛型函数内部是不持有具体泛型的,这里被迫用了 as,没什么好的思路处理,除非
|
|
104
|
+
// provider.createRecorder 能返回 Recorder<PE> 才能进一步优化。
|
|
105
|
+
const recorder = provider.createRecorder(omit(opts, ["providerId"]));
|
|
106
|
+
this.recorders.push(recorder);
|
|
107
|
+
recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
|
|
108
|
+
recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
|
|
109
|
+
recorder.on("videoFileCreated", ({ filename }) => this.emit("videoFileCreated", { recorder, filename }));
|
|
110
|
+
recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder, filename }));
|
|
111
|
+
recorder.on("RecordStop", ({ recordHandle, reason }) => this.emit("RecordStop", { recorder, recordHandle, reason }));
|
|
112
|
+
recorder.on("Message", (message) => this.emit("Message", { recorder, message }));
|
|
113
|
+
recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder, keys }));
|
|
114
|
+
recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder, ...log }));
|
|
115
|
+
this.emit("RecorderAdded", recorder);
|
|
116
|
+
return recorder;
|
|
117
|
+
},
|
|
118
|
+
removeRecorder(recorder) {
|
|
119
|
+
const idx = this.recorders.findIndex((item) => item === recorder);
|
|
120
|
+
if (idx === -1)
|
|
121
|
+
return;
|
|
122
|
+
recorder.recordHandle?.stop("remove recorder");
|
|
123
|
+
this.recorders.splice(idx, 1);
|
|
124
|
+
delete tempBanObj[recorder.channelId];
|
|
125
|
+
this.emit("RecorderRemoved", recorder);
|
|
126
|
+
},
|
|
127
|
+
async startRecord(id) {
|
|
128
|
+
const recorder = this.recorders.find((item) => item.id === id);
|
|
129
|
+
if (recorder == null)
|
|
130
|
+
return;
|
|
131
|
+
if (recorder.recordHandle != null)
|
|
132
|
+
return;
|
|
133
|
+
await recorder.checkLiveStatusAndRecord({
|
|
134
|
+
getSavePath(data) {
|
|
135
|
+
return genSavePathFromRule(manager, recorder, data);
|
|
136
|
+
},
|
|
137
|
+
qualityRetry: 0,
|
|
138
|
+
});
|
|
139
|
+
delete tempBanObj[recorder.channelId];
|
|
140
|
+
recorder.tempStopIntervalCheck = false;
|
|
141
|
+
return recorder;
|
|
142
|
+
},
|
|
143
|
+
async stopRecord(id) {
|
|
144
|
+
const recorder = this.recorders.find((item) => item.id === id);
|
|
145
|
+
if (recorder == null)
|
|
146
|
+
return;
|
|
147
|
+
if (recorder.recordHandle == null)
|
|
148
|
+
return;
|
|
149
|
+
const liveId = recorder.liveInfo?.liveId;
|
|
150
|
+
await recorder.recordHandle.stop("manual stop", true);
|
|
151
|
+
if (liveId) {
|
|
152
|
+
tempBanObj[recorder.channelId] = liveId;
|
|
153
|
+
recorder.tempStopIntervalCheck = true;
|
|
154
|
+
}
|
|
155
|
+
return recorder;
|
|
156
|
+
},
|
|
157
|
+
autoCheckLiveStatusAndRecord: opts.autoCheckLiveStatusAndRecord ?? true,
|
|
158
|
+
autoCheckInterval: opts.autoCheckInterval ?? 1000,
|
|
159
|
+
isCheckLoopRunning: false,
|
|
160
|
+
startCheckLoop() {
|
|
161
|
+
if (this.isCheckLoopRunning)
|
|
162
|
+
return;
|
|
163
|
+
this.isCheckLoopRunning = true;
|
|
164
|
+
// TODO: emit updated event
|
|
165
|
+
const checkLoop = async () => {
|
|
166
|
+
try {
|
|
167
|
+
await multiThreadCheck(this);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
this.emit("error", { source: "multiThreadCheck", err });
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (!this.isCheckLoopRunning) {
|
|
174
|
+
// do nothing
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
checkLoopTimer = setTimeout(checkLoop, this.autoCheckInterval);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
void checkLoop();
|
|
182
|
+
},
|
|
183
|
+
stopCheckLoop() {
|
|
184
|
+
if (!this.isCheckLoopRunning)
|
|
185
|
+
return;
|
|
186
|
+
this.isCheckLoopRunning = false;
|
|
187
|
+
// TODO: emit updated event
|
|
188
|
+
clearTimeout(checkLoopTimer);
|
|
189
|
+
},
|
|
190
|
+
savePathRule: opts.savePathRule ??
|
|
191
|
+
path.join(process.cwd(), "{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}.mp4"),
|
|
192
|
+
autoRemoveSystemReservedChars: opts.autoRemoveSystemReservedChars ?? true,
|
|
193
|
+
biliBatchQuery: opts.biliBatchQuery ?? false,
|
|
194
|
+
ffmpegOutputArgs: opts.ffmpegOutputArgs ??
|
|
195
|
+
"-c copy" +
|
|
196
|
+
/**
|
|
197
|
+
* FragmentMP4 可以边录边播(浏览器原生支持),具有一定的抗损坏能力,录制中 KILL 只会丢失
|
|
198
|
+
* 最后一个片段,而 FLV 格式如果录制中 KILL 了需要手动修复下 keyframes。所以默认使用 fmp4 格式。
|
|
199
|
+
*/
|
|
200
|
+
" -movflags faststart+frag_keyframe+empty_moov" +
|
|
201
|
+
/**
|
|
202
|
+
* 浏览器加载 FragmentMP4 会需要先把它所有的 moof boxes 都加载完成后才能播放,
|
|
203
|
+
* 默认的分段时长很小,会产生大量的 moof,导致加载很慢,所以这里设置一个分段的最小时长。
|
|
204
|
+
*
|
|
205
|
+
* TODO: 这个浏览器行为或许是可以优化的,比如试试给 fmp4 在录制完成后设置或者录制过程中实时更新 mvhd.duration。
|
|
206
|
+
* https://stackoverflow.com/questions/55887980/how-to-use-media-source-extension-mse-low-latency-mode
|
|
207
|
+
* https://stackoverflow.com/questions/61803136/ffmpeg-fragmented-mp4-takes-long-time-to-start-playing-on-chrome
|
|
208
|
+
*
|
|
209
|
+
* TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
|
|
210
|
+
*/
|
|
211
|
+
" -min_frag_duration 60000000",
|
|
212
|
+
};
|
|
213
|
+
const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
|
|
214
|
+
const args = parseArgsStringToArgv(ffmpegOutputArgs);
|
|
215
|
+
manager.providers.forEach((p) => p.setFFMPEGOutputArgs(args));
|
|
216
|
+
};
|
|
217
|
+
// setProvidersFFMPEGOutputArgs(manager.ffmpegOutputArgs);
|
|
218
|
+
const proxyManager = new Proxy(manager, {
|
|
219
|
+
set(obj, prop, value) {
|
|
220
|
+
Reflect.set(obj, prop, value);
|
|
221
|
+
if (prop === "ffmpegOutputArgs") {
|
|
222
|
+
setProvidersFFMPEGOutputArgs(value);
|
|
223
|
+
}
|
|
224
|
+
if (isConfigurableProp(prop)) {
|
|
225
|
+
obj.emit("Updated", [prop]);
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
return proxyManager;
|
|
231
|
+
}
|
|
232
|
+
export function genSavePathFromRule(manager, recorder, extData) {
|
|
233
|
+
// TODO: 这里随便写的,后面再优化
|
|
234
|
+
const provider = manager.providers.find((p) => p.id === recorder.toJSON().providerId);
|
|
235
|
+
const now = extData?.startTime ? new Date(extData.startTime) : new Date();
|
|
236
|
+
const params = {
|
|
237
|
+
platform: provider?.name ?? "unknown",
|
|
238
|
+
channelId: recorder.channelId,
|
|
239
|
+
remarks: recorder.remarks ?? "",
|
|
240
|
+
year: formatDate(now, "yyyy"),
|
|
241
|
+
month: formatDate(now, "MM"),
|
|
242
|
+
date: formatDate(now, "dd"),
|
|
243
|
+
hour: formatDate(now, "HH"),
|
|
244
|
+
min: formatDate(now, "mm"),
|
|
245
|
+
sec: formatDate(now, "ss"),
|
|
246
|
+
...extData,
|
|
247
|
+
};
|
|
248
|
+
if (manager.autoRemoveSystemReservedChars) {
|
|
249
|
+
for (const key in params) {
|
|
250
|
+
params[key] = removeSystemReservedChars(String(params[key]));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return formatTemplate(manager.savePathRule, params);
|
|
254
|
+
}
|
|
255
|
+
export { StreamManager };
|
|
256
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
|