@bililive-tools/bilibili-recorder 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/bilibili_api.d.ts +1 -2
- package/lib/danma.js +6 -3
- package/lib/index.js +64 -8
- package/lib/stream.js +14 -3
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +8 -0
- package/package.json +3 -3
package/lib/bilibili_api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type LiveStatus = 0 | 1 | 2;
|
|
1
|
+
export type LiveStatus = 0 | 1 | 2;
|
|
2
2
|
export declare function getRoomInit(roomIdOrShortId: number): Promise<{
|
|
3
3
|
room_id: number;
|
|
4
4
|
short_id: number;
|
|
@@ -110,4 +110,3 @@ export interface SourceProfile {
|
|
|
110
110
|
extra: string;
|
|
111
111
|
stream_ttl: number;
|
|
112
112
|
}
|
|
113
|
-
export {};
|
package/lib/danma.js
CHANGED
|
@@ -21,7 +21,7 @@ class DanmaClient extends EventEmitter {
|
|
|
21
21
|
return;
|
|
22
22
|
const comment = {
|
|
23
23
|
type: "comment",
|
|
24
|
-
timestamp: msg.timestamp,
|
|
24
|
+
timestamp: msg.body.timestamp,
|
|
25
25
|
text: content,
|
|
26
26
|
color: msg.body.content_color,
|
|
27
27
|
mode: msg.body.type,
|
|
@@ -41,7 +41,7 @@ class DanmaClient extends EventEmitter {
|
|
|
41
41
|
const content = msg.body.content.replaceAll(/[\r\n]/g, "");
|
|
42
42
|
const comment = {
|
|
43
43
|
type: "super_chat",
|
|
44
|
-
timestamp: msg.
|
|
44
|
+
timestamp: msg.raw.send_time,
|
|
45
45
|
text: content,
|
|
46
46
|
price: msg.body.price,
|
|
47
47
|
sender: {
|
|
@@ -79,7 +79,7 @@ class DanmaClient extends EventEmitter {
|
|
|
79
79
|
onGift: (msg) => {
|
|
80
80
|
const gift = {
|
|
81
81
|
type: "give_gift",
|
|
82
|
-
timestamp: msg.
|
|
82
|
+
timestamp: msg.raw.send_time,
|
|
83
83
|
name: msg.body.gift_name,
|
|
84
84
|
count: msg.body.amount,
|
|
85
85
|
price: msg.body.coin_type === "silver" ? 0 : msg.body.price / 1000,
|
|
@@ -98,6 +98,9 @@ class DanmaClient extends EventEmitter {
|
|
|
98
98
|
};
|
|
99
99
|
this.emit("Message", gift);
|
|
100
100
|
},
|
|
101
|
+
onRoomInfoChange: (msg) => {
|
|
102
|
+
this.emit("RoomInfoChange", msg);
|
|
103
|
+
},
|
|
101
104
|
};
|
|
102
105
|
this.client = startListen(this.roomId, handler, {
|
|
103
106
|
ws: {
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import mitt from "mitt";
|
|
3
3
|
import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, FFMPEGRecorder, } from "@bililive-tools/manager";
|
|
4
4
|
import { getInfo, getStream, getLiveStatus, getStrictStream } from "./stream.js";
|
|
5
|
-
import { ensureFolderExist } from "./utils.js";
|
|
5
|
+
import { ensureFolderExist, hasKeyword } from "./utils.js";
|
|
6
6
|
import DanmaClient from "./danma.js";
|
|
7
7
|
function createRecorder(opts) {
|
|
8
8
|
// 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
|
|
@@ -67,7 +67,7 @@ const ffmpegOutputOptions = [
|
|
|
67
67
|
"-movflags",
|
|
68
68
|
"faststart+frag_keyframe+empty_moov",
|
|
69
69
|
"-min_frag_duration",
|
|
70
|
-
"
|
|
70
|
+
"10000000",
|
|
71
71
|
];
|
|
72
72
|
const ffmpegInputOptions = [
|
|
73
73
|
"-reconnect",
|
|
@@ -103,7 +103,21 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
103
103
|
return null;
|
|
104
104
|
if (!living)
|
|
105
105
|
return null;
|
|
106
|
-
|
|
106
|
+
// 检查标题是否包含关键词,如果包含则不自动录制
|
|
107
|
+
// 手动开始录制时不检查标题关键词
|
|
108
|
+
if (!isManualStart &&
|
|
109
|
+
this.titleKeywords &&
|
|
110
|
+
typeof this.titleKeywords === "string" &&
|
|
111
|
+
this.titleKeywords.trim()) {
|
|
112
|
+
const hasTitleKeyword = hasKeyword(_title, this.titleKeywords);
|
|
113
|
+
if (hasTitleKeyword) {
|
|
114
|
+
this.emit("DebugLog", {
|
|
115
|
+
type: "common",
|
|
116
|
+
text: `跳过录制:直播间标题 "${_title}" 包含关键词 "${this.titleKeywords}"`,
|
|
117
|
+
});
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
107
121
|
const liveInfo = await getInfo(this.channelId);
|
|
108
122
|
const { owner, title, roomId } = liveInfo;
|
|
109
123
|
this.liveInfo = liveInfo;
|
|
@@ -165,8 +179,13 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
165
179
|
});
|
|
166
180
|
}, 50 * 60 * 1000);
|
|
167
181
|
}
|
|
182
|
+
let isCutting = false;
|
|
168
183
|
let isEnded = false;
|
|
169
184
|
const onEnd = (...args) => {
|
|
185
|
+
if (isCutting) {
|
|
186
|
+
isCutting = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
170
189
|
if (isEnded)
|
|
171
190
|
return;
|
|
172
191
|
isEnded = true;
|
|
@@ -182,11 +201,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
182
201
|
outputOptions: ffmpegOutputOptions,
|
|
183
202
|
inputOptions: ffmpegInputOptions,
|
|
184
203
|
segment: this.segment ?? 0,
|
|
185
|
-
getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
|
|
204
|
+
getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
|
|
186
205
|
isHls: streamOptions.protocol_name === "http_hls",
|
|
187
206
|
disableDanma: this.disableProvideCommentsWhenRecording,
|
|
188
207
|
videoFormat: this.videoFormat,
|
|
189
|
-
}, onEnd)
|
|
208
|
+
}, onEnd, async () => {
|
|
209
|
+
const info = await getInfo(this.channelId);
|
|
210
|
+
return info;
|
|
211
|
+
});
|
|
190
212
|
const savePath = getSavePath({
|
|
191
213
|
owner,
|
|
192
214
|
title,
|
|
@@ -198,8 +220,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
198
220
|
this.state = "idle";
|
|
199
221
|
throw err;
|
|
200
222
|
}
|
|
201
|
-
const handleVideoCreated = async ({ filename }) => {
|
|
202
|
-
this.emit("videoFileCreated", { filename });
|
|
223
|
+
const handleVideoCreated = async ({ filename, title, cover }) => {
|
|
224
|
+
this.emit("videoFileCreated", { filename, cover });
|
|
225
|
+
if (title && this?.liveInfo) {
|
|
226
|
+
this.liveInfo.title = title;
|
|
227
|
+
}
|
|
228
|
+
if (cover && this?.liveInfo) {
|
|
229
|
+
this.liveInfo.cover = cover;
|
|
230
|
+
}
|
|
203
231
|
const extraDataController = recorder.getExtraDataController();
|
|
204
232
|
extraDataController?.setMeta({
|
|
205
233
|
room_id: String(roomId),
|
|
@@ -225,7 +253,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
225
253
|
});
|
|
226
254
|
let danmaClient = new DanmaClient(roomId, this.auth, this.uid);
|
|
227
255
|
if (!this.disableProvideCommentsWhenRecording) {
|
|
228
|
-
danmaClient
|
|
256
|
+
danmaClient.on("Message", (msg) => {
|
|
229
257
|
const extraDataController = recorder.getExtraDataController();
|
|
230
258
|
if (!extraDataController)
|
|
231
259
|
return;
|
|
@@ -236,10 +264,37 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
236
264
|
this.emit("Message", msg);
|
|
237
265
|
extraDataController.addMessage(msg);
|
|
238
266
|
});
|
|
267
|
+
danmaClient.on("onRoomInfoChange", (msg) => {
|
|
268
|
+
if (!isManualStart &&
|
|
269
|
+
this.titleKeywords &&
|
|
270
|
+
typeof this.titleKeywords === "string" &&
|
|
271
|
+
this.titleKeywords.trim()) {
|
|
272
|
+
const title = msg?.body?.title ?? "";
|
|
273
|
+
const hasTitleKeyword = hasKeyword(title, this.titleKeywords);
|
|
274
|
+
if (hasTitleKeyword) {
|
|
275
|
+
this.emit("DebugLog", {
|
|
276
|
+
type: "common",
|
|
277
|
+
text: `检测到标题包含关键词,停止录制:直播间标题 "${title}" 包含关键词 "${this.titleKeywords}"`,
|
|
278
|
+
});
|
|
279
|
+
// 停止录制
|
|
280
|
+
this.recordHandle && this.recordHandle.stop("直播间标题包含关键词");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
239
284
|
danmaClient.start();
|
|
240
285
|
}
|
|
241
286
|
const ffmpegArgs = recorder.getArguments();
|
|
242
287
|
recorder.run();
|
|
288
|
+
const cut = utils.singleton(async () => {
|
|
289
|
+
if (!this.recordHandle)
|
|
290
|
+
return;
|
|
291
|
+
if (isCutting)
|
|
292
|
+
return;
|
|
293
|
+
isCutting = true;
|
|
294
|
+
await recorder.stop();
|
|
295
|
+
recorder.createCommand();
|
|
296
|
+
recorder.run();
|
|
297
|
+
});
|
|
243
298
|
const stop = utils.singleton(async (reason) => {
|
|
244
299
|
if (!this.recordHandle)
|
|
245
300
|
return;
|
|
@@ -271,6 +326,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
271
326
|
ffmpegArgs,
|
|
272
327
|
savePath: savePath,
|
|
273
328
|
stop,
|
|
329
|
+
cut,
|
|
274
330
|
};
|
|
275
331
|
this.emit("RecordStart", this.recordHandle);
|
|
276
332
|
return this.recordHandle;
|
package/lib/stream.js
CHANGED
|
@@ -155,10 +155,17 @@ async function getLiveInfo(roomIdOrShortId, opts) {
|
|
|
155
155
|
let streamInfo;
|
|
156
156
|
let streamOptions;
|
|
157
157
|
for (const condition of conditons) {
|
|
158
|
-
|
|
158
|
+
const streamList = res.playurl_info.playurl.stream
|
|
159
159
|
.find(({ protocol_name }) => protocol_name === condition.protocol_name)
|
|
160
160
|
?.format.find(({ format_name }) => format_name === condition.format_name)
|
|
161
|
-
?.codec.
|
|
161
|
+
?.codec.filter(({ codec_name }) => codec_name === condition.codec_name);
|
|
162
|
+
if (streamList && streamList.length > 1) {
|
|
163
|
+
// 由于录播姬直推hevc时,指定qn,服务端仍会返回其他画质的流,这里需要指定找一下流
|
|
164
|
+
streamInfo = streamList.find((item) => item.current_qn === opts.qn);
|
|
165
|
+
}
|
|
166
|
+
if (!streamInfo) {
|
|
167
|
+
streamInfo = streamList?.[0];
|
|
168
|
+
}
|
|
162
169
|
if (streamInfo) {
|
|
163
170
|
streamOptions = {
|
|
164
171
|
...condition,
|
|
@@ -167,7 +174,11 @@ async function getLiveInfo(roomIdOrShortId, opts) {
|
|
|
167
174
|
break;
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
|
-
console.log(
|
|
177
|
+
// console.log(
|
|
178
|
+
// "streamOptions",
|
|
179
|
+
// streamOptions,
|
|
180
|
+
// JSON.stringify(res.playurl_info.playurl.stream, null, 2),
|
|
181
|
+
// );
|
|
171
182
|
assert(streamInfo, "没有找到支持的流");
|
|
172
183
|
const streams = streamInfo.accept_qn.map((qn) => {
|
|
173
184
|
const qnDesc = res.playurl_info.playurl.g_qn_desc.find((item) => item.qn === qn);
|
package/lib/utils.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare function assertStringType(data: unknown, msg?: string): asserts d
|
|
|
21
21
|
export declare function assertNumberType(data: unknown, msg?: string): asserts data is number;
|
|
22
22
|
export declare function assertObjectType(data: unknown, msg?: string): asserts data is object;
|
|
23
23
|
export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => boolean;
|
|
24
|
+
export declare function hasKeyword(title: string, titleKeywords: string): boolean;
|
package/lib/utils.js
CHANGED
|
@@ -81,3 +81,11 @@ export function createInvalidStreamChecker(count = 10) {
|
|
|
81
81
|
return false;
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
export function hasKeyword(title, titleKeywords) {
|
|
85
|
+
const keywords = titleKeywords
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((k) => k.trim())
|
|
88
|
+
.filter((k) => k);
|
|
89
|
+
const hasTitleKeyword = keywords.some((keyword) => title.toLowerCase().includes(keyword.toLowerCase()));
|
|
90
|
+
return hasTitleKeyword;
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/bilibili-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "bililive-tools bilibili recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"blive-message-listener": "^0.5.0",
|
|
38
38
|
"mitt": "^3.0.1",
|
|
39
|
-
"tiny-bilibili-ws": "^1.0.
|
|
39
|
+
"tiny-bilibili-ws": "^1.0.2",
|
|
40
40
|
"lodash-es": "^4.17.21",
|
|
41
41
|
"axios": "^1.7.8",
|
|
42
|
-
"@bililive-tools/manager": "^1.
|
|
42
|
+
"@bililive-tools/manager": "^1.3.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc",
|