@bililive-tools/manager 1.2.1 → 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 +16 -5
- package/lib/record_extra_data_controller.js +4 -4
- package/lib/recorder.d.ts +5 -4
- package/lib/streamManager.d.ts +14 -2
- package/lib/streamManager.js +26 -7
- 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
|
@@ -104,10 +104,10 @@ export function createRecorderManager(opts) {
|
|
|
104
104
|
this.recorders.push(recorder);
|
|
105
105
|
recorder.on("RecordStart", (recordHandle) => this.emit("RecordStart", { recorder, recordHandle }));
|
|
106
106
|
recorder.on("RecordSegment", (recordHandle) => this.emit("RecordSegment", { recorder, recordHandle }));
|
|
107
|
-
recorder.on("videoFileCreated", ({ filename }) => {
|
|
107
|
+
recorder.on("videoFileCreated", ({ filename, cover }) => {
|
|
108
108
|
if (recorder.saveCover && recorder?.liveInfo?.cover) {
|
|
109
109
|
const coverPath = replaceExtName(filename, ".jpg");
|
|
110
|
-
downloadImage(recorder?.liveInfo?.cover, coverPath);
|
|
110
|
+
downloadImage(cover ?? recorder?.liveInfo?.cover, coverPath);
|
|
111
111
|
}
|
|
112
112
|
this.emit("videoFileCreated", { recorder, filename });
|
|
113
113
|
});
|
|
@@ -119,8 +119,10 @@ export function createRecorderManager(opts) {
|
|
|
119
119
|
recorder.on("progress", (progress) => {
|
|
120
120
|
this.emit("RecorderProgress", { recorder, progress });
|
|
121
121
|
});
|
|
122
|
-
recorder.on("
|
|
123
|
-
|
|
122
|
+
recorder.on("videoFileCreated", () => {
|
|
123
|
+
if (!recorder.liveInfo?.liveId)
|
|
124
|
+
return;
|
|
125
|
+
const key = `${recorder.channelId}-${recorder.liveInfo?.liveId}`;
|
|
124
126
|
if (liveStartObj[key])
|
|
125
127
|
return;
|
|
126
128
|
liveStartObj[key] = true;
|
|
@@ -168,6 +170,15 @@ export function createRecorderManager(opts) {
|
|
|
168
170
|
}
|
|
169
171
|
return recorder;
|
|
170
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
|
+
},
|
|
171
182
|
autoCheckInterval: opts.autoCheckInterval ?? 1000,
|
|
172
183
|
isCheckLoopRunning: false,
|
|
173
184
|
startCheckLoop() {
|
|
@@ -221,7 +232,7 @@ export function createRecorderManager(opts) {
|
|
|
221
232
|
*
|
|
222
233
|
* TODO: 如果浏览器行为无法优化,并且想进一步优化加载速度,可以考虑录制时使用 fmp4,录制完成后再转一次普通 mp4。
|
|
223
234
|
*/
|
|
224
|
-
" -min_frag_duration
|
|
235
|
+
" -min_frag_duration 10000000",
|
|
225
236
|
};
|
|
226
237
|
const setProvidersFFMPEGOutputArgs = (ffmpegOutputArgs) => {
|
|
227
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 {
|
|
@@ -25,7 +31,13 @@ export declare class StreamManager extends EventEmitter {
|
|
|
25
31
|
recordStartTime?: number;
|
|
26
32
|
hasSegment: boolean;
|
|
27
33
|
private videoFormat?;
|
|
28
|
-
|
|
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
|
+
});
|
|
29
41
|
handleVideoStarted(stderrLine: string): Promise<void>;
|
|
30
42
|
handleVideoCompleted(): Promise<void>;
|
|
31
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" });
|
|
@@ -75,12 +92,14 @@ export class StreamManager extends EventEmitter {
|
|
|
75
92
|
recordStartTime;
|
|
76
93
|
hasSegment;
|
|
77
94
|
videoFormat;
|
|
78
|
-
|
|
95
|
+
callBack;
|
|
96
|
+
constructor(getSavePath, hasSegment, disableDanma, videoFormat, callBack) {
|
|
79
97
|
super();
|
|
80
98
|
const recordSavePath = getSavePath({ startTime: Date.now() });
|
|
81
99
|
this.recordSavePath = recordSavePath;
|
|
82
100
|
this.videoFormat = videoFormat;
|
|
83
101
|
this.hasSegment = hasSegment;
|
|
102
|
+
this.callBack = callBack;
|
|
84
103
|
if (hasSegment) {
|
|
85
104
|
this.segment = new Segment(getSavePath, disableDanma, this.videoExt);
|
|
86
105
|
this.segment.on("DebugLog", (data) => {
|
|
@@ -103,7 +122,7 @@ export class StreamManager extends EventEmitter {
|
|
|
103
122
|
async handleVideoStarted(stderrLine) {
|
|
104
123
|
if (this.segment) {
|
|
105
124
|
if (isFfmpegStartSegment(stderrLine)) {
|
|
106
|
-
await this.segment.onSegmentStart(stderrLine);
|
|
125
|
+
await this.segment.onSegmentStart(stderrLine, this.callBack);
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
else {
|
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
|
};
|