@bililive-tools/manager 1.5.0 → 1.6.1
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/README.md +6 -4
- package/lib/cache.d.ts +17 -0
- package/lib/cache.js +47 -0
- package/lib/common.d.ts +1 -1
- package/lib/common.js +3 -1
- package/lib/index.d.ts +5 -2
- package/lib/index.js +11 -2
- package/lib/manager.d.ts +20 -15
- package/lib/manager.js +65 -15
- package/lib/recorder/FFMPEGRecorder.d.ts +37 -0
- package/lib/{FFMPEGRecorder.js → recorder/FFMPEGRecorder.js} +35 -14
- package/lib/recorder/IRecorder.d.ts +81 -0
- package/lib/recorder/IRecorder.js +1 -0
- package/lib/recorder/index.d.ts +26 -0
- package/lib/recorder/index.js +18 -0
- package/lib/recorder/mesioRecorder.d.ts +46 -0
- package/lib/recorder/mesioRecorder.js +195 -0
- package/lib/{streamManager.d.ts → recorder/streamManager.d.ts} +13 -8
- package/lib/{streamManager.js → recorder/streamManager.js} +64 -35
- package/lib/recorder.d.ts +13 -6
- package/lib/utils.d.ts +11 -1
- package/lib/utils.js +22 -4
- package/lib/xml_stream_controller.d.ts +23 -0
- package/lib/xml_stream_controller.js +240 -0
- package/package.json +3 -3
- package/lib/FFMPEGRecorder.d.ts +0 -49
package/README.md
CHANGED
|
@@ -72,14 +72,16 @@ manager.startCheckLoop();
|
|
|
72
72
|
|
|
73
73
|
手动开启录制
|
|
74
74
|
|
|
75
|
-
### setFFMPEGPath
|
|
76
|
-
|
|
77
|
-
设置ffmpeg可执行路径
|
|
75
|
+
### setFFMPEGPath & setMesioPath
|
|
78
76
|
|
|
79
77
|
```ts
|
|
80
|
-
import { setFFMPEGPath } from "@bililive-tools/manager";
|
|
78
|
+
import { setFFMPEGPath, setMesioPath } from "@bililive-tools/manager";
|
|
81
79
|
|
|
80
|
+
// 设置ffmpeg可执行路径
|
|
82
81
|
setFFMPEGPath("ffmpeg.exe");
|
|
82
|
+
|
|
83
|
+
// 设置mesio可执行文件路径
|
|
84
|
+
setMesioPath("mesio.exe");
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
## savePathRule 占位符参数
|
package/lib/cache.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class Cache {
|
|
2
|
+
private static instance;
|
|
3
|
+
private data;
|
|
4
|
+
private constructor();
|
|
5
|
+
static getInstance(): Cache;
|
|
6
|
+
set(key: string, value: any): void;
|
|
7
|
+
get(key: string): any;
|
|
8
|
+
has(key: string): boolean;
|
|
9
|
+
delete(key: string): boolean;
|
|
10
|
+
clear(): void;
|
|
11
|
+
get size(): number;
|
|
12
|
+
keys(): IterableIterator<string>;
|
|
13
|
+
values(): IterableIterator<any>;
|
|
14
|
+
entries(): IterableIterator<[string, any]>;
|
|
15
|
+
forEach(callbackfn: (value: any, key: string, map: Map<string, any>) => void, thisArg?: any): void;
|
|
16
|
+
[Symbol.iterator](): IterableIterator<[string, any]>;
|
|
17
|
+
}
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
static instance;
|
|
3
|
+
data;
|
|
4
|
+
constructor() {
|
|
5
|
+
this.data = new Map();
|
|
6
|
+
}
|
|
7
|
+
static getInstance() {
|
|
8
|
+
if (!Cache.instance) {
|
|
9
|
+
Cache.instance = new Cache();
|
|
10
|
+
}
|
|
11
|
+
return Cache.instance;
|
|
12
|
+
}
|
|
13
|
+
set(key, value) {
|
|
14
|
+
this.data.set(key, value);
|
|
15
|
+
}
|
|
16
|
+
get(key) {
|
|
17
|
+
return this.data.get(key);
|
|
18
|
+
}
|
|
19
|
+
has(key) {
|
|
20
|
+
return this.data.has(key);
|
|
21
|
+
}
|
|
22
|
+
delete(key) {
|
|
23
|
+
return this.data.delete(key);
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.data.clear();
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
return this.data.size;
|
|
30
|
+
}
|
|
31
|
+
keys() {
|
|
32
|
+
return this.data.keys();
|
|
33
|
+
}
|
|
34
|
+
values() {
|
|
35
|
+
return this.data.values();
|
|
36
|
+
}
|
|
37
|
+
entries() {
|
|
38
|
+
return this.data.entries();
|
|
39
|
+
}
|
|
40
|
+
forEach(callbackfn, thisArg) {
|
|
41
|
+
this.data.forEach(callbackfn, thisArg);
|
|
42
|
+
}
|
|
43
|
+
// 实现 Symbol.iterator 接口,支持 for...of 循环
|
|
44
|
+
[Symbol.iterator]() {
|
|
45
|
+
return this.data[Symbol.iterator]();
|
|
46
|
+
}
|
|
47
|
+
}
|
package/lib/common.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type ChannelId = string;
|
|
|
3
3
|
export declare const Qualities: readonly ["lowest", "low", "medium", "high", "highest"];
|
|
4
4
|
export declare const BiliQualities: readonly [30000, 20000, 10000, 400, 250, 150, 80];
|
|
5
5
|
export declare const DouyuQualities: readonly [0, 2, 3, 4, 8];
|
|
6
|
-
export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
|
|
6
|
+
export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1];
|
|
7
7
|
export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
|
|
8
8
|
export type Quality = (typeof Qualities)[number] | (typeof BiliQualities)[number] | (typeof DouyuQualities)[number] | (typeof HuYaQualities)[number] | (typeof DouYinQualities)[number];
|
|
9
9
|
export interface MessageSender<E extends AnyObject = UnknownObject> {
|
package/lib/common.js
CHANGED
|
@@ -2,5 +2,7 @@ export const Qualities = ["lowest", "low", "medium", "high", "highest"];
|
|
|
2
2
|
export const BiliQualities = [30000, 20000, 10000, 400, 250, 150, 80];
|
|
3
3
|
export const DouyuQualities = [0, 2, 3, 4, 8];
|
|
4
4
|
// 14100: 2K HDR;14000:2K;4200:HDR(10M);0:原画;8000:蓝光8M;4000:蓝光4M;2000:超清;500:流畅
|
|
5
|
-
export const HuYaQualities = [
|
|
5
|
+
export const HuYaQualities = [
|
|
6
|
+
0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500, -1,
|
|
7
|
+
];
|
|
6
8
|
export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
|
package/lib/index.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export * from "./common.js";
|
|
|
7
7
|
export * from "./recorder.js";
|
|
8
8
|
export * from "./manager.js";
|
|
9
9
|
export * from "./record_extra_data_controller.js";
|
|
10
|
-
export * from "./FFMPEGRecorder.js";
|
|
10
|
+
export * from "./recorder/FFMPEGRecorder.js";
|
|
11
|
+
export { createBaseRecorder } from "./recorder/index.js";
|
|
11
12
|
export { utils };
|
|
12
13
|
/**
|
|
13
14
|
* 提供一些 utils
|
|
@@ -17,5 +18,7 @@ export declare function defaultToJSON<E extends AnyObject>(provider: RecorderPro
|
|
|
17
18
|
export declare function genRecorderUUID(): Recorder["id"];
|
|
18
19
|
export declare function genRecordUUID(): RecordHandle["id"];
|
|
19
20
|
export declare function setFFMPEGPath(newPath: string): void;
|
|
20
|
-
export declare const createFFMPEGBuilder: (
|
|
21
|
+
export declare const createFFMPEGBuilder: (...args: Parameters<typeof ffmpeg>) => ffmpeg.FfmpegCommand;
|
|
22
|
+
export declare function setMesioPath(newPath: string): void;
|
|
23
|
+
export declare function getMesioPath(): string;
|
|
21
24
|
export declare function getDataFolderPath<E extends AnyObject>(provider: RecorderProvider<E>): string;
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,8 @@ export * from "./common.js";
|
|
|
6
6
|
export * from "./recorder.js";
|
|
7
7
|
export * from "./manager.js";
|
|
8
8
|
export * from "./record_extra_data_controller.js";
|
|
9
|
-
export * from "./FFMPEGRecorder.js";
|
|
9
|
+
export * from "./recorder/FFMPEGRecorder.js";
|
|
10
|
+
export { createBaseRecorder } from "./recorder/index.js";
|
|
10
11
|
export { utils };
|
|
11
12
|
/**
|
|
12
13
|
* 提供一些 utils
|
|
@@ -21,7 +22,6 @@ export function defaultToJSON(provider, recorder) {
|
|
|
21
22
|
...pick(recorder, [
|
|
22
23
|
"id",
|
|
23
24
|
"channelId",
|
|
24
|
-
"owner",
|
|
25
25
|
"remarks",
|
|
26
26
|
"disableAutoCheck",
|
|
27
27
|
"quality",
|
|
@@ -36,6 +36,7 @@ export function defaultToJSON(provider, recorder) {
|
|
|
36
36
|
"liveInfo",
|
|
37
37
|
"uid",
|
|
38
38
|
"titleKeywords",
|
|
39
|
+
// "recordHandle",
|
|
39
40
|
]),
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -55,6 +56,14 @@ export const createFFMPEGBuilder = (...args) => {
|
|
|
55
56
|
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
56
57
|
return ffmpeg(...args);
|
|
57
58
|
};
|
|
59
|
+
// Mesio path management
|
|
60
|
+
let mesioPath = "mesio";
|
|
61
|
+
export function setMesioPath(newPath) {
|
|
62
|
+
mesioPath = newPath;
|
|
63
|
+
}
|
|
64
|
+
export function getMesioPath() {
|
|
65
|
+
return mesioPath;
|
|
66
|
+
}
|
|
58
67
|
export function getDataFolderPath(provider) {
|
|
59
68
|
return "./" + provider.id;
|
|
60
69
|
}
|
package/lib/manager.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { Emitter } from "mitt";
|
|
|
2
2
|
import { ChannelId, Message } from "./common.js";
|
|
3
3
|
import { RecorderCreateOpts, Recorder, SerializedRecorder, RecordHandle, DebugLog, Progress } from "./recorder.js";
|
|
4
4
|
import { AnyObject, UnknownObject } from "./utils.js";
|
|
5
|
-
import { StreamManager } from "./streamManager.js";
|
|
5
|
+
import { StreamManager } from "./recorder/streamManager.js";
|
|
6
|
+
import { Cache } from "./cache.js";
|
|
6
7
|
export interface RecorderProvider<E extends AnyObject> {
|
|
7
8
|
id: string;
|
|
8
9
|
name: string;
|
|
@@ -12,14 +13,14 @@ export interface RecorderProvider<E extends AnyObject> {
|
|
|
12
13
|
id: ChannelId;
|
|
13
14
|
title: string;
|
|
14
15
|
owner: string;
|
|
15
|
-
uid?: number;
|
|
16
|
+
uid?: number | string;
|
|
16
17
|
avatar?: string;
|
|
17
18
|
} | null>;
|
|
18
19
|
createRecorder: (this: RecorderProvider<E>, opts: Omit<RecorderCreateOpts<E>, "providerId">) => Recorder<E>;
|
|
19
20
|
fromJSON: <T extends SerializedRecorder<E>>(this: RecorderProvider<E>, json: T) => Recorder<E>;
|
|
20
21
|
setFFMPEGOutputArgs: (this: RecorderProvider<E>, args: string[]) => void;
|
|
21
22
|
}
|
|
22
|
-
declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "ffmpegOutputArgs", "biliBatchQuery"];
|
|
23
|
+
declare const configurableProps: readonly ["savePathRule", "autoRemoveSystemReservedChars", "autoCheckInterval", "ffmpegOutputArgs", "biliBatchQuery", "recordRetryImmediately"];
|
|
23
24
|
type ConfigurableProp = (typeof configurableProps)[number];
|
|
24
25
|
export interface RecorderManager<ME extends UnknownObject, P extends RecorderProvider<AnyObject> = RecorderProvider<UnknownObject>, PE extends AnyObject = GetProviderExtra<P>, E extends AnyObject = ME & PE> extends Emitter<{
|
|
25
26
|
error: {
|
|
@@ -27,44 +28,44 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
27
28
|
err: unknown;
|
|
28
29
|
};
|
|
29
30
|
RecordStart: {
|
|
30
|
-
recorder:
|
|
31
|
+
recorder: SerializedRecorder<E>;
|
|
31
32
|
recordHandle: RecordHandle;
|
|
32
33
|
};
|
|
33
34
|
RecordSegment: {
|
|
34
|
-
recorder:
|
|
35
|
+
recorder: SerializedRecorder<E>;
|
|
35
36
|
recordHandle?: RecordHandle;
|
|
36
37
|
};
|
|
37
38
|
videoFileCreated: {
|
|
38
|
-
recorder:
|
|
39
|
+
recorder: SerializedRecorder<E>;
|
|
39
40
|
filename: string;
|
|
40
41
|
cover?: string;
|
|
41
42
|
};
|
|
42
43
|
videoFileCompleted: {
|
|
43
|
-
recorder:
|
|
44
|
+
recorder: SerializedRecorder<E>;
|
|
44
45
|
filename: string;
|
|
45
46
|
};
|
|
46
47
|
RecorderProgress: {
|
|
47
|
-
recorder:
|
|
48
|
+
recorder: SerializedRecorder<E>;
|
|
48
49
|
progress: Progress;
|
|
49
50
|
};
|
|
50
51
|
RecoderLiveStart: {
|
|
51
52
|
recorder: Recorder<E>;
|
|
52
53
|
};
|
|
53
54
|
RecordStop: {
|
|
54
|
-
recorder:
|
|
55
|
+
recorder: SerializedRecorder<E>;
|
|
55
56
|
recordHandle: RecordHandle;
|
|
56
57
|
reason?: string;
|
|
57
58
|
};
|
|
58
59
|
Message: {
|
|
59
|
-
recorder:
|
|
60
|
+
recorder: SerializedRecorder<E>;
|
|
60
61
|
message: Message;
|
|
61
62
|
};
|
|
62
63
|
RecorderUpdated: {
|
|
63
|
-
recorder:
|
|
64
|
+
recorder: SerializedRecorder<E>;
|
|
64
65
|
keys: (string | keyof Recorder<E>)[];
|
|
65
66
|
};
|
|
66
|
-
RecorderAdded:
|
|
67
|
-
RecorderRemoved:
|
|
67
|
+
RecorderAdded: SerializedRecorder<E>;
|
|
68
|
+
RecorderRemoved: SerializedRecorder<E>;
|
|
68
69
|
RecorderDebugLog: DebugLog & {
|
|
69
70
|
recorder: Recorder<E>;
|
|
70
71
|
};
|
|
@@ -73,7 +74,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
73
74
|
providers: P[];
|
|
74
75
|
getChannelURLMatchedRecorderProviders: (this: RecorderManager<ME, P, PE, E>, channelURL: string) => P[];
|
|
75
76
|
recorders: Recorder<E>[];
|
|
76
|
-
addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: RecorderCreateOpts<E>) => Recorder<E>;
|
|
77
|
+
addRecorder: (this: RecorderManager<ME, P, PE, E>, opts: Omit<RecorderCreateOpts<E>, "cache">) => Recorder<E>;
|
|
77
78
|
removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
|
|
78
79
|
startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
79
80
|
stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
@@ -87,6 +88,10 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
87
88
|
ffmpegOutputArgs: string;
|
|
88
89
|
/** b站使用批量查询接口 */
|
|
89
90
|
biliBatchQuery: boolean;
|
|
91
|
+
/** 测试:录制错误立即重试 */
|
|
92
|
+
recordRetryImmediately: boolean;
|
|
93
|
+
/** 缓存实例 */
|
|
94
|
+
cache: Cache;
|
|
90
95
|
}
|
|
91
96
|
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>> & {
|
|
92
97
|
providers: P[];
|
|
@@ -98,4 +103,4 @@ export declare function genSavePathFromRule<ME extends AnyObject, P extends Reco
|
|
|
98
103
|
startTime?: number;
|
|
99
104
|
}): string;
|
|
100
105
|
export type GetProviderExtra<P> = P extends RecorderProvider<infer E> ? E : never;
|
|
101
|
-
export { StreamManager };
|
|
106
|
+
export { StreamManager, Cache };
|
package/lib/manager.js
CHANGED
|
@@ -5,13 +5,15 @@ import { omit, range } from "lodash-es";
|
|
|
5
5
|
import { parseArgsStringToArgv } from "string-argv";
|
|
6
6
|
import { getBiliStatusInfoByRoomIds } from "./api.js";
|
|
7
7
|
import { formatDate, removeSystemReservedChars, formatTemplate, replaceExtName, downloadImage, isBetweenTimeRange, } from "./utils.js";
|
|
8
|
-
import { StreamManager } from "./streamManager.js";
|
|
8
|
+
import { StreamManager } from "./recorder/streamManager.js";
|
|
9
|
+
import { Cache } from "./cache.js";
|
|
9
10
|
const configurableProps = [
|
|
10
11
|
"savePathRule",
|
|
11
12
|
"autoRemoveSystemReservedChars",
|
|
12
13
|
"autoCheckInterval",
|
|
13
14
|
"ffmpegOutputArgs",
|
|
14
15
|
"biliBatchQuery",
|
|
16
|
+
"recordRetryImmediately",
|
|
15
17
|
];
|
|
16
18
|
function isConfigurableProp(prop) {
|
|
17
19
|
return configurableProps.includes(prop);
|
|
@@ -89,6 +91,10 @@ export function createRecorderManager(opts) {
|
|
|
89
91
|
const tempBanObj = {};
|
|
90
92
|
// 用于是否触发LiveStart事件,不要重复触发
|
|
91
93
|
const liveStartObj = {};
|
|
94
|
+
// 用于记录触发重试直播场次的次数
|
|
95
|
+
const retryCountObj = {};
|
|
96
|
+
// 获取缓存单例
|
|
97
|
+
const cache = Cache.getInstance();
|
|
92
98
|
const manager = {
|
|
93
99
|
// @ts-ignore
|
|
94
100
|
...mitt(),
|
|
@@ -103,24 +109,62 @@ export function createRecorderManager(opts) {
|
|
|
103
109
|
throw new Error("Cant find provider " + opts.providerId);
|
|
104
110
|
// TODO: 因为泛型函数内部是不持有具体泛型的,这里被迫用了 as,没什么好的思路处理,除非
|
|
105
111
|
// provider.createRecorder 能返回 Recorder<PE> 才能进一步优化。
|
|
106
|
-
const recorder = provider.createRecorder(
|
|
112
|
+
const recorder = provider.createRecorder({
|
|
113
|
+
...omit(opts, ["providerId"]),
|
|
114
|
+
cache,
|
|
115
|
+
});
|
|
107
116
|
this.recorders.push(recorder);
|
|
108
|
-
recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
|
|
109
|
-
recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
|
|
117
|
+
recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder: recorder.toJSON(), recordHandle }));
|
|
118
|
+
recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder: recorder.toJSON(), recordHandle }));
|
|
110
119
|
recorder.on("videoFileCreated", ({ filename, cover }) => {
|
|
111
120
|
if (recorder.saveCover && recorder?.liveInfo?.cover) {
|
|
112
121
|
const coverPath = replaceExtName(filename, ".jpg");
|
|
113
122
|
downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
|
|
114
123
|
}
|
|
115
|
-
this.emit("videoFileCreated", { recorder, filename });
|
|
124
|
+
this.emit("videoFileCreated", { recorder: recorder.toJSON(), filename });
|
|
125
|
+
});
|
|
126
|
+
recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder: recorder.toJSON(), filename }));
|
|
127
|
+
recorder.on("Message", (message) => this.emit("Message", { recorder: recorder.toJSON(), message }));
|
|
128
|
+
recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder: recorder.toJSON(), keys }));
|
|
129
|
+
recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder: recorder, ...log }));
|
|
130
|
+
recorder.on("RecordStop", ({ recordHandle, reason }) => {
|
|
131
|
+
this.emit("RecordStop", { recorder: recorder.toJSON(), recordHandle, reason });
|
|
132
|
+
// 如果reason中存在"invalid stream",说明直播由于某些原因中断了,虽然会在下一次周期检查中继续,但是会遗漏一段时间。
|
|
133
|
+
// 这时候可以触发一次检查,但出于直播可能抽风的原因,为避免风控,一场直播最多触发五次。
|
|
134
|
+
// 测试阶段,还需要一个开关,默认关闭,几个版本后转正使用
|
|
135
|
+
// 也许之后还能链接复用,但也会引入更多复杂度,需要谨慎考虑
|
|
136
|
+
// 虎牙直播结束后可能额外触发导致错误,忽略虎牙直播间:https://www.huya.com/910323
|
|
137
|
+
if (manager.recordRetryImmediately &&
|
|
138
|
+
recorder.providerId !== "HuYa" &&
|
|
139
|
+
reason &&
|
|
140
|
+
reason.includes("invalid stream") &&
|
|
141
|
+
recorder?.liveInfo?.liveId) {
|
|
142
|
+
const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
|
|
143
|
+
if (retryCountObj[key] > 5)
|
|
144
|
+
return;
|
|
145
|
+
if (!retryCountObj[key]) {
|
|
146
|
+
retryCountObj[key] = 0;
|
|
147
|
+
}
|
|
148
|
+
if (retryCountObj[key] < 5) {
|
|
149
|
+
retryCountObj[key]++;
|
|
150
|
+
}
|
|
151
|
+
this.emit("RecorderDebugLog", {
|
|
152
|
+
recorder,
|
|
153
|
+
type: "common",
|
|
154
|
+
text: `录制${recorder?.channelId}因“${reason}”中断,触发重试直播(${retryCountObj[key]})`,
|
|
155
|
+
});
|
|
156
|
+
// 触发一次检查,等待一秒使状态清理完毕
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
recorder.checkLiveStatusAndRecord({
|
|
159
|
+
getSavePath(data) {
|
|
160
|
+
return genSavePathFromRule(manager, recorder, data);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}, 1000);
|
|
164
|
+
}
|
|
116
165
|
});
|
|
117
|
-
recorder.on("videoFileCompleted", ({ filename }) => this.emit("videoFileCompleted", { recorder, filename }));
|
|
118
|
-
recorder.on("RecordStop", ({ recordHandle, reason }) => this.emit("RecordStop", { recorder, recordHandle, reason }));
|
|
119
|
-
recorder.on("Message", (message) => this.emit("Message", { recorder, message }));
|
|
120
|
-
recorder.on("Updated", (keys) => this.emit("RecorderUpdated", { recorder, keys }));
|
|
121
|
-
recorder.on("DebugLog", (log) => this.emit("RecorderDebugLog", { recorder, ...log }));
|
|
122
166
|
recorder.on("progress", (progress) => {
|
|
123
|
-
this.emit("RecorderProgress", { recorder, progress });
|
|
167
|
+
this.emit("RecorderProgress", { recorder: recorder.toJSON(), progress });
|
|
124
168
|
});
|
|
125
169
|
recorder.on("videoFileCreated", () => {
|
|
126
170
|
if (!recorder.liveInfo?.liveId)
|
|
@@ -129,9 +173,9 @@ export function createRecorderManager(opts) {
|
|
|
129
173
|
if (liveStartObj[key])
|
|
130
174
|
return;
|
|
131
175
|
liveStartObj[key] = true;
|
|
132
|
-
this.emit("RecoderLiveStart", { recorder });
|
|
176
|
+
this.emit("RecoderLiveStart", { recorder: recorder });
|
|
133
177
|
});
|
|
134
|
-
this.emit("RecorderAdded", recorder);
|
|
178
|
+
this.emit("RecorderAdded", recorder.toJSON());
|
|
135
179
|
return recorder;
|
|
136
180
|
},
|
|
137
181
|
removeRecorder(recorder) {
|
|
@@ -141,7 +185,7 @@ export function createRecorderManager(opts) {
|
|
|
141
185
|
recorder.recordHandle?.stop("remove recorder");
|
|
142
186
|
this.recorders.splice(idx, 1);
|
|
143
187
|
delete tempBanObj[recorder.channelId];
|
|
144
|
-
this.emit("RecorderRemoved", recorder);
|
|
188
|
+
this.emit("RecorderRemoved", recorder.toJSON());
|
|
145
189
|
},
|
|
146
190
|
async startRecord(id) {
|
|
147
191
|
const recorder = this.recorders.find((item) => item.id === id);
|
|
@@ -218,6 +262,7 @@ export function createRecorderManager(opts) {
|
|
|
218
262
|
path.join(process.cwd(), "{platform}/{owner}/{year}-{month}-{date} {hour}-{min}-{sec} {title}"),
|
|
219
263
|
autoRemoveSystemReservedChars: opts.autoRemoveSystemReservedChars ?? true,
|
|
220
264
|
biliBatchQuery: opts.biliBatchQuery ?? false,
|
|
265
|
+
recordRetryImmediately: opts.recordRetryImmediately ?? false,
|
|
221
266
|
ffmpegOutputArgs: opts.ffmpegOutputArgs ??
|
|
222
267
|
"-c copy" +
|
|
223
268
|
/**
|
|
@@ -236,6 +281,7 @@ export function createRecorderManager(opts) {
|
|
|
236
281
|
* TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
|
|
237
282
|
*/
|
|
238
283
|
" -min_frag_duration 10000000",
|
|
284
|
+
cache,
|
|
239
285
|
};
|
|
240
286
|
const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
|
|
241
287
|
const args = parseArgsStringToArgv(ffmpegOutputArgs);
|
|
@@ -260,6 +306,8 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
260
306
|
// TODO: 这里随便写的,后面再优化
|
|
261
307
|
const provider = manager.providers.find((p) => p.id === recorder.toJSON().providerId);
|
|
262
308
|
const now = extData?.startTime ? new Date(extData.startTime) : new Date();
|
|
309
|
+
const owner = (extData?.owner ?? "").replaceAll("%", "_");
|
|
310
|
+
const title = (extData?.title ?? "").replaceAll("%", "_");
|
|
263
311
|
const params = {
|
|
264
312
|
platform: provider?.name ?? "unknown",
|
|
265
313
|
channelId: recorder.channelId,
|
|
@@ -271,6 +319,8 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
271
319
|
min: formatDate(now, "mm"),
|
|
272
320
|
sec: formatDate(now, "ss"),
|
|
273
321
|
...extData,
|
|
322
|
+
owner: owner,
|
|
323
|
+
title: title,
|
|
274
324
|
};
|
|
275
325
|
if (manager.autoRemoveSystemReservedChars) {
|
|
276
326
|
for (const key in params) {
|
|
@@ -286,4 +336,4 @@ export function genSavePathFromRule(manager, recorder, extData) {
|
|
|
286
336
|
}
|
|
287
337
|
return formatTemplate(savePathRule, params);
|
|
288
338
|
}
|
|
289
|
-
export { StreamManager };
|
|
339
|
+
export { StreamManager, Cache };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import { IRecorder, FFMPEGRecorderOptions } from "./IRecorder.js";
|
|
3
|
+
export declare class FFMPEGRecorder extends EventEmitter implements IRecorder {
|
|
4
|
+
private onEnd;
|
|
5
|
+
private onUpdateLiveInfo;
|
|
6
|
+
private command;
|
|
7
|
+
private streamManager;
|
|
8
|
+
private timeoutChecker;
|
|
9
|
+
readonly hasSegment: boolean;
|
|
10
|
+
readonly getSavePath: (data: {
|
|
11
|
+
startTime: number;
|
|
12
|
+
title?: string;
|
|
13
|
+
}) => string;
|
|
14
|
+
readonly segment: number;
|
|
15
|
+
ffmpegOutputOptions: string[];
|
|
16
|
+
readonly inputOptions: string[];
|
|
17
|
+
readonly isHls: boolean;
|
|
18
|
+
readonly disableDanma: boolean;
|
|
19
|
+
readonly url: string;
|
|
20
|
+
formatName: "flv" | "ts" | "fmp4";
|
|
21
|
+
videoFormat: "ts" | "mkv" | "mp4";
|
|
22
|
+
readonly headers: {
|
|
23
|
+
[key: string]: string | undefined;
|
|
24
|
+
} | undefined;
|
|
25
|
+
constructor(opts: FFMPEGRecorderOptions, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
26
|
+
title?: string;
|
|
27
|
+
cover?: string;
|
|
28
|
+
}>);
|
|
29
|
+
createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
|
|
30
|
+
formatLine(line: string): {
|
|
31
|
+
time: string | null;
|
|
32
|
+
} | null;
|
|
33
|
+
run(): void;
|
|
34
|
+
getArguments(): string[];
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
getExtraDataController(): import("../xml_stream_controller.js").XmlStreamController | null;
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import { createFFMPEGBuilder, StreamManager, utils } from "
|
|
3
|
-
import { createInvalidStreamChecker, assert } from "
|
|
2
|
+
import { createFFMPEGBuilder, StreamManager, utils } from "../index.js";
|
|
3
|
+
import { createInvalidStreamChecker, assert } from "../utils.js";
|
|
4
4
|
export class FFMPEGRecorder extends EventEmitter {
|
|
5
5
|
onEnd;
|
|
6
6
|
onUpdateLiveInfo;
|
|
@@ -15,30 +15,50 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
15
15
|
isHls;
|
|
16
16
|
disableDanma = false;
|
|
17
17
|
url;
|
|
18
|
+
formatName;
|
|
19
|
+
videoFormat;
|
|
18
20
|
headers;
|
|
19
21
|
constructor(opts, onEnd, onUpdateLiveInfo) {
|
|
20
22
|
super();
|
|
21
23
|
this.onEnd = onEnd;
|
|
22
24
|
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
23
25
|
const hasSegment = !!opts.segment;
|
|
26
|
+
this.hasSegment = hasSegment;
|
|
27
|
+
let formatName = "flv";
|
|
28
|
+
if (opts.url.includes(".m3u8")) {
|
|
29
|
+
formatName = "ts";
|
|
30
|
+
}
|
|
31
|
+
this.formatName = opts.formatName ?? formatName;
|
|
32
|
+
if (this.formatName === "fmp4" || this.formatName === "ts") {
|
|
33
|
+
this.isHls = true;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.isHls = false;
|
|
37
|
+
}
|
|
38
|
+
let videoFormat = opts.videoFormat ?? "auto";
|
|
39
|
+
if (videoFormat === "auto") {
|
|
40
|
+
if (!this.hasSegment) {
|
|
41
|
+
videoFormat = "mp4";
|
|
42
|
+
if (this.formatName === "ts") {
|
|
43
|
+
videoFormat = "ts";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
videoFormat = "ts";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.videoFormat = videoFormat;
|
|
24
51
|
this.disableDanma = opts.disableDanma ?? false;
|
|
25
|
-
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma,
|
|
52
|
+
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, "ffmpeg", this.videoFormat, {
|
|
26
53
|
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
27
54
|
});
|
|
28
55
|
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
|
|
29
|
-
this.hasSegment = hasSegment;
|
|
30
56
|
this.getSavePath = opts.getSavePath;
|
|
31
57
|
this.ffmpegOutputOptions = opts.outputOptions;
|
|
32
58
|
this.inputOptions = opts.inputOptions ?? [];
|
|
33
59
|
this.url = opts.url;
|
|
34
60
|
this.segment = opts.segment;
|
|
35
61
|
this.headers = opts.headers;
|
|
36
|
-
if (opts.isHls === undefined) {
|
|
37
|
-
this.isHls = this.url.includes("m3u8");
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
this.isHls = opts.isHls;
|
|
41
|
-
}
|
|
42
62
|
this.command = this.createCommand();
|
|
43
63
|
this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
|
|
44
64
|
this.emit("videoFileCreated", { filename, cover });
|
|
@@ -79,15 +99,16 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
79
99
|
.on("end", () => this.onEnd("finished"))
|
|
80
100
|
.on("stderr", async (stderrLine) => {
|
|
81
101
|
assert(typeof stderrLine === "string");
|
|
82
|
-
await this.streamManager.handleVideoStarted(stderrLine);
|
|
83
102
|
this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
|
|
103
|
+
const [isInvalid, reason] = isInvalidStream(stderrLine);
|
|
104
|
+
if (isInvalid) {
|
|
105
|
+
this.onEnd(reason);
|
|
106
|
+
}
|
|
107
|
+
await this.streamManager.handleVideoStarted(stderrLine);
|
|
84
108
|
const info = this.formatLine(stderrLine);
|
|
85
109
|
if (info) {
|
|
86
110
|
this.emit("progress", info);
|
|
87
111
|
}
|
|
88
|
-
if (isInvalidStream(stderrLine)) {
|
|
89
|
-
this.onEnd("invalid stream");
|
|
90
|
-
}
|
|
91
112
|
})
|
|
92
113
|
.on("stderr", this.timeoutChecker?.update);
|
|
93
114
|
if (this.hasSegment) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/**
|
|
3
|
+
* 录制器构造函数选项的基础接口
|
|
4
|
+
*/
|
|
5
|
+
export interface BaseRecorderOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
getSavePath: (data: {
|
|
8
|
+
startTime: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
}) => string;
|
|
11
|
+
segment: number;
|
|
12
|
+
inputOptions?: string[];
|
|
13
|
+
disableDanma?: boolean;
|
|
14
|
+
formatName?: "flv" | "ts" | "fmp4";
|
|
15
|
+
headers?: {
|
|
16
|
+
[key: string]: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 录制器接口定义
|
|
21
|
+
*/
|
|
22
|
+
export interface IRecorder extends EventEmitter {
|
|
23
|
+
readonly hasSegment: boolean;
|
|
24
|
+
readonly segment: number;
|
|
25
|
+
readonly inputOptions: string[];
|
|
26
|
+
readonly isHls: boolean;
|
|
27
|
+
readonly disableDanma: boolean;
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly headers: {
|
|
30
|
+
[key: string]: string | undefined;
|
|
31
|
+
} | undefined;
|
|
32
|
+
readonly getSavePath: (data: {
|
|
33
|
+
startTime: number;
|
|
34
|
+
title?: string;
|
|
35
|
+
}) => string;
|
|
36
|
+
run(): void;
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
getArguments(): string[];
|
|
39
|
+
getExtraDataController(): any;
|
|
40
|
+
createCommand(): any;
|
|
41
|
+
on(event: "videoFileCreated", listener: (data: {
|
|
42
|
+
filename: string;
|
|
43
|
+
cover?: string;
|
|
44
|
+
}) => void): this;
|
|
45
|
+
on(event: "videoFileCompleted", listener: (data: {
|
|
46
|
+
filename: string;
|
|
47
|
+
}) => void): this;
|
|
48
|
+
on(event: "DebugLog", listener: (data: {
|
|
49
|
+
type: string;
|
|
50
|
+
text: string;
|
|
51
|
+
}) => void): this;
|
|
52
|
+
on(event: "progress", listener: (info: any) => void): this;
|
|
53
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
54
|
+
emit(event: "videoFileCreated", data: {
|
|
55
|
+
filename: string;
|
|
56
|
+
cover?: string;
|
|
57
|
+
}): boolean;
|
|
58
|
+
emit(event: "videoFileCompleted", data: {
|
|
59
|
+
filename: string;
|
|
60
|
+
}): boolean;
|
|
61
|
+
emit(event: "DebugLog", data: {
|
|
62
|
+
type: string;
|
|
63
|
+
text: string;
|
|
64
|
+
}): boolean;
|
|
65
|
+
emit(event: "progress", info: any): boolean;
|
|
66
|
+
emit(event: string, ...args: any[]): boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* FFMPEG录制器特定选项
|
|
70
|
+
*/
|
|
71
|
+
export interface FFMPEGRecorderOptions extends BaseRecorderOptions {
|
|
72
|
+
outputOptions: string[];
|
|
73
|
+
videoFormat?: "auto" | "ts" | "mkv" | "mp4";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Mesio录制器特定选项
|
|
77
|
+
*/
|
|
78
|
+
export interface MesioRecorderOptions extends BaseRecorderOptions {
|
|
79
|
+
outputOptions?: string[];
|
|
80
|
+
isHls?: boolean;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|