@bililive-tools/bilibili-recorder 1.0.1 → 1.2.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 CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  # 安装
8
8
 
9
+ **建议所有录制器和manager包都升级到最新版,我不会对兼容性做过多考虑**
10
+
9
11
  `npm i @bililive-tools/bilibili-recorder @bililive-tools/manager`
10
12
 
11
13
  # 使用
@@ -33,11 +35,11 @@ manager.startCheckLoop();
33
35
  interface Options {
34
36
  channelId: string; // 长直播间ID,具体解析见文档,也可自行解析
35
37
  quality: number; // 见画质参数
36
- qualityRetry?: number; // 画质匹配重试次数
38
+ qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
37
39
  streamPriorities: []; // 废弃
38
40
  sourcePriorities: []; // 废弃
39
41
  disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
40
- segment?: number; // 分段参数
42
+ segment?: number; // 分段参数,单位分钟
41
43
  disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
42
44
  saveGiftDanma?: boolean; // 保存礼物弹幕,包含舰长
43
45
  saveSCDanma?: boolean; // 保存SC
@@ -48,6 +50,7 @@ interface Options {
48
50
  codecName?: CodecName; // 见 CodecName 参数
49
51
  useM3U8Proxy?: boolean; // 是否使用m3u8代理,由于hls及fmp4存在一个小时超时时间,需自行实现代理避免
50
52
  m3u8ProxyUrl?: string; // 代理链接,文档待补充
53
+ videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
51
54
  }
52
55
  ```
53
56
 
@@ -49,6 +49,7 @@ export declare function getRoomBaseInfo<RoomId extends number>(roomId: RoomId):
49
49
  live_time: string;
50
50
  live_status: LiveStatus;
51
51
  cover: string;
52
+ is_encrypted: boolean;
52
53
  }>>;
53
54
  export declare function getPlayURL(roomId: number, opts?: {
54
55
  useHLS?: boolean;
@@ -85,7 +86,7 @@ export declare function getRoomPlayInfo(roomIdOrShortId: number, opts?: {
85
86
  };
86
87
  }>;
87
88
  export interface ProtocolInfo {
88
- protocol_name: string | "http_stream" | "http_hls";
89
+ protocol_name: "http_stream" | "http_hls";
89
90
  format: FormatInfo[];
90
91
  }
91
92
  export interface FormatInfo {
package/lib/danma.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { EventEmitter } from "node:events";
2
+ declare class DanmaClient extends EventEmitter {
3
+ private client;
4
+ private roomId;
5
+ private auth;
6
+ private uid;
7
+ private retryCount;
8
+ constructor(roomId: number, auth: string | undefined, uid: number | undefined);
9
+ start(): void;
10
+ stop(): void;
11
+ }
12
+ export default DanmaClient;
package/lib/danma.js ADDED
@@ -0,0 +1,124 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { startListen } from "./blive-message-listener/index.js";
3
+ class DanmaClient extends EventEmitter {
4
+ client = null;
5
+ roomId;
6
+ auth;
7
+ uid;
8
+ retryCount = 10;
9
+ constructor(roomId, auth, uid) {
10
+ super();
11
+ this.roomId = roomId;
12
+ this.auth = auth;
13
+ this.uid = uid;
14
+ }
15
+ start() {
16
+ const handler = {
17
+ onIncomeDanmu: (msg) => {
18
+ let content = msg.body.content;
19
+ content = content.replace(/(^\s*)|(\s*$)/g, "").replace(/[\r\n]/g, "");
20
+ if (content === "")
21
+ return;
22
+ const comment = {
23
+ type: "comment",
24
+ timestamp: msg.timestamp,
25
+ text: content,
26
+ color: msg.body.content_color,
27
+ mode: msg.body.type,
28
+ sender: {
29
+ uid: String(msg.body.user.uid),
30
+ name: msg.body.user.uname,
31
+ avatar: msg.body.user.face,
32
+ extra: {
33
+ badgeName: msg.body.user.badge?.name,
34
+ badgeLevel: msg.body.user.badge?.level,
35
+ },
36
+ },
37
+ };
38
+ this.emit("Message", comment);
39
+ },
40
+ onIncomeSuperChat: (msg) => {
41
+ const content = msg.body.content.replaceAll(/[\r\n]/g, "");
42
+ const comment = {
43
+ type: "super_chat",
44
+ timestamp: msg.timestamp,
45
+ text: content,
46
+ price: msg.body.price,
47
+ sender: {
48
+ uid: String(msg.body.user.uid),
49
+ name: msg.body.user.uname,
50
+ avatar: msg.body.user.face,
51
+ extra: {
52
+ badgeName: msg.body.user.badge?.name,
53
+ badgeLevel: msg.body.user.badge?.level,
54
+ },
55
+ },
56
+ };
57
+ this.emit("Message", comment);
58
+ },
59
+ onGuardBuy: (msg) => {
60
+ const gift = {
61
+ type: "guard",
62
+ timestamp: msg.timestamp,
63
+ name: msg.body.gift_name,
64
+ price: msg.body.price,
65
+ count: 1,
66
+ level: msg.body.guard_level,
67
+ sender: {
68
+ uid: String(msg.body.user.uid),
69
+ name: msg.body.user.uname,
70
+ avatar: msg.body.user.face,
71
+ extra: {
72
+ badgeName: msg.body.user.badge?.name,
73
+ badgeLevel: msg.body.user.badge?.level,
74
+ },
75
+ },
76
+ };
77
+ this.emit("Message", gift);
78
+ },
79
+ onGift: (msg) => {
80
+ const gift = {
81
+ type: "give_gift",
82
+ timestamp: msg.timestamp,
83
+ name: msg.body.gift_name,
84
+ count: msg.body.amount,
85
+ price: msg.body.coin_type === "silver" ? 0 : msg.body.price / 1000,
86
+ sender: {
87
+ uid: String(msg.body.user.uid),
88
+ name: msg.body.user.uname,
89
+ avatar: msg.body.user.face,
90
+ extra: {
91
+ badgeName: msg.body.user.badge?.name,
92
+ badgeLevel: msg.body.user.badge?.level,
93
+ },
94
+ },
95
+ extra: {
96
+ hits: msg.body.combo?.combo_num,
97
+ },
98
+ };
99
+ this.emit("Message", gift);
100
+ },
101
+ };
102
+ this.client = startListen(this.roomId, handler, {
103
+ ws: {
104
+ headers: {
105
+ Cookie: this.auth ?? "",
106
+ },
107
+ uid: this.uid ?? 0,
108
+ },
109
+ });
110
+ this.client.live.on("error", (err) => {
111
+ this.retryCount -= 1;
112
+ if (this.retryCount > 0) {
113
+ setTimeout(() => {
114
+ this.client && this.client.reconnect();
115
+ }, 2000);
116
+ }
117
+ this.emit("error", err);
118
+ });
119
+ }
120
+ stop() {
121
+ this.client?.close();
122
+ }
123
+ }
124
+ export default DanmaClient;
package/lib/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { RecorderProvider } from "@bililive-tools/manager";
1
+ import type { RecorderProvider } from "@bililive-tools/manager";
2
2
  export declare const provider: RecorderProvider<Record<string, unknown>>;
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
  import mitt from "mitt";
3
- import { createFFMPEGBuilder, defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, StreamManager, utils, } from "@bililive-tools/manager";
3
+ import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, utils, FFMPEGRecorder, } from "@bililive-tools/manager";
4
4
  import { getInfo, getStream, getLiveStatus, getStrictStream } from "./stream.js";
5
- import { assertStringType, ensureFolderExist, createInvalidStreamChecker } from "./utils.js";
6
- import { startListen } from "./blive-message-listener/index.js";
5
+ import { ensureFolderExist } from "./utils.js";
6
+ import DanmaClient from "./danma.js";
7
7
  function createRecorder(opts) {
8
8
  // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
9
9
  // 此标志来操作这个对象的地方,不然会跳过 proxy 的拦截。
@@ -68,26 +68,30 @@ const ffmpegOutputOptions = [
68
68
  "faststart+frag_keyframe+empty_moov",
69
69
  "-min_frag_duration",
70
70
  "60000000",
71
+ ];
72
+ const ffmpegInputOptions = [
71
73
  "-reconnect",
72
74
  "1",
73
75
  "-reconnect_streamed",
74
76
  "1",
75
77
  "-reconnect_delay_max",
76
- "5",
78
+ "10",
77
79
  "-rw_timeout",
78
- "5000000",
80
+ "15000000",
81
+ "-headers",
82
+ "Referer:https://live.bilibili.com/",
79
83
  ];
80
- const checkLiveStatusAndRecord = async function ({ getSavePath, qualityRetry, banLiveId, }) {
84
+ const checkLiveStatusAndRecord = async function ({ getSavePath, isManualStart, banLiveId, }) {
81
85
  if (this.recordHandle != null)
82
86
  return this.recordHandle;
83
- const { living, liveId } = await getLiveStatus(this.channelId);
87
+ const { living, liveId, owner: _owner, title: _title } = await getLiveStatus(this.channelId);
84
88
  this.liveInfo = {
85
89
  living,
86
- owner: "",
87
- title: "",
90
+ owner: _owner,
91
+ title: _title,
88
92
  avatar: "",
89
93
  cover: "",
90
- liveId: "",
94
+ liveId: liveId,
91
95
  };
92
96
  if (liveId === banLiveId) {
93
97
  this.tempStopIntervalCheck = true;
@@ -99,16 +103,22 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, qualityRetry, ba
99
103
  return null;
100
104
  if (!living)
101
105
  return null;
106
+ this.emit("LiveStart", { liveId });
102
107
  const liveInfo = await getInfo(this.channelId);
103
- const { owner, title, roomId, cover } = liveInfo;
108
+ const { owner, title, roomId } = liveInfo;
104
109
  this.liveInfo = liveInfo;
105
- this.state = "recording";
106
110
  let res;
107
111
  // TODO: 先不做什么错误处理,就简单包一下预期上会有错误的地方
108
112
  try {
109
- let strictQuality = !!this.qualityRetry;
110
- if (qualityRetry !== undefined) {
111
- strictQuality = !!qualityRetry;
113
+ let strictQuality = false;
114
+ if (this.qualityRetry > 0) {
115
+ strictQuality = true;
116
+ }
117
+ if (this.qualityMaxRetry < 0) {
118
+ strictQuality = true;
119
+ }
120
+ if (isManualStart) {
121
+ strictQuality = false;
112
122
  }
113
123
  res = await getStream({
114
124
  channelId: this.channelId,
@@ -124,6 +134,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, qualityRetry, ba
124
134
  this.state = "idle";
125
135
  throw err;
126
136
  }
137
+ this.state = "recording";
127
138
  const { streamOptions, currentStream: stream, sources: availableSources, streams: availableStreams, } = res;
128
139
  this.availableStreams = availableStreams.map((s) => s.desc);
129
140
  this.availableSources = availableSources.map((s) => s.name);
@@ -145,16 +156,41 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, qualityRetry, ba
145
156
  format_name: streamOptions.format_name,
146
157
  codec_name: streamOptions.codec_name,
147
158
  });
148
- stream.url = url;
159
+ if (this.recordHandle) {
160
+ this.recordHandle.url = url;
161
+ }
149
162
  this.emit("DebugLog", {
150
163
  type: "common",
151
164
  text: `update stream: ${url}`,
152
165
  });
153
166
  }, 50 * 60 * 1000);
154
167
  }
155
- // console.log(streamOptions.protocol_name, url);
156
- const savePath = getSavePath({ owner, title });
157
- const hasSegment = !!this.segment;
168
+ let isEnded = false;
169
+ const onEnd = (...args) => {
170
+ if (isEnded)
171
+ return;
172
+ isEnded = true;
173
+ this.emit("DebugLog", {
174
+ type: "common",
175
+ text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
176
+ });
177
+ const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
178
+ this.recordHandle?.stop(reason);
179
+ };
180
+ const recorder = new FFMPEGRecorder({
181
+ url: url,
182
+ outputOptions: ffmpegOutputOptions,
183
+ inputOptions: ffmpegInputOptions,
184
+ segment: this.segment ?? 0,
185
+ getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
186
+ isHls: streamOptions.protocol_name === "http_hls",
187
+ disableDanma: this.disableProvideCommentsWhenRecording,
188
+ videoFormat: this.videoFormat,
189
+ }, onEnd);
190
+ const savePath = getSavePath({
191
+ owner,
192
+ title,
193
+ });
158
194
  try {
159
195
  ensureFolderExist(savePath);
160
196
  }
@@ -162,221 +198,70 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, qualityRetry, ba
162
198
  this.state = "idle";
163
199
  throw err;
164
200
  }
165
- const streamManager = new StreamManager(this, getSavePath, owner, title, savePath, hasSegment);
166
201
  const handleVideoCreated = async ({ filename }) => {
167
- const extraDataController = streamManager?.getExtraDataController();
202
+ this.emit("videoFileCreated", { filename });
203
+ const extraDataController = recorder.getExtraDataController();
168
204
  extraDataController?.setMeta({
169
205
  room_id: String(roomId),
170
206
  platform: provider?.id,
171
207
  liveStartTimestamp: liveInfo.startTime?.getTime(),
208
+ recordStopTimestamp: Date.now(),
209
+ title: title,
210
+ user_name: owner,
172
211
  });
173
- if (this.saveCover) {
174
- const coverPath = utils.replaceExtName(filename, ".jpg");
175
- utils.downloadImage(cover, coverPath);
176
- }
177
212
  };
178
- this.on("videoFileCreated", handleVideoCreated);
179
- let client = null;
213
+ recorder.on("videoFileCreated", handleVideoCreated);
214
+ recorder.on("videoFileCompleted", ({ filename }) => {
215
+ this.emit("videoFileCompleted", { filename });
216
+ });
217
+ recorder.on("DebugLog", (data) => {
218
+ this.emit("DebugLog", data);
219
+ });
220
+ recorder.on("progress", (progress) => {
221
+ if (this.recordHandle) {
222
+ this.recordHandle.progress = progress;
223
+ }
224
+ this.emit("progress", progress);
225
+ });
226
+ let danmaClient = new DanmaClient(roomId, this.auth, this.uid);
180
227
  if (!this.disableProvideCommentsWhenRecording) {
181
- const handler = {
182
- onIncomeDanmu: (msg) => {
183
- const extraDataController = streamManager.getExtraDataController();
184
- if (!extraDataController)
185
- return;
186
- let content = msg.body.content;
187
- // 去除前后空格,回车,换行
188
- content = content.replace(/(^\s*)|(\s*$)/g, "").replace(/[\r\n]/g, "");
189
- if (content === "")
190
- return;
191
- const comment = {
192
- type: "comment",
193
- timestamp: msg.timestamp,
194
- text: content,
195
- color: msg.body.content_color,
196
- mode: msg.body.type,
197
- sender: {
198
- uid: String(msg.body.user.uid),
199
- name: msg.body.user.uname,
200
- avatar: msg.body.user.face,
201
- extra: {
202
- badgeName: msg.body.user.badge?.name,
203
- badgeLevel: msg.body.user.badge?.level,
204
- },
205
- },
206
- };
207
- this.emit("Message", comment);
208
- extraDataController.addMessage(comment);
209
- },
210
- onIncomeSuperChat: (msg) => {
211
- const extraDataController = streamManager.getExtraDataController();
212
- if (!extraDataController)
213
- return;
214
- if (this.saveSCDanma === false)
215
- return;
216
- const content = msg.body.content.replaceAll(/[\r\n]/g, "");
217
- // console.log(msg.id, msg.body);
218
- const comment = {
219
- type: "super_chat",
220
- timestamp: msg.timestamp,
221
- text: content,
222
- price: msg.body.price,
223
- sender: {
224
- uid: String(msg.body.user.uid),
225
- name: msg.body.user.uname,
226
- avatar: msg.body.user.face,
227
- extra: {
228
- badgeName: msg.body.user.badge?.name,
229
- badgeLevel: msg.body.user.badge?.level,
230
- },
231
- },
232
- };
233
- this.emit("Message", comment);
234
- extraDataController.addMessage(comment);
235
- },
236
- onGuardBuy: (msg) => {
237
- const extraDataController = streamManager.getExtraDataController();
238
- if (!extraDataController)
239
- return;
240
- // console.log("guard", msg);
241
- if (this.saveGiftDanma === false)
242
- return;
243
- const gift = {
244
- type: "guard",
245
- timestamp: msg.timestamp,
246
- name: msg.body.gift_name,
247
- price: msg.body.price,
248
- count: 1,
249
- level: msg.body.guard_level,
250
- sender: {
251
- uid: String(msg.body.user.uid),
252
- name: msg.body.user.uname,
253
- avatar: msg.body.user.face,
254
- extra: {
255
- badgeName: msg.body.user.badge?.name,
256
- badgeLevel: msg.body.user.badge?.level,
257
- },
258
- },
259
- };
260
- this.emit("Message", gift);
261
- extraDataController.addMessage(gift);
262
- },
263
- onGift: (msg) => {
264
- const extraDataController = streamManager.getExtraDataController();
265
- if (!extraDataController)
266
- return;
267
- // console.log("gift", msg);
268
- if (this.saveGiftDanma === false)
269
- return;
270
- const gift = {
271
- type: "give_gift",
272
- timestamp: msg.timestamp,
273
- name: msg.body.gift_name,
274
- count: msg.body.amount,
275
- price: msg.body.coin_type === "silver" ? 0 : msg.body.price / 1000,
276
- sender: {
277
- uid: String(msg.body.user.uid),
278
- name: msg.body.user.uname,
279
- avatar: msg.body.user.face,
280
- extra: {
281
- badgeName: msg.body.user.badge?.name,
282
- badgeLevel: msg.body.user.badge?.level,
283
- },
284
- },
285
- extra: {
286
- hits: msg.body.combo?.combo_num,
287
- },
288
- };
289
- this.emit("Message", gift);
290
- extraDataController.addMessage(gift);
291
- },
292
- };
293
- // 弹幕协议不能走短 id,所以不能直接用 channelId。
294
- client = startListen(roomId, handler, {
295
- ws: {
296
- headers: {
297
- Cookie: this.auth ?? "",
298
- },
299
- uid: this.uid ?? 0,
300
- },
228
+ danmaClient = danmaClient.on("Message", (msg) => {
229
+ const extraDataController = recorder.getExtraDataController();
230
+ if (!extraDataController)
231
+ return;
232
+ if (msg.type === "super_chat" && this.saveSCDanma === false)
233
+ return;
234
+ if ((msg.type === "give_gift" || msg.type === "guard") && this.saveGiftDanma === false)
235
+ return;
236
+ this.emit("Message", msg);
237
+ extraDataController.addMessage(msg);
301
238
  });
239
+ danmaClient.start();
302
240
  }
303
- let isEnded = false;
304
- const onEnd = (...args) => {
305
- if (isEnded)
306
- return;
307
- isEnded = true;
308
- this.emit("DebugLog", {
309
- type: "common",
310
- text: `ffmpeg end, reason: ${JSON.stringify(args, (_, v) => (v instanceof Error ? v.stack : v))}`,
311
- });
312
- const reason = args[0] instanceof Error ? args[0].message : String(args[0]);
313
- this.recordHandle?.stop(reason);
314
- };
315
- const isInvalidStream = createInvalidStreamChecker();
316
- const timeoutChecker = utils.createTimeoutChecker(() => onEnd("ffmpeg timeout"), 3 * 10e3);
317
- const command = createFFMPEGBuilder()
318
- .input(url)
319
- .addInputOptions("-user_agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:107.0) Gecko/20100101 Firefox/107.0", "-headers", "Referer: https://live.bilibili.com/")
320
- .outputOptions(ffmpegOutputOptions)
321
- .output(streamManager.videoFilePath)
322
- .on("start", async () => {
323
- try {
324
- await streamManager.handleVideoStarted();
325
- }
326
- catch (err) {
327
- onEnd("ffmpeg start error");
328
- this.emit("DebugLog", { type: "common", text: String(err) });
329
- }
330
- })
331
- .on("error", onEnd)
332
- .on("end", () => onEnd("finished"))
333
- .on("stderr", async (stderrLine) => {
334
- assertStringType(stderrLine);
335
- if (utils.isFfmpegStartSegment(stderrLine)) {
336
- try {
337
- await streamManager.handleVideoStarted(stderrLine);
338
- }
339
- catch (err) {
340
- onEnd("ffmpeg start error");
341
- this.emit("DebugLog", { type: "common", text: String(err) });
342
- }
343
- }
344
- this.emit("DebugLog", { type: "ffmpeg", text: stderrLine });
345
- if (isInvalidStream(stderrLine)) {
346
- onEnd("invalid stream");
347
- }
348
- })
349
- .on("stderr", timeoutChecker.update);
350
- if (hasSegment) {
351
- command.outputOptions("-f", "segment", "-segment_time", String(this.segment * 60), "-reset_timestamps", "1");
352
- }
353
- const ffmpegArgs = command._getArguments();
354
- command.run();
241
+ const ffmpegArgs = recorder.getArguments();
242
+ recorder.run();
355
243
  const stop = utils.singleton(async (reason) => {
356
244
  if (!this.recordHandle)
357
245
  return;
358
246
  this.state = "stopping-record";
359
- // TODO: emit update event
360
- timeoutChecker.stop();
247
+ intervalId && clearInterval(intervalId);
248
+ danmaClient.stop();
361
249
  try {
362
- // @ts-ignore
363
- command.ffmpegProc?.stdin?.write("q");
364
- client?.close();
365
- this.usedStream = undefined;
366
- this.usedSource = undefined;
367
- await streamManager.handleVideoCompleted();
250
+ await recorder.stop();
368
251
  }
369
252
  catch (err) {
370
- // TODO: 这个 stop 经常报错,这里先把错误吞掉,以后再处理。
371
- this.emit("DebugLog", { type: "common", text: String(err) });
253
+ this.emit("DebugLog", {
254
+ type: "common",
255
+ text: `stop ffmpeg error: ${String(err)}`,
256
+ });
372
257
  }
258
+ this.usedStream = undefined;
259
+ this.usedSource = undefined;
373
260
  this.emit("RecordStop", { recordHandle: this.recordHandle, reason });
374
- this.off("videoFileCreated", handleVideoCreated);
375
261
  this.recordHandle = undefined;
376
262
  this.liveInfo = undefined;
377
263
  this.state = "idle";
378
264
  this.qualityRetry = this.qualityMaxRetry;
379
- intervalId && clearInterval(intervalId);
380
265
  });
381
266
  this.recordHandle = {
382
267
  id: genRecordUUID(),
package/lib/stream.d.ts CHANGED
@@ -11,13 +11,14 @@ export declare function getStrictStream(roomId: number, options: {
11
11
  export declare function getLiveStatus(channelId: string): Promise<{
12
12
  living: boolean;
13
13
  liveId: string;
14
+ owner: string;
15
+ title: string;
14
16
  }>;
15
17
  export declare function getInfo(channelId: string): Promise<{
16
18
  living: boolean;
17
19
  owner: string;
18
20
  title: string;
19
21
  roomId: number;
20
- shortId: number;
21
22
  avatar: string;
22
23
  cover: string;
23
24
  startTime: Date;
@@ -39,7 +40,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality">
39
40
  sources: SourceProfile[];
40
41
  name: string;
41
42
  streamOptions: {
42
- protocol_name: string;
43
+ protocol_name: "http_stream" | "http_hls";
43
44
  format_name: string;
44
45
  codec_name: string;
45
46
  qn: number;
package/lib/stream.js CHANGED
@@ -13,12 +13,25 @@ export async function getStrictStream(roomId, options) {
13
13
  return url;
14
14
  }
15
15
  export async function getLiveStatus(channelId) {
16
+ const obj = await getRoomBaseInfo(Number(channelId));
17
+ const data = obj[Number(channelId)];
18
+ if (data) {
19
+ const startTime = new Date(data.live_time);
20
+ return {
21
+ living: data.live_status === 1 && !data.is_encrypted,
22
+ liveId: utils.md5(`${channelId}-${startTime?.getTime()}`),
23
+ owner: data.uname,
24
+ title: data.title,
25
+ };
26
+ }
16
27
  const roomInit = await getRoomInit(Number(channelId));
17
28
  const startTime = new Date(roomInit.live_time * 1000);
18
29
  return {
19
30
  living: roomInit.live_status === 1 && !roomInit.encrypted,
20
31
  liveId: utils.md5(`${roomInit.room_id}-${startTime?.getTime()}`),
21
32
  ...roomInit,
33
+ owner: "",
34
+ title: "",
22
35
  };
23
36
  }
24
37
  export async function getInfo(channelId) {
@@ -38,7 +51,6 @@ export async function getInfo(channelId) {
38
51
  avatar: "",
39
52
  cover: status.cover,
40
53
  roomId: roomInit.room_id,
41
- shortId: roomInit.short_id,
42
54
  liveId: utils.md5(`${roomInit.room_id}-${startTime?.getTime()}`),
43
55
  };
44
56
  }
@@ -51,7 +63,6 @@ export async function getInfo(channelId) {
51
63
  avatar: status.face,
52
64
  cover: status.cover_from_user,
53
65
  roomId: roomInit.room_id,
54
- shortId: roomInit.short_id,
55
66
  startTime: startTime,
56
67
  liveId: utils.md5(`${roomInit.room_id}-${startTime.getTime()}`),
57
68
  };
@@ -68,13 +79,13 @@ async function getLiveInfo(roomIdOrShortId, opts) {
68
79
  },
69
80
  {
70
81
  protocol_name: "http_hls",
71
- format_name: "fmp4",
82
+ format_name: "ts",
72
83
  codec_name: "avc",
73
84
  sort: 8,
74
85
  },
75
86
  {
76
87
  protocol_name: "http_hls",
77
- format_name: "ts",
88
+ format_name: "fmp4",
78
89
  codec_name: "avc",
79
90
  sort: 7,
80
91
  },
@@ -86,13 +97,13 @@ async function getLiveInfo(roomIdOrShortId, opts) {
86
97
  },
87
98
  {
88
99
  protocol_name: "http_hls",
89
- format_name: "fmp4",
100
+ format_name: "ts",
90
101
  codec_name: "hevc",
91
102
  sort: 5,
92
103
  },
93
104
  {
94
105
  protocol_name: "http_hls",
95
- format_name: "ts",
106
+ format_name: "fmp4",
96
107
  codec_name: "hevc",
97
108
  sort: 4,
98
109
  },
package/lib/utils.d.ts CHANGED
@@ -20,4 +20,4 @@ export declare function assert(assertion: unknown, msg?: string): asserts assert
20
20
  export declare function assertStringType(data: unknown, msg?: string): asserts data is string;
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
- export declare function createInvalidStreamChecker(): (ffmpegLogLine: string) => boolean;
23
+ export declare function createInvalidStreamChecker(count?: number): (ffmpegLogLine: string) => boolean;
package/lib/utils.js CHANGED
@@ -59,7 +59,7 @@ export function assertNumberType(data, msg) {
59
59
  export function assertObjectType(data, msg) {
60
60
  assert(typeof data === "object", msg);
61
61
  }
62
- export function createInvalidStreamChecker() {
62
+ export function createInvalidStreamChecker(count = 10) {
63
63
  let prevFrame = 0;
64
64
  let frameUnchangedCount = 0;
65
65
  return (ffmpegLogLine) => {
@@ -68,7 +68,7 @@ export function createInvalidStreamChecker() {
68
68
  const [, frameText] = streamInfo;
69
69
  const frame = Number(frameText);
70
70
  if (frame === prevFrame) {
71
- if (++frameUnchangedCount >= 10) {
71
+ if (++frameUnchangedCount >= count) {
72
72
  return true;
73
73
  }
74
74
  }
@@ -78,9 +78,6 @@ export function createInvalidStreamChecker() {
78
78
  }
79
79
  return false;
80
80
  }
81
- // if (ffmpegLogLine.includes("HTTP error 404 Not Found")) {
82
- // return true;
83
- // }
84
81
  return false;
85
82
  };
86
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/bilibili-recorder",
3
- "version": "1.0.1",
3
+ "version": "1.2.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.1",
40
40
  "lodash-es": "^4.17.21",
41
41
  "axios": "^1.7.8",
42
- "@bililive-tools/manager": "1.0.1"
42
+ "@bililive-tools/manager": "^1.2.0"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc",