@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.
@@ -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.timestamp,
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.timestamp,
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
- "60000000",
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
- this.emit("LiveStart", { liveId });
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 = danmaClient.on("Message", (msg) => {
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
- streamInfo = res.playurl_info.playurl.stream
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.find(({ codec_name }) => codec_name === condition.codec_name);
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("streamOptions", streamOptions, res.playurl_info.playurl.stream);
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.2.0",
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.1",
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.2.0"
42
+ "@bililive-tools/manager": "^1.3.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc",