@bililive-tools/bilibili-recorder 1.3.0 → 1.5.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/README.md +2 -0
- package/lib/bilibili_api.d.ts +2 -0
- package/lib/bilibili_api.js +12 -0
- package/lib/danma.d.ts +7 -2
- package/lib/danma.js +64 -7
- package/lib/index.js +8 -2
- package/lib/stream.d.ts +1 -0
- package/lib/stream.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ interface Options {
|
|
|
43
43
|
disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
|
|
44
44
|
saveGiftDanma?: boolean; // 保存礼物弹幕,包含舰长
|
|
45
45
|
saveSCDanma?: boolean; // 保存SC
|
|
46
|
+
useServerTimestamp?: boolean; // 控制弹幕是否使用服务端时间戳,默认为true
|
|
46
47
|
saveCover?: boolean; // 保存封面
|
|
47
48
|
auth?: string; // 登录所需cookie
|
|
48
49
|
uid?: number; // cookie所有者uid,用于弹幕录制
|
|
@@ -51,6 +52,7 @@ interface Options {
|
|
|
51
52
|
useM3U8Proxy?: boolean; // 是否使用m3u8代理,由于hls及fmp4存在一个小时超时时间,需自行实现代理避免
|
|
52
53
|
m3u8ProxyUrl?: string; // 代理链接,文档待补充
|
|
53
54
|
videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
|
|
55
|
+
onlyAudio?: boolean; // 只录制音频,默认为否
|
|
54
56
|
}
|
|
55
57
|
```
|
|
56
58
|
|
package/lib/bilibili_api.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare function getPlayURL(roomId: number, opts?: {
|
|
|
71
71
|
export declare function getRoomPlayInfo(roomIdOrShortId: number, opts?: {
|
|
72
72
|
qn?: number;
|
|
73
73
|
cookie?: string;
|
|
74
|
+
onlyAudio?: boolean;
|
|
74
75
|
}): Promise<{
|
|
75
76
|
uid: number;
|
|
76
77
|
room_id: number;
|
|
@@ -85,6 +86,7 @@ export declare function getRoomPlayInfo(roomIdOrShortId: number, opts?: {
|
|
|
85
86
|
};
|
|
86
87
|
};
|
|
87
88
|
}>;
|
|
89
|
+
export declare function getBuvidConf(): Promise<any>;
|
|
88
90
|
export interface ProtocolInfo {
|
|
89
91
|
protocol_name: "http_stream" | "http_hls";
|
|
90
92
|
format: FormatInfo[];
|
package/lib/bilibili_api.js
CHANGED
|
@@ -61,6 +61,7 @@ export async function getRoomPlayInfo(roomIdOrShortId, opts = {}) {
|
|
|
61
61
|
codec: "0,1",
|
|
62
62
|
// 0 flv, 1 ts, 2 fmp4
|
|
63
63
|
format: "0,1,2",
|
|
64
|
+
only_audio: opts.onlyAudio ? "1" : "0",
|
|
64
65
|
},
|
|
65
66
|
headers: {
|
|
66
67
|
Cookie: opts.cookie,
|
|
@@ -69,3 +70,14 @@ export async function getRoomPlayInfo(roomIdOrShortId, opts = {}) {
|
|
|
69
70
|
assert(res.data.code === 0, `Unexpected resp, code ${res.data.code}, msg ${res.data.message}`);
|
|
70
71
|
return res.data.data;
|
|
71
72
|
}
|
|
73
|
+
export async function getBuvidConf() {
|
|
74
|
+
const res = await fetch("https://api.bilibili.com/x/frontend/finger/spi", {
|
|
75
|
+
headers: {
|
|
76
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
throw new Error(`Failed to get buvid conf: ${res.statusText}`);
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
return data;
|
|
83
|
+
}
|
package/lib/danma.d.ts
CHANGED
|
@@ -5,8 +5,13 @@ declare class DanmaClient extends EventEmitter {
|
|
|
5
5
|
private auth;
|
|
6
6
|
private uid;
|
|
7
7
|
private retryCount;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
private useServerTimestamp;
|
|
9
|
+
constructor(roomId: number, { auth, uid, useServerTimestamp, }: {
|
|
10
|
+
auth: string | undefined;
|
|
11
|
+
uid: number | undefined;
|
|
12
|
+
useServerTimestamp?: boolean;
|
|
13
|
+
});
|
|
14
|
+
start(): Promise<void>;
|
|
10
15
|
stop(): void;
|
|
11
16
|
}
|
|
12
17
|
export default DanmaClient;
|
package/lib/danma.js
CHANGED
|
@@ -1,18 +1,63 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { startListen } from "./blive-message-listener/index.js";
|
|
3
|
+
import { getBuvidConf } from "./bilibili_api.js";
|
|
4
|
+
// 全局缓存,一天过期时间 (24 * 60 * 60 * 1000 ms)
|
|
5
|
+
const CACHE_DURATION = 24 * 60 * 60 * 1000;
|
|
6
|
+
let buvidCache = null;
|
|
7
|
+
// 获取带缓存的 buvid 配置
|
|
8
|
+
async function getCachedBuvidConf() {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
// 检查缓存是否有效
|
|
11
|
+
if (buvidCache && now - buvidCache.timestamp < CACHE_DURATION) {
|
|
12
|
+
return buvidCache.data;
|
|
13
|
+
}
|
|
14
|
+
// 缓存失效或不存在,重新获取(带重试)
|
|
15
|
+
const info = await getBuvidConfWithRetry();
|
|
16
|
+
buvidCache = {
|
|
17
|
+
data: info,
|
|
18
|
+
timestamp: now,
|
|
19
|
+
};
|
|
20
|
+
return info;
|
|
21
|
+
}
|
|
22
|
+
// 带重试功能的 getBuvidConf
|
|
23
|
+
async function getBuvidConfWithRetry(maxRetries = 3, retryDelay = 1000) {
|
|
24
|
+
let lastError;
|
|
25
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await getBuvidConf();
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
// 如果是最后一次尝试,直接抛出错误
|
|
33
|
+
if (attempt === maxRetries) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
// 等待指定时间后重试,使用指数退避策略
|
|
37
|
+
const delay = retryDelay * Math.pow(2, attempt - 1);
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 这里不应该到达,但为了类型安全
|
|
42
|
+
throw lastError;
|
|
43
|
+
}
|
|
3
44
|
class DanmaClient extends EventEmitter {
|
|
4
45
|
client = null;
|
|
5
46
|
roomId;
|
|
6
47
|
auth;
|
|
7
48
|
uid;
|
|
8
49
|
retryCount = 10;
|
|
9
|
-
|
|
50
|
+
useServerTimestamp;
|
|
51
|
+
constructor(roomId, { auth, uid, useServerTimestamp, }) {
|
|
10
52
|
super();
|
|
11
53
|
this.roomId = roomId;
|
|
12
54
|
this.auth = auth;
|
|
13
55
|
this.uid = uid;
|
|
56
|
+
this.useServerTimestamp = useServerTimestamp ?? true;
|
|
14
57
|
}
|
|
15
|
-
start() {
|
|
58
|
+
async start() {
|
|
59
|
+
const info = await getCachedBuvidConf();
|
|
60
|
+
const buvid3 = info.data.b_3;
|
|
16
61
|
const handler = {
|
|
17
62
|
onIncomeDanmu: (msg) => {
|
|
18
63
|
let content = msg.body.content;
|
|
@@ -21,7 +66,7 @@ class DanmaClient extends EventEmitter {
|
|
|
21
66
|
return;
|
|
22
67
|
const comment = {
|
|
23
68
|
type: "comment",
|
|
24
|
-
timestamp: msg.body.timestamp,
|
|
69
|
+
timestamp: this.useServerTimestamp ? msg.body.timestamp : Date.now(),
|
|
25
70
|
text: content,
|
|
26
71
|
color: msg.body.content_color,
|
|
27
72
|
mode: msg.body.type,
|
|
@@ -41,7 +86,7 @@ class DanmaClient extends EventEmitter {
|
|
|
41
86
|
const content = msg.body.content.replaceAll(/[\r\n]/g, "");
|
|
42
87
|
const comment = {
|
|
43
88
|
type: "super_chat",
|
|
44
|
-
timestamp: msg.raw.send_time,
|
|
89
|
+
timestamp: this.useServerTimestamp ? msg.raw.send_time : Date.now(),
|
|
45
90
|
text: content,
|
|
46
91
|
price: msg.body.price,
|
|
47
92
|
sender: {
|
|
@@ -59,7 +104,7 @@ class DanmaClient extends EventEmitter {
|
|
|
59
104
|
onGuardBuy: (msg) => {
|
|
60
105
|
const gift = {
|
|
61
106
|
type: "guard",
|
|
62
|
-
timestamp: msg.timestamp,
|
|
107
|
+
timestamp: this.useServerTimestamp ? msg.timestamp : Date.now(),
|
|
63
108
|
name: msg.body.gift_name,
|
|
64
109
|
price: msg.body.price,
|
|
65
110
|
count: 1,
|
|
@@ -79,7 +124,7 @@ class DanmaClient extends EventEmitter {
|
|
|
79
124
|
onGift: (msg) => {
|
|
80
125
|
const gift = {
|
|
81
126
|
type: "give_gift",
|
|
82
|
-
timestamp: msg
|
|
127
|
+
timestamp: this.useServerTimestamp ? msg?.raw?.data?.timestamp * 1000 : Date.now(),
|
|
83
128
|
name: msg.body.gift_name,
|
|
84
129
|
count: msg.body.amount,
|
|
85
130
|
price: msg.body.coin_type === "silver" ? 0 : msg.body.price / 1000,
|
|
@@ -102,10 +147,22 @@ class DanmaClient extends EventEmitter {
|
|
|
102
147
|
this.emit("RoomInfoChange", msg);
|
|
103
148
|
},
|
|
104
149
|
};
|
|
150
|
+
let lastAuth = "";
|
|
151
|
+
if (this.auth?.includes("buvid3")) {
|
|
152
|
+
lastAuth = this.auth;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (this.auth) {
|
|
156
|
+
lastAuth = `${this.auth}; buvid3=${buvid3}`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lastAuth = `buvid3=${buvid3}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
105
162
|
this.client = startListen(this.roomId, handler, {
|
|
106
163
|
ws: {
|
|
107
164
|
headers: {
|
|
108
|
-
Cookie:
|
|
165
|
+
Cookie: lastAuth,
|
|
109
166
|
},
|
|
110
167
|
uid: this.uid ?? 0,
|
|
111
168
|
},
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ function createRecorder(opts) {
|
|
|
19
19
|
qualityMaxRetry: opts.qualityRetry ?? 0,
|
|
20
20
|
qualityRetry: opts.qualityRetry ?? 0,
|
|
21
21
|
useM3U8Proxy: opts.useM3U8Proxy ?? false,
|
|
22
|
+
useServerTimestamp: opts.useServerTimestamp ?? true,
|
|
22
23
|
m3u8ProxyUrl: opts.m3u8ProxyUrl,
|
|
23
24
|
formatName: opts.formatName ?? "auto",
|
|
24
25
|
codecName: opts.codecName ?? "auto",
|
|
@@ -141,6 +142,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
141
142
|
strictQuality: strictQuality,
|
|
142
143
|
formatName: this.formatName,
|
|
143
144
|
codecName: this.codecName,
|
|
145
|
+
onlyAudio: this.onlyAudio,
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
catch (err) {
|
|
@@ -233,7 +235,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
233
235
|
room_id: String(roomId),
|
|
234
236
|
platform: provider?.id,
|
|
235
237
|
liveStartTimestamp: liveInfo.startTime?.getTime(),
|
|
236
|
-
recordStopTimestamp: Date.now(),
|
|
238
|
+
// recordStopTimestamp: Date.now(),
|
|
237
239
|
title: title,
|
|
238
240
|
user_name: owner,
|
|
239
241
|
});
|
|
@@ -251,7 +253,11 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, b
|
|
|
251
253
|
}
|
|
252
254
|
this.emit("progress", progress);
|
|
253
255
|
});
|
|
254
|
-
let danmaClient = new DanmaClient(roomId,
|
|
256
|
+
let danmaClient = new DanmaClient(roomId, {
|
|
257
|
+
auth: this.auth,
|
|
258
|
+
uid: this.uid,
|
|
259
|
+
useServerTimestamp: this.useServerTimestamp,
|
|
260
|
+
});
|
|
255
261
|
if (!this.disableProvideCommentsWhenRecording) {
|
|
256
262
|
danmaClient.on("Message", (msg) => {
|
|
257
263
|
const extraDataController = recorder.getExtraDataController();
|
package/lib/stream.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality">
|
|
|
30
30
|
strictQuality?: boolean;
|
|
31
31
|
formatName: RecorderCreateOpts["formatName"];
|
|
32
32
|
codecName: RecorderCreateOpts["codecName"];
|
|
33
|
+
onlyAudio?: boolean;
|
|
33
34
|
}): Promise<{
|
|
34
35
|
currentStream: {
|
|
35
36
|
name: string;
|
package/lib/stream.js
CHANGED
|
@@ -211,6 +211,7 @@ export async function getStream(opts) {
|
|
|
211
211
|
cookie: opts.cookie,
|
|
212
212
|
formatName: opts.formatName,
|
|
213
213
|
codecName: opts.codecName,
|
|
214
|
+
onlyAudio: opts.onlyAudio,
|
|
214
215
|
});
|
|
215
216
|
// console.log(JSON.stringify(liveInfo, null, 2));
|
|
216
217
|
if (liveInfo.current_qn !== qn && opts.strictQuality) {
|
|
@@ -224,6 +225,7 @@ export async function getStream(opts) {
|
|
|
224
225
|
cookie: opts.cookie,
|
|
225
226
|
formatName: opts.formatName,
|
|
226
227
|
codecName: opts.codecName,
|
|
228
|
+
onlyAudio: opts.onlyAudio,
|
|
227
229
|
});
|
|
228
230
|
}
|
|
229
231
|
let expectSource = liveInfo.sources[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bililive-tools/bilibili-recorder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "bililive-tools bilibili recorder implemention",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
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.4.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc",
|