@bililive-tools/manager 1.2.0 → 1.3.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/lib/FFMPEGRecorder.d.ts +14 -2
- package/lib/FFMPEGRecorder.js +38 -14
- package/lib/common.d.ts +1 -1
- package/lib/common.js +1 -1
- package/lib/manager.d.ts +2 -0
- package/lib/manager.js +18 -5
- package/lib/record_extra_data_controller.js +4 -4
- package/lib/recorder.d.ts +5 -4
- package/lib/streamManager.d.ts +15 -2
- package/lib/streamManager.js +29 -8
- package/lib/utils.d.ts +11 -1
- package/lib/utils.js +29 -2
- package/package.json +1 -1
package/lib/FFMPEGRecorder.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
2
|
export declare class FFMPEGRecorder extends EventEmitter {
|
|
3
3
|
private onEnd;
|
|
4
|
+
private onUpdateLiveInfo;
|
|
4
5
|
private command;
|
|
5
6
|
private streamManager;
|
|
6
7
|
private timeoutChecker;
|
|
7
8
|
hasSegment: boolean;
|
|
8
9
|
getSavePath: (data: {
|
|
9
10
|
startTime: number;
|
|
11
|
+
title?: string;
|
|
10
12
|
}) => string;
|
|
11
13
|
segment: number;
|
|
12
14
|
ffmpegOutputOptions: string[];
|
|
@@ -14,10 +16,14 @@ export declare class FFMPEGRecorder extends EventEmitter {
|
|
|
14
16
|
isHls: boolean;
|
|
15
17
|
disableDanma: boolean;
|
|
16
18
|
url: string;
|
|
19
|
+
headers: {
|
|
20
|
+
[key: string]: string | undefined;
|
|
21
|
+
} | undefined;
|
|
17
22
|
constructor(opts: {
|
|
18
23
|
url: string;
|
|
19
24
|
getSavePath: (data: {
|
|
20
25
|
startTime: number;
|
|
26
|
+
title?: string;
|
|
21
27
|
}) => string;
|
|
22
28
|
segment: number;
|
|
23
29
|
outputOptions: string[];
|
|
@@ -25,8 +31,14 @@ export declare class FFMPEGRecorder extends EventEmitter {
|
|
|
25
31
|
isHls?: boolean;
|
|
26
32
|
disableDanma?: boolean;
|
|
27
33
|
videoFormat?: "auto" | "ts" | "mkv";
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
headers?: {
|
|
35
|
+
[key: string]: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
}, onEnd: (...args: unknown[]) => void, onUpdateLiveInfo: () => Promise<{
|
|
38
|
+
title?: string;
|
|
39
|
+
cover?: string;
|
|
40
|
+
}>);
|
|
41
|
+
createCommand(): import("@renmu/fluent-ffmpeg").FfmpegCommand;
|
|
30
42
|
formatLine(line: string): {
|
|
31
43
|
time: string | null;
|
|
32
44
|
} | null;
|
package/lib/FFMPEGRecorder.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createFFMPEGBuilder, StreamManager, utils } from "./index.js";
|
|
|
3
3
|
import { createInvalidStreamChecker, assert } from "./utils.js";
|
|
4
4
|
export class FFMPEGRecorder extends EventEmitter {
|
|
5
5
|
onEnd;
|
|
6
|
+
onUpdateLiveInfo;
|
|
6
7
|
command;
|
|
7
8
|
streamManager;
|
|
8
9
|
timeoutChecker;
|
|
@@ -11,26 +12,36 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
11
12
|
segment;
|
|
12
13
|
ffmpegOutputOptions = [];
|
|
13
14
|
inputOptions = [];
|
|
14
|
-
isHls
|
|
15
|
+
isHls;
|
|
15
16
|
disableDanma = false;
|
|
16
17
|
url;
|
|
17
|
-
|
|
18
|
+
headers;
|
|
19
|
+
constructor(opts, onEnd, onUpdateLiveInfo) {
|
|
18
20
|
super();
|
|
19
21
|
this.onEnd = onEnd;
|
|
22
|
+
this.onUpdateLiveInfo = onUpdateLiveInfo;
|
|
20
23
|
const hasSegment = !!opts.segment;
|
|
21
24
|
this.disableDanma = opts.disableDanma ?? false;
|
|
22
|
-
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat
|
|
23
|
-
|
|
25
|
+
this.streamManager = new StreamManager(opts.getSavePath, hasSegment, this.disableDanma, opts.videoFormat, {
|
|
26
|
+
onUpdateLiveInfo: this.onUpdateLiveInfo,
|
|
27
|
+
});
|
|
28
|
+
this.timeoutChecker = utils.createTimeoutChecker(() => this.onEnd("ffmpeg timeout"), 3 * 10e3, false);
|
|
24
29
|
this.hasSegment = hasSegment;
|
|
25
30
|
this.getSavePath = opts.getSavePath;
|
|
26
31
|
this.ffmpegOutputOptions = opts.outputOptions;
|
|
27
32
|
this.inputOptions = opts.inputOptions ?? [];
|
|
28
33
|
this.url = opts.url;
|
|
29
34
|
this.segment = opts.segment;
|
|
30
|
-
this.
|
|
35
|
+
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
|
+
}
|
|
31
42
|
this.command = this.createCommand();
|
|
32
|
-
this.streamManager.on("videoFileCreated", ({ filename }) => {
|
|
33
|
-
this.emit("videoFileCreated", { filename });
|
|
43
|
+
this.streamManager.on("videoFileCreated", ({ filename, cover }) => {
|
|
44
|
+
this.emit("videoFileCreated", { filename, cover });
|
|
34
45
|
});
|
|
35
46
|
this.streamManager.on("videoFileCompleted", ({ filename }) => {
|
|
36
47
|
this.emit("videoFileCompleted", { filename });
|
|
@@ -40,15 +51,28 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
40
51
|
});
|
|
41
52
|
}
|
|
42
53
|
createCommand() {
|
|
54
|
+
this.timeoutChecker?.start();
|
|
43
55
|
const invalidCount = this.isHls ? 35 : 15;
|
|
44
56
|
const isInvalidStream = createInvalidStreamChecker(invalidCount);
|
|
45
|
-
const
|
|
46
|
-
.input(this.url)
|
|
47
|
-
.inputOptions([
|
|
57
|
+
const inputOptions = [
|
|
48
58
|
...this.inputOptions,
|
|
49
59
|
"-user_agent",
|
|
50
60
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36",
|
|
51
|
-
]
|
|
61
|
+
];
|
|
62
|
+
if (this.headers) {
|
|
63
|
+
const headers = [];
|
|
64
|
+
Object.entries(this.headers).forEach(([key, value]) => {
|
|
65
|
+
if (!value)
|
|
66
|
+
return;
|
|
67
|
+
headers.push(`${key}:${value}`);
|
|
68
|
+
});
|
|
69
|
+
if (headers.length) {
|
|
70
|
+
inputOptions.push("-headers", headers.join("\\r\\n"));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const command = createFFMPEGBuilder()
|
|
74
|
+
.input(this.url)
|
|
75
|
+
.inputOptions(inputOptions)
|
|
52
76
|
.outputOptions(this.ffmpegOutputOptions)
|
|
53
77
|
.output(this.streamManager.videoFilePath)
|
|
54
78
|
.on("error", this.onEnd)
|
|
@@ -56,7 +80,6 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
56
80
|
.on("stderr", async (stderrLine) => {
|
|
57
81
|
assert(typeof stderrLine === "string");
|
|
58
82
|
await this.streamManager.handleVideoStarted(stderrLine);
|
|
59
|
-
// TODO:解析时间
|
|
60
83
|
this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
|
|
61
84
|
const info = this.formatLine(stderrLine);
|
|
62
85
|
if (info) {
|
|
@@ -66,7 +89,7 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
66
89
|
this.onEnd("invalid stream");
|
|
67
90
|
}
|
|
68
91
|
})
|
|
69
|
-
.on("stderr", this.timeoutChecker
|
|
92
|
+
.on("stderr", this.timeoutChecker?.update);
|
|
70
93
|
if (this.hasSegment) {
|
|
71
94
|
command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
|
|
72
95
|
}
|
|
@@ -94,8 +117,9 @@ export class FFMPEGRecorder extends EventEmitter {
|
|
|
94
117
|
async stop() {
|
|
95
118
|
this.timeoutChecker.stop();
|
|
96
119
|
try {
|
|
120
|
+
this.command.kill("SIGINT");
|
|
97
121
|
// @ts-ignore
|
|
98
|
-
this.command.ffmpegProc?.stdin?.write("q");
|
|
122
|
+
// this.command.ffmpegProc?.stdin?.write("q");
|
|
99
123
|
await this.streamManager.handleVideoCompleted();
|
|
100
124
|
}
|
|
101
125
|
catch (err) {
|
package/lib/common.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const Qualities: readonly ["lowest", "low", "medium", "high", "hi
|
|
|
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
6
|
export declare const HuYaQualities: readonly [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
|
|
7
|
-
export declare const DouYinQualities: readonly ["origin", "uhd", "hd", "sd", "ld", "ao"];
|
|
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> {
|
|
10
10
|
uid?: string;
|
package/lib/common.js
CHANGED
|
@@ -3,4 +3,4 @@ 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
5
|
export const HuYaQualities = [0, 20000, 14100, 14000, 10000, 8000, 4200, 4000, 2000, 500];
|
|
6
|
-
export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao"];
|
|
6
|
+
export const DouYinQualities = ["origin", "uhd", "hd", "sd", "ld", "ao", "real_origin"];
|
package/lib/manager.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
36
36
|
videoFileCreated: {
|
|
37
37
|
recorder: Recorder<E>;
|
|
38
38
|
filename: string;
|
|
39
|
+
cover?: string;
|
|
39
40
|
};
|
|
40
41
|
videoFileCompleted: {
|
|
41
42
|
recorder: Recorder<E>;
|
|
@@ -75,6 +76,7 @@ export interface RecorderManager<ME extends UnknownObject, P extends RecorderPro
|
|
|
75
76
|
removeRecorder: (this: RecorderManager<ME, P, PE, E>, recorder: Recorder<E>) => void;
|
|
76
77
|
startRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
77
78
|
stopRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
79
|
+
cutRecord: (this: RecorderManager<ME, P, PE, E>, id: string) => Promise<Recorder<E> | undefined>;
|
|
78
80
|
autoCheckInterval: number;
|
|
79
81
|
isCheckLoopRunning: boolean;
|
|
80
82
|
startCheckLoop: (this: RecorderManager<ME, P, PE, E>) => void;
|
package/lib/manager.js
CHANGED
|
@@ -54,6 +54,8 @@ export function createRecorderManager(opts) {
|
|
|
54
54
|
}
|
|
55
55
|
catch (err) {
|
|
56
56
|
manager.emit("error", { source: "getBiliStatusInfoByRoomIds", err });
|
|
57
|
+
// 如果批量查询失败,则使用单个查询
|
|
58
|
+
needCheckRecorders = needCheckRecorders.concat(biliNeedCheckRecorders);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
const checkOnce = async () => {
|
|
@@ -102,10 +104,10 @@ export function createRecorderManager(opts) {
|
|
|
102
104
|
this.recorders.push(recorder);
|
|
103
105
|
recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
|
|
104
106
|
recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
|
|
105
|
-
recorder.on("videoFileCreated", ({ filename }) => {
|
|
107
|
+
recorder.on("videoFileCreated", ({ filename, cover }) => {
|
|
106
108
|
if (recorder.saveCover && recorder?.liveInfo?.cover) {
|
|
107
109
|
const coverPath = replaceExtName(filename, ".jpg");
|
|
108
|
-
downloadImage(recorder?.liveInfo?.cover, coverPath);
|
|
110
|
+
downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
|
|
109
111
|
}
|
|
110
112
|
this.emit("videoFileCreated", { recorder, filename });
|
|
111
113
|
});
|
|
@@ -117,8 +119,10 @@ export function createRecorderManager(opts) {
|
|
|
117
119
|
recorder.on("progress", (progress) => {
|
|
118
120
|
this.emit("RecorderProgress", { recorder, progress });
|
|
119
121
|
});
|
|
120
|
-
recorder.on("
|
|
121
|
-
|
|
122
|
+
recorder.on("videoFileCreated", () => {
|
|
123
|
+
if (!recorder.liveInfo?.liveId)
|
|
124
|
+
return;
|
|
125
|
+
const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
|
|
122
126
|
if (liveStartObj[key])
|
|
123
127
|
return;
|
|
124
128
|
liveStartObj[key] = true;
|
|
@@ -166,6 +170,15 @@ export function createRecorderManager(opts) {
|
|
|
166
170
|
}
|
|
167
171
|
return recorder;
|
|
168
172
|
},
|
|
173
|
+
async cutRecord(id) {
|
|
174
|
+
const recorder = this.recorders.find((item) => item.id === id);
|
|
175
|
+
if (recorder == null)
|
|
176
|
+
return;
|
|
177
|
+
if (recorder.recordHandle == null)
|
|
178
|
+
return;
|
|
179
|
+
await recorder.recordHandle.cut();
|
|
180
|
+
return recorder;
|
|
181
|
+
},
|
|
169
182
|
autoCheckInterval: opts.autoCheckInterval ?? 1000,
|
|
170
183
|
isCheckLoopRunning: false,
|
|
171
184
|
startCheckLoop() {
|
|
@@ -219,7 +232,7 @@ export function createRecorderManager(opts) {
|
|
|
219
232
|
*
|
|
220
233
|
* TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
|
|
221
234
|
*/
|
|
222
|
-
" -min_frag_duration
|
|
235
|
+
" -min_frag_duration 10000000",
|
|
223
236
|
};
|
|
224
237
|
const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
|
|
225
238
|
const args = parseArgsStringToArgv(ffmpegOutputArgs);
|
|
@@ -62,7 +62,7 @@ export function convert2Xml(data) {
|
|
|
62
62
|
const comments = data.messages
|
|
63
63
|
.filter((item) => item.type === "comment")
|
|
64
64
|
.map((ele) => {
|
|
65
|
-
const progress = (ele.timestamp - metadata
|
|
65
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
66
66
|
const data = {
|
|
67
67
|
"@@p": "",
|
|
68
68
|
"@@progress": progress,
|
|
@@ -93,7 +93,7 @@ export function convert2Xml(data) {
|
|
|
93
93
|
const gifts = data.messages
|
|
94
94
|
.filter((item) => item.type === "give_gift")
|
|
95
95
|
.map((ele) => {
|
|
96
|
-
const progress = (ele.timestamp - metadata
|
|
96
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
97
97
|
const data = {
|
|
98
98
|
"@@ts": progress,
|
|
99
99
|
"@@giftname": String(ele.name),
|
|
@@ -108,7 +108,7 @@ export function convert2Xml(data) {
|
|
|
108
108
|
const superChats = data.messages
|
|
109
109
|
.filter((item) => item.type === "super_chat")
|
|
110
110
|
.map((ele) => {
|
|
111
|
-
const progress = (ele.timestamp - metadata
|
|
111
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
112
112
|
const data = {
|
|
113
113
|
"@@ts": progress,
|
|
114
114
|
"@@price": String(ele.price * 1000),
|
|
@@ -122,7 +122,7 @@ export function convert2Xml(data) {
|
|
|
122
122
|
const guardGift = data.messages
|
|
123
123
|
.filter((item) => item.type === "guard")
|
|
124
124
|
.map((ele) => {
|
|
125
|
-
const progress = (ele.timestamp - metadata
|
|
125
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
126
126
|
const data = {
|
|
127
127
|
"@@ts": progress,
|
|
128
128
|
"@@price": String(ele.price * 1000),
|
package/lib/recorder.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
14
14
|
quality: Quality;
|
|
15
15
|
streamPriorities: string[];
|
|
16
16
|
sourcePriorities: string[];
|
|
17
|
-
formatPriorities?:
|
|
17
|
+
formatPriorities?: Array<"flv" | "hls">;
|
|
18
18
|
source?: string;
|
|
19
19
|
segment?: number;
|
|
20
20
|
saveGiftDanma?: boolean;
|
|
@@ -41,6 +41,8 @@ export interface RecorderCreateOpts<E extends AnyObject = UnknownObject> {
|
|
|
41
41
|
titleKeywords?: string;
|
|
42
42
|
/** 用于指定录制文件格式,auto时,分段使用ts,不分段使用mp4 */
|
|
43
43
|
videoFormat?: "auto" | "ts" | "mkv";
|
|
44
|
+
/** 流格式优先级 */
|
|
45
|
+
formatriorities?: Array<"flv" | "hls">;
|
|
44
46
|
extra?: Partial<E>;
|
|
45
47
|
}
|
|
46
48
|
export type SerializedRecorder<E extends AnyObject> = PickRequired<RecorderCreateOpts<E>, "id">;
|
|
@@ -57,6 +59,7 @@ export interface RecordHandle {
|
|
|
57
59
|
progress?: Progress;
|
|
58
60
|
savePath: string;
|
|
59
61
|
stop: (this: RecordHandle, reason?: string, tempStopIntervalCheck?: boolean) => Promise<void>;
|
|
62
|
+
cut: (this: RecordHandle) => Promise<void>;
|
|
60
63
|
}
|
|
61
64
|
export interface DebugLog {
|
|
62
65
|
type: string | "common" | "ffmpeg";
|
|
@@ -72,14 +75,12 @@ export interface Recorder<E extends AnyObject = UnknownObject> extends Emitter<{
|
|
|
72
75
|
RecordSegment?: RecordHandle;
|
|
73
76
|
videoFileCreated: {
|
|
74
77
|
filename: string;
|
|
78
|
+
cover?: string;
|
|
75
79
|
};
|
|
76
80
|
videoFileCompleted: {
|
|
77
81
|
filename: string;
|
|
78
82
|
};
|
|
79
83
|
progress: Progress;
|
|
80
|
-
LiveStart: {
|
|
81
|
-
liveId: string;
|
|
82
|
-
};
|
|
83
84
|
RecordStop: {
|
|
84
85
|
recordHandle: RecordHandle;
|
|
85
86
|
reason?: string;
|
package/lib/streamManager.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import EventEmitter from "node:events";
|
|
|
2
2
|
import { createRecordExtraDataController } from "./record_extra_data_controller.js";
|
|
3
3
|
export type GetSavePath = (data: {
|
|
4
4
|
startTime: number;
|
|
5
|
+
title?: string;
|
|
5
6
|
}) => string;
|
|
6
7
|
export declare class Segment extends EventEmitter {
|
|
7
8
|
extraDataController: ReturnType<typeof createRecordExtraDataController> | null;
|
|
@@ -15,7 +16,12 @@ export declare class Segment extends EventEmitter {
|
|
|
15
16
|
videoExt: "ts" | "mkv" | "mp4";
|
|
16
17
|
constructor(getSavePath: GetSavePath, disableDanma: boolean, videoExt: "ts" | "mkv" | "mp4");
|
|
17
18
|
handleSegmentEnd(): Promise<void>;
|
|
18
|
-
onSegmentStart(stderrLine: string
|
|
19
|
+
onSegmentStart(stderrLine: string, callBack?: {
|
|
20
|
+
onUpdateLiveInfo: () => Promise<{
|
|
21
|
+
title?: string;
|
|
22
|
+
cover?: string;
|
|
23
|
+
}>;
|
|
24
|
+
}): Promise<void>;
|
|
19
25
|
get outputFilePath(): string;
|
|
20
26
|
}
|
|
21
27
|
export declare class StreamManager extends EventEmitter {
|
|
@@ -23,8 +29,15 @@ export declare class StreamManager extends EventEmitter {
|
|
|
23
29
|
private extraDataController;
|
|
24
30
|
recordSavePath: string;
|
|
25
31
|
recordStartTime?: number;
|
|
32
|
+
hasSegment: boolean;
|
|
26
33
|
private videoFormat?;
|
|
27
|
-
|
|
34
|
+
private callBack?;
|
|
35
|
+
constructor(getSavePath: GetSavePath, hasSegment: boolean, disableDanma: boolean, videoFormat?: "auto" | "ts" | "mkv", callBack?: {
|
|
36
|
+
onUpdateLiveInfo: () => Promise<{
|
|
37
|
+
title?: string;
|
|
38
|
+
cover?: string;
|
|
39
|
+
}>;
|
|
40
|
+
});
|
|
28
41
|
handleVideoStarted(stderrLine: string): Promise<void>;
|
|
29
42
|
handleVideoCompleted(): Promise<void>;
|
|
30
43
|
getExtraDataController(): import("./record_extra_data_controller.js").RecordExtraDataController | null;
|
package/lib/streamManager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventEmitter from "node:events";
|
|
2
|
-
import fs from "fs
|
|
2
|
+
import fs from "fs/promises";
|
|
3
3
|
import { createRecordExtraDataController } from "./record_extra_data_controller.js";
|
|
4
|
-
import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isFfmpegStart } from "./utils.js";
|
|
4
|
+
import { replaceExtName, ensureFolderExist, isFfmpegStartSegment, isFfmpegStart, retry, } from "./utils.js";
|
|
5
5
|
export class Segment extends EventEmitter {
|
|
6
6
|
extraDataController = null;
|
|
7
7
|
init = true;
|
|
@@ -28,7 +28,7 @@ export class Segment extends EventEmitter {
|
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
30
|
await Promise.all([
|
|
31
|
-
fs.rename(this.rawRecordingVideoPath, this.outputFilePath),
|
|
31
|
+
retry(() => fs.rename(this.rawRecordingVideoPath, this.outputFilePath)),
|
|
32
32
|
this.extraDataController?.flush(),
|
|
33
33
|
]);
|
|
34
34
|
this.emit("videoFileCompleted", { filename: this.outputFilePath });
|
|
@@ -40,14 +40,27 @@ export class Segment extends EventEmitter {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
async onSegmentStart(stderrLine) {
|
|
43
|
+
async onSegmentStart(stderrLine, callBack) {
|
|
44
44
|
if (!this.init) {
|
|
45
45
|
await this.handleSegmentEnd();
|
|
46
46
|
}
|
|
47
47
|
this.init = false;
|
|
48
48
|
const startTime = Date.now();
|
|
49
|
+
let liveInfo = { title: "", cover: "" };
|
|
50
|
+
if (callBack?.onUpdateLiveInfo) {
|
|
51
|
+
try {
|
|
52
|
+
liveInfo = await callBack.onUpdateLiveInfo();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
this.emit("DebugLog", {
|
|
56
|
+
type: "common",
|
|
57
|
+
text: "onUpdateLiveInfo error " + String(err),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
this.outputVideoFilePath = this.getSavePath({
|
|
50
62
|
startTime: startTime,
|
|
63
|
+
title: liveInfo?.title,
|
|
51
64
|
});
|
|
52
65
|
ensureFolderExist(this.outputVideoFilePath);
|
|
53
66
|
if (!this.disableDanma) {
|
|
@@ -58,7 +71,11 @@ export class Segment extends EventEmitter {
|
|
|
58
71
|
if (match) {
|
|
59
72
|
const filename = match[1];
|
|
60
73
|
this.rawRecordingVideoPath = filename;
|
|
61
|
-
this.emit("videoFileCreated", {
|
|
74
|
+
this.emit("videoFileCreated", {
|
|
75
|
+
filename: this.outputFilePath,
|
|
76
|
+
title: liveInfo?.title,
|
|
77
|
+
cover: liveInfo?.cover,
|
|
78
|
+
});
|
|
62
79
|
}
|
|
63
80
|
else {
|
|
64
81
|
this.emit("DebugLog", { type: "ffmpeg", text: "No match found" });
|
|
@@ -73,12 +90,16 @@ export class StreamManager extends EventEmitter {
|
|
|
73
90
|
extraDataController = null;
|
|
74
91
|
recordSavePath;
|
|
75
92
|
recordStartTime;
|
|
93
|
+
hasSegment;
|
|
76
94
|
videoFormat;
|
|
77
|
-
|
|
95
|
+
callBack;
|
|
96
|
+
constructor(getSavePath, hasSegment, disableDanma, videoFormat, callBack) {
|
|
78
97
|
super();
|
|
79
98
|
const recordSavePath = getSavePath({ startTime: Date.now() });
|
|
80
99
|
this.recordSavePath = recordSavePath;
|
|
81
100
|
this.videoFormat = videoFormat;
|
|
101
|
+
this.hasSegment = hasSegment;
|
|
102
|
+
this.callBack = callBack;
|
|
82
103
|
if (hasSegment) {
|
|
83
104
|
this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
|
|
84
105
|
this.segment.on("DebugLog", (data) => {
|
|
@@ -101,7 +122,7 @@ export class StreamManager extends EventEmitter {
|
|
|
101
122
|
async handleVideoStarted(stderrLine) {
|
|
102
123
|
if (this.segment) {
|
|
103
124
|
if (isFfmpegStartSegment(stderrLine)) {
|
|
104
|
-
await this.segment.onSegmentStart(stderrLine);
|
|
125
|
+
await this.segment.onSegmentStart(stderrLine, this.callBack);
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
else {
|
|
@@ -133,7 +154,7 @@ export class StreamManager extends EventEmitter {
|
|
|
133
154
|
return "mkv";
|
|
134
155
|
}
|
|
135
156
|
else if (this.videoFormat === "auto") {
|
|
136
|
-
if (!this.
|
|
157
|
+
if (!this.hasSegment) {
|
|
137
158
|
return "mp4";
|
|
138
159
|
}
|
|
139
160
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -39,9 +39,10 @@ export declare function isFfmpegStartSegment(line: string): boolean;
|
|
|
39
39
|
export declare function isFfmpegStart(line: string): boolean;
|
|
40
40
|
export declare const formatTemplate: (string: string, ...args: any[]) => string;
|
|
41
41
|
export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => boolean;
|
|
42
|
-
export declare function createTimeoutChecker(onTimeout: () => void, time: number): {
|
|
42
|
+
export declare function createTimeoutChecker(onTimeout: () => void, time: number, autoStart?: boolean): {
|
|
43
43
|
update: () => void;
|
|
44
44
|
stop: () => void;
|
|
45
|
+
start: () => void;
|
|
45
46
|
};
|
|
46
47
|
export declare function downloadImage(imageUrl: string, savePath: string): Promise<void>;
|
|
47
48
|
/**
|
|
@@ -52,6 +53,14 @@ export declare function downloadImage(imageUrl: string, savePath: string): Promi
|
|
|
52
53
|
* @returns 排序后的对象数组
|
|
53
54
|
*/
|
|
54
55
|
export declare function sortByKeyOrder<T, K extends keyof T>(objects: T[], order: T[K][], key: K): T[];
|
|
56
|
+
/**
|
|
57
|
+
* 重试执行异步函数
|
|
58
|
+
* @param fn 要重试的异步函数
|
|
59
|
+
* @param retries 重试次数,默认为3次
|
|
60
|
+
* @param delay 重试延迟时间(毫秒),默认为1000ms
|
|
61
|
+
* @returns Promise
|
|
62
|
+
*/
|
|
63
|
+
export declare function retry<T>(fn: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
|
|
55
64
|
declare const _default: {
|
|
56
65
|
replaceExtName: typeof replaceExtName;
|
|
57
66
|
singleton: typeof singleton;
|
|
@@ -69,5 +78,6 @@ declare const _default: {
|
|
|
69
78
|
md5: (str: string) => string;
|
|
70
79
|
uuid: () => `${string}-${string}-${string}-${string}-${string}`;
|
|
71
80
|
sortByKeyOrder: typeof sortByKeyOrder;
|
|
81
|
+
retry: typeof retry;
|
|
72
82
|
};
|
|
73
83
|
export default _default;
|
package/lib/utils.js
CHANGED
|
@@ -177,7 +177,7 @@ export function createInvalidStreamChecker(count = 15) {
|
|
|
177
177
|
return false;
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
-
export function createTimeoutChecker(onTimeout, time) {
|
|
180
|
+
export function createTimeoutChecker(onTimeout, time, autoStart = true) {
|
|
181
181
|
let timer = null;
|
|
182
182
|
let stopped = false;
|
|
183
183
|
const update = () => {
|
|
@@ -190,7 +190,13 @@ export function createTimeoutChecker(onTimeout, time) {
|
|
|
190
190
|
onTimeout();
|
|
191
191
|
}, time);
|
|
192
192
|
};
|
|
193
|
-
|
|
193
|
+
const start = () => {
|
|
194
|
+
stopped = false;
|
|
195
|
+
update();
|
|
196
|
+
};
|
|
197
|
+
if (autoStart) {
|
|
198
|
+
start();
|
|
199
|
+
}
|
|
194
200
|
return {
|
|
195
201
|
update,
|
|
196
202
|
stop() {
|
|
@@ -199,6 +205,7 @@ export function createTimeoutChecker(onTimeout, time) {
|
|
|
199
205
|
clearTimeout(timer);
|
|
200
206
|
timer = null;
|
|
201
207
|
},
|
|
208
|
+
start,
|
|
202
209
|
};
|
|
203
210
|
}
|
|
204
211
|
export async function downloadImage(imageUrl, savePath) {
|
|
@@ -231,6 +238,25 @@ export function sortByKeyOrder(objects, order, key) {
|
|
|
231
238
|
return indexA - indexB;
|
|
232
239
|
});
|
|
233
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* 重试执行异步函数
|
|
243
|
+
* @param fn 要重试的异步函数
|
|
244
|
+
* @param retries 重试次数,默认为3次
|
|
245
|
+
* @param delay 重试延迟时间(毫秒),默认为1000ms
|
|
246
|
+
* @returns Promise
|
|
247
|
+
*/
|
|
248
|
+
export async function retry(fn, retries = 3, delay = 1000) {
|
|
249
|
+
try {
|
|
250
|
+
return await fn();
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (retries <= 0) {
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
257
|
+
return retry(fn, retries - 1, delay);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
234
260
|
export default {
|
|
235
261
|
replaceExtName,
|
|
236
262
|
singleton,
|
|
@@ -248,4 +274,5 @@ export default {
|
|
|
248
274
|
md5,
|
|
249
275
|
uuid,
|
|
250
276
|
sortByKeyOrder,
|
|
277
|
+
retry,
|
|
251
278
|
};
|