@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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Message } from "./common.js";
|
|
2
|
+
export interface XmlStreamData {
|
|
3
|
+
meta: {
|
|
4
|
+
title?: string;
|
|
5
|
+
recordStartTimestamp: number;
|
|
6
|
+
recordStopTimestamp?: number;
|
|
7
|
+
liveStartTimestamp?: number;
|
|
8
|
+
ffmpegArgs?: string[];
|
|
9
|
+
platform?: string;
|
|
10
|
+
user_name?: string;
|
|
11
|
+
room_id?: string;
|
|
12
|
+
};
|
|
13
|
+
/** 缓存的消息,待写入到文件 */
|
|
14
|
+
pendingMessages: Message[];
|
|
15
|
+
}
|
|
16
|
+
export interface XmlStreamController {
|
|
17
|
+
/** 设计上来说,外部程序不应该能直接修改 data 上的东西 */
|
|
18
|
+
readonly data: XmlStreamData;
|
|
19
|
+
addMessage: (message: Message) => void;
|
|
20
|
+
setMeta: (meta: Partial<XmlStreamData["meta"]>) => void;
|
|
21
|
+
flush: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createRecordExtraDataController(savePath: string): XmlStreamController;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML流式写入控制器,用于实时写入弹幕、礼物等信息到XML文件
|
|
3
|
+
* 相比原有的json方案,这个实现每隔5秒就会写入数据,减少内存占用和数据丢失风险
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { XMLBuilder } from "fast-xml-parser";
|
|
7
|
+
import { pick } from "lodash-es";
|
|
8
|
+
import { asyncThrottle } from "./utils.js";
|
|
9
|
+
export function createRecordExtraDataController(savePath) {
|
|
10
|
+
const data = {
|
|
11
|
+
meta: {
|
|
12
|
+
recordStartTimestamp: Date.now(),
|
|
13
|
+
},
|
|
14
|
+
pendingMessages: [],
|
|
15
|
+
};
|
|
16
|
+
let hasCompleted = false;
|
|
17
|
+
let isWriting = false;
|
|
18
|
+
let isInitialized = false;
|
|
19
|
+
// 初始化文件
|
|
20
|
+
const initializeFile = async () => {
|
|
21
|
+
if (isInitialized)
|
|
22
|
+
return;
|
|
23
|
+
isInitialized = true;
|
|
24
|
+
try {
|
|
25
|
+
// 创建XML文件头,使用占位符预留metadata位置
|
|
26
|
+
const header = `<?xml version="1.0" encoding="utf-8"?>\n<i>\n<!--METADATA_PLACEHOLDER-->\n`;
|
|
27
|
+
await fs.promises.writeFile(savePath, header);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error("初始化XML文件失败:", error);
|
|
31
|
+
isInitialized = false;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
// 每10秒写入一次数据
|
|
36
|
+
const scheduleWrite = asyncThrottle(() => writeToFile(), 10e3, {
|
|
37
|
+
immediateRunWhenEndOfDefer: true,
|
|
38
|
+
});
|
|
39
|
+
const writeToFile = async () => {
|
|
40
|
+
if (isWriting || hasCompleted || data.pendingMessages.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// 确保文件已初始化
|
|
44
|
+
await initializeFile();
|
|
45
|
+
isWriting = true;
|
|
46
|
+
try {
|
|
47
|
+
// 获取待写入的消息
|
|
48
|
+
const messagesToWrite = [...data.pendingMessages];
|
|
49
|
+
data.pendingMessages = [];
|
|
50
|
+
// 生成XML内容
|
|
51
|
+
const xmlContent = generateXmlContent(data.meta, messagesToWrite);
|
|
52
|
+
// 追加写入文件
|
|
53
|
+
await appendToXmlFile(savePath, xmlContent);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error("写入XML文件失败:", error);
|
|
57
|
+
// 如果写入失败,将消息重新加入队列
|
|
58
|
+
data.pendingMessages = [...data.pendingMessages];
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
isWriting = false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const addMessage = (message) => {
|
|
65
|
+
if (hasCompleted)
|
|
66
|
+
return;
|
|
67
|
+
// if (!isInitialized) return;
|
|
68
|
+
data.pendingMessages.push(message);
|
|
69
|
+
// 确保文件已初始化
|
|
70
|
+
initializeFile().catch(console.error);
|
|
71
|
+
scheduleWrite();
|
|
72
|
+
};
|
|
73
|
+
const setMeta = (meta) => {
|
|
74
|
+
if (hasCompleted)
|
|
75
|
+
return;
|
|
76
|
+
data.meta = {
|
|
77
|
+
...data.meta,
|
|
78
|
+
...meta,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const flush = async () => {
|
|
82
|
+
if (hasCompleted)
|
|
83
|
+
return;
|
|
84
|
+
hasCompleted = true;
|
|
85
|
+
scheduleWrite.cancel();
|
|
86
|
+
await initializeFile().catch(console.error);
|
|
87
|
+
// 写入剩余的数据
|
|
88
|
+
if (data.pendingMessages.length > 0) {
|
|
89
|
+
await writeToFile();
|
|
90
|
+
}
|
|
91
|
+
// 完成XML文件(添加结束标签等)
|
|
92
|
+
await finalizeXmlFile(savePath, data.meta);
|
|
93
|
+
// 清理内存
|
|
94
|
+
data.pendingMessages = [];
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
data,
|
|
98
|
+
addMessage,
|
|
99
|
+
setMeta,
|
|
100
|
+
flush,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 生成XML内容片段
|
|
105
|
+
*/
|
|
106
|
+
function generateXmlContent(metadata, messages) {
|
|
107
|
+
const builder = new XMLBuilder({
|
|
108
|
+
ignoreAttributes: false,
|
|
109
|
+
attributeNamePrefix: "@@",
|
|
110
|
+
format: true,
|
|
111
|
+
});
|
|
112
|
+
const comments = messages
|
|
113
|
+
.filter((item) => item.type === "comment")
|
|
114
|
+
.map((ele) => {
|
|
115
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
116
|
+
const data = {
|
|
117
|
+
"@@p": "",
|
|
118
|
+
"@@progress": progress,
|
|
119
|
+
"@@mode": String(ele.mode ?? 1),
|
|
120
|
+
"@@fontsize": String(25),
|
|
121
|
+
"@@color": String(parseInt((ele.color || "#ffffff").replace("#", ""), 16)),
|
|
122
|
+
"@@midHash": String(ele?.sender?.uid),
|
|
123
|
+
"#text": String(ele?.text || ""),
|
|
124
|
+
"@@ctime": String(ele.timestamp),
|
|
125
|
+
"@@pool": String(0),
|
|
126
|
+
"@@weight": String(0),
|
|
127
|
+
"@@user": String(ele.sender?.name),
|
|
128
|
+
"@@uid": String(ele?.sender?.uid),
|
|
129
|
+
"@@timestamp": String(ele.timestamp),
|
|
130
|
+
};
|
|
131
|
+
data["@@p"] = [
|
|
132
|
+
data["@@progress"],
|
|
133
|
+
data["@@mode"],
|
|
134
|
+
data["@@fontsize"],
|
|
135
|
+
data["@@color"],
|
|
136
|
+
data["@@ctime"],
|
|
137
|
+
data["@@pool"],
|
|
138
|
+
data["@@midHash"],
|
|
139
|
+
data["@@uid"],
|
|
140
|
+
data["@@weight"],
|
|
141
|
+
].join(",");
|
|
142
|
+
return pick(data, ["@@p", "#text", "@@user", "@@uid", "@@timestamp"]);
|
|
143
|
+
});
|
|
144
|
+
const gifts = messages
|
|
145
|
+
.filter((item) => item.type === "give_gift")
|
|
146
|
+
.map((ele) => {
|
|
147
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
148
|
+
return {
|
|
149
|
+
"@@ts": progress,
|
|
150
|
+
"@@giftname": String(ele.name),
|
|
151
|
+
"@@giftcount": String(ele.count),
|
|
152
|
+
"@@price": String(ele.price * 1000),
|
|
153
|
+
"@@user": String(ele.sender?.name),
|
|
154
|
+
"@@uid": String(ele?.sender?.uid),
|
|
155
|
+
"@@timestamp": String(ele.timestamp),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
const superChats = messages
|
|
159
|
+
.filter((item) => item.type === "super_chat")
|
|
160
|
+
.map((ele) => {
|
|
161
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
162
|
+
return {
|
|
163
|
+
"@@ts": progress,
|
|
164
|
+
"@@price": String(ele.price * 1000),
|
|
165
|
+
"#text": String(ele.text),
|
|
166
|
+
"@@user": String(ele.sender?.name),
|
|
167
|
+
"@@uid": String(ele?.sender?.uid),
|
|
168
|
+
"@@timestamp": String(ele.timestamp),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
const guardGift = messages
|
|
172
|
+
.filter((item) => item.type === "guard")
|
|
173
|
+
.map((ele) => {
|
|
174
|
+
const progress = Math.max((ele.timestamp - metadata.recordStartTimestamp) / 1000, 0);
|
|
175
|
+
return {
|
|
176
|
+
"@@ts": progress,
|
|
177
|
+
"@@price": String(ele.price * 1000),
|
|
178
|
+
"@@giftname": String(ele.name),
|
|
179
|
+
"@@giftcount": String(ele.count),
|
|
180
|
+
"@@level": String(ele.level),
|
|
181
|
+
"@@user": String(ele.sender?.name),
|
|
182
|
+
"@@uid": String(ele?.sender?.uid),
|
|
183
|
+
"@@timestamp": String(ele.timestamp),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
// 构建这一批消息的XML片段
|
|
187
|
+
const fragment = {
|
|
188
|
+
d: comments,
|
|
189
|
+
gift: gifts,
|
|
190
|
+
sc: superChats,
|
|
191
|
+
guard: guardGift,
|
|
192
|
+
};
|
|
193
|
+
return builder.build(fragment);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 追加内容到XML文件
|
|
197
|
+
*/
|
|
198
|
+
async function appendToXmlFile(filePath, content) {
|
|
199
|
+
try {
|
|
200
|
+
// 直接追加内容
|
|
201
|
+
await fs.promises.appendFile(filePath, content);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error(`写入XML文件失败: ${filePath}`, error);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 完成XML文件写入
|
|
210
|
+
*/
|
|
211
|
+
async function finalizeXmlFile(filePath, metadata) {
|
|
212
|
+
try {
|
|
213
|
+
const builder = new XMLBuilder({
|
|
214
|
+
ignoreAttributes: false,
|
|
215
|
+
attributeNamePrefix: "@@",
|
|
216
|
+
format: true,
|
|
217
|
+
});
|
|
218
|
+
// 生成metadata XML
|
|
219
|
+
const metadataXml = builder.build({
|
|
220
|
+
metadata: {
|
|
221
|
+
platform: metadata.platform,
|
|
222
|
+
video_start_time: metadata.recordStartTimestamp,
|
|
223
|
+
live_start_time: metadata.liveStartTimestamp,
|
|
224
|
+
room_title: metadata.title,
|
|
225
|
+
user_name: metadata.user_name,
|
|
226
|
+
room_id: metadata.room_id,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
// 读取文件内容
|
|
230
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
231
|
+
// 替换占位符为实际的metadata,并添加结束标签
|
|
232
|
+
const finalContent = content.replace("<!--METADATA_PLACEHOLDER-->", metadataXml) + "</i>";
|
|
233
|
+
// 写回文件
|
|
234
|
+
await fs.promises.writeFile(filePath, finalContent);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.error(`完成XML文件写入失败: ${filePath}`, error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Batch scheduling recorders",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"author": "renmu123",
|
|
33
33
|
"license": "LGPL",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@renmu/fluent-ffmpeg": "2.3.
|
|
35
|
+
"@renmu/fluent-ffmpeg": "2.3.3",
|
|
36
36
|
"fast-xml-parser": "^4.5.0",
|
|
37
|
-
"filenamify": "^
|
|
37
|
+
"filenamify": "^7.0.0",
|
|
38
38
|
"mitt": "^3.0.1",
|
|
39
39
|
"string-argv": "^0.3.2",
|
|
40
40
|
"lodash-es": "^4.17.21",
|
package/lib/FFMPEGRecorder.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import EventEmitter from "node:events";
|
|
2
|
-
export declare class FFMPEGRecorder extends EventEmitter {
|
|
3
|
-
private onEnd;
|
|
4
|
-
private onUpdateLiveInfo;
|
|
5
|
-
private command;
|
|
6
|
-
private streamManager;
|
|
7
|
-
private timeoutChecker;
|
|
8
|
-
hasSegment: boolean;
|
|
9
|
-
getSavePath: (data: {
|
|
10
|
-
startTime: number;
|
|
11
|
-
title?: string;
|
|
12
|
-
}) => string;
|
|
13
|
-
segment: number;
|
|
14
|
-
ffmpegOutputOptions: string[];
|
|
15
|
-
inputOptions: string[];
|
|
16
|
-
isHls: boolean;
|
|
17
|
-
disableDanma: boolean;
|
|
18
|
-
url: string;
|
|
19
|
-
headers: {
|
|
20
|
-
[key: string]: string | undefined;
|
|
21
|
-
} | undefined;
|
|
22
|
-
constructor(opts: {
|
|
23
|
-
url: string;
|
|
24
|
-
getSavePath: (data: {
|
|
25
|
-
startTime: number;
|
|
26
|
-
title?: string;
|
|
27
|
-
}) => string;
|
|
28
|
-
segment: number;
|
|
29
|
-
outputOptions: string[];
|
|
30
|
-
inputOptions?: string[];
|
|
31
|
-
isHls?: boolean;
|
|
32
|
-
disableDanma?: boolean;
|
|
33
|
-
videoFormat?: "auto" | "ts" | "mkv";
|
|
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;
|
|
42
|
-
formatLine(line: string): {
|
|
43
|
-
time: string | null;
|
|
44
|
-
} | null;
|
|
45
|
-
run(): void;
|
|
46
|
-
getArguments(): string[];
|
|
47
|
-
stop(): Promise<void>;
|
|
48
|
-
getExtraDataController(): import("./record_extra_data_controller.js").RecordExtraDataController | null;
|
|
49
|
-
}
|