@bililive-tools/douyin-recorder 1.16.1 → 1.17.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.
@@ -18,6 +18,7 @@ export declare function getRoomInfo(webRoomId: string, opts?: {
18
18
  uid?: string | number;
19
19
  }): Promise<{
20
20
  living: boolean;
21
+ isLiveRadio?: boolean;
21
22
  roomId: string;
22
23
  owner: string;
23
24
  title: string;
package/lib/douyin_api.js CHANGED
@@ -159,6 +159,7 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
159
159
  if (!res.data.includes("直播中")) {
160
160
  return {
161
161
  living: false,
162
+ isLiveRadio: false,
162
163
  nickname: "",
163
164
  sec_uid: "",
164
165
  avatar: "",
@@ -177,19 +178,38 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
177
178
  .replace(/\\"/g, '"')
178
179
  .replace(/\\"/g, '"')
179
180
  .replace(/"\$\w+"/g, "null");
180
- // const roomRegex = /(\{\\"common\\":.*?)"\]\)/;
181
- // const roomMatch = res.data.match(roomRegex);
182
- // if (!roomMatch) {
183
- // throw new Error("No room match found in HTML");
184
- // }
185
- // let roomJsonStr = roomMatch[1];
186
- // roomJsonStr = roomJsonStr
187
- // .replace(/\\"/g, '"')
188
- // .replace(/\\"/g, '"')
189
- // .replace(/"\$\w+"/g, "null");
190
181
  try {
191
- // console.log(userJsonStr);
192
182
  const userData = JSON.parse(userJsonStr);
183
+ const roomData = userData?.user?.user?.roomData;
184
+ const streamUrl = roomData?.stream_url;
185
+ let liveCoreSdkData = null;
186
+ if (streamUrl) {
187
+ liveCoreSdkData = { live_core_sdk_data: streamUrl.live_core_sdk_data };
188
+ if (liveCoreSdkData?.live_core_sdk_data?.pull_data) {
189
+ const flvPullUrl = streamUrl.flv_pull_url;
190
+ let streamData = {};
191
+ for (const quality of [{ key: "or4", desc: "原画" }, ...qualityList]) {
192
+ const flvUrls = Object.values(flvPullUrl);
193
+ if (flvUrls.some((url) => url.includes(`${quality.key}`))) {
194
+ const url = flvUrls.find((url) => url.includes(`${quality.key}`));
195
+ const convertedQuality = quality.key === "or4" ? "origin" : quality.key;
196
+ streamData[convertedQuality] = {
197
+ // @ts-ignore
198
+ main: {
199
+ flv: url,
200
+ hls: "",
201
+ },
202
+ };
203
+ }
204
+ }
205
+ liveCoreSdkData.live_core_sdk_data.pull_data.stream_data = streamData;
206
+ }
207
+ else {
208
+ liveCoreSdkData = null;
209
+ }
210
+ }
211
+ // console.log("pppp", JSON.stringify(liveCoreSdkData, null, 2));
212
+ // console.log(JSON.stringify(streamUrl, null, 2));
193
213
  // console.log(JSON.stringify(userData, null, 2));
194
214
  // const roomData = JSON.parse(roomJsonStr);
195
215
  // console.log(roomData);
@@ -197,15 +217,16 @@ async function getRoomInfoByUserWeb(secUserId, opts = {}) {
197
217
  // const streamData = data.state.streamStore.streamData;
198
218
  return {
199
219
  living: userData?.user?.user?.roomData?.status === 2,
220
+ isLiveRadio: roomData?.live_type_audio ?? false,
200
221
  nickname: userData?.user?.user?.nickname ?? "",
201
222
  sec_uid: userData?.user?.user?.secUid ?? "",
202
223
  avatar: userData?.user?.user?.avatar ?? "",
203
- api: "webHTML",
224
+ api: "userHTML",
204
225
  room: {
205
226
  title: "",
206
227
  cover: "",
207
228
  id_str: userData?.user?.user?.roomIdStr,
208
- stream_url: null,
229
+ stream_url: liveCoreSdkData,
209
230
  },
210
231
  };
211
232
  }
@@ -250,8 +271,10 @@ async function getRoomInfoByHtml(webRoomId, opts = {}) {
250
271
  const data = JSON.parse(jsonStr);
251
272
  const roomInfo = data.state.roomStore.roomInfo;
252
273
  const streamData = data.state.streamStore.streamData;
274
+ const isLiveRadio = roomInfo.enter_mode == 1;
253
275
  return {
254
- living: roomInfo?.room?.status === 2,
276
+ living: roomInfo?.room?.status === 2 || isLiveRadio,
277
+ isLiveRadio: isLiveRadio,
255
278
  nickname: roomInfo?.anchor?.nickname ?? "",
256
279
  sec_uid: roomInfo?.anchor?.sec_uid ?? "",
257
280
  avatar: roomInfo?.anchor?.avatar_thumb?.url_list?.[0] ?? "",
@@ -260,15 +283,17 @@ async function getRoomInfoByHtml(webRoomId, opts = {}) {
260
283
  title: roomInfo?.room?.title ?? "",
261
284
  cover: roomInfo?.room?.cover?.url_list?.[0] ?? "",
262
285
  id_str: roomInfo?.room?.id_str ?? "",
263
- stream_url: {
264
- pull_datas: roomInfo?.room?.stream_url?.pull_datas,
265
- live_core_sdk_data: {
266
- pull_data: {
267
- options: { qualities: streamData.H264_streamData?.options?.qualities ?? [] },
268
- stream_data: streamData.H264_streamData?.stream ?? {},
286
+ stream_url: roomInfo?.room?.stream_url?.pull_datas
287
+ ? {
288
+ pull_datas: roomInfo?.room?.stream_url?.pull_datas,
289
+ live_core_sdk_data: {
290
+ pull_data: {
291
+ options: { qualities: streamData.H264_streamData?.options?.qualities ?? [] },
292
+ stream_data: streamData.H264_streamData?.stream ?? {},
293
+ },
269
294
  },
270
- },
271
- },
295
+ }
296
+ : null,
272
297
  },
273
298
  };
274
299
  }
@@ -316,6 +341,7 @@ async function getRoomInfoByWeb(webRoomId, opts = {}) {
316
341
  // 直播已结束
317
342
  return {
318
343
  living: false,
344
+ isLiveRadio: false,
319
345
  nickname: "",
320
346
  sec_uid: "",
321
347
  avatar: "",
@@ -332,7 +358,8 @@ async function getRoomInfoByWeb(webRoomId, opts = {}) {
332
358
  const data = res.data.data;
333
359
  const room = data?.data?.[0];
334
360
  return {
335
- living: data?.room_status === 0,
361
+ living: data?.room_status === 0 || data?.room_status === 1,
362
+ isLiveRadio: data?.room_status === 1,
336
363
  nickname: data?.user?.nickname ?? "",
337
364
  avatar: data?.user?.avatar_thumb?.url_list?.[0] ?? "",
338
365
  sec_uid: data?.user?.sec_uid ?? "",
@@ -371,6 +398,7 @@ async function getRoomInfoByMobile(secUserId, opts = {}) {
371
398
  const room = res?.data?.data?.room;
372
399
  return {
373
400
  living: room?.status === 2,
401
+ isLiveRadio: room?.live_type_audio ?? false,
374
402
  nickname: room?.owner?.nickname,
375
403
  sec_uid: room?.owner?.sec_uid,
376
404
  avatar: room?.owner?.avatar_thumb?.url_list?.[0],
@@ -408,11 +436,12 @@ export async function getRoomInfo(webRoomId, opts = {}) {
408
436
  else {
409
437
  data = await getRoomInfoByWeb(webRoomId, opts);
410
438
  }
411
- // console.log(JSON.stringify(data, null, 2));
412
439
  const room = data.room;
413
- if (api === "userHTML") {
440
+ assert(room, `No room data, id ${webRoomId}`);
441
+ if (!room?.stream_url) {
414
442
  return {
415
443
  living: data.living,
444
+ isLiveRadio: data.isLiveRadio,
416
445
  roomId: webRoomId,
417
446
  owner: data.nickname,
418
447
  title: room?.title ?? data.nickname,
@@ -425,22 +454,6 @@ export async function getRoomInfo(webRoomId, opts = {}) {
425
454
  api: data.api,
426
455
  };
427
456
  }
428
- assert(room, `No room data, id ${webRoomId}`);
429
- if (room?.stream_url == null) {
430
- return {
431
- living: false,
432
- roomId: webRoomId,
433
- owner: data.nickname,
434
- title: room?.title ?? data.nickname,
435
- streams: [],
436
- sources: [],
437
- avatar: data.avatar,
438
- cover: room.cover,
439
- liveId: room.id_str,
440
- uid: data.sec_uid,
441
- api: data.api,
442
- };
443
- }
444
457
  let qualities = [];
445
458
  let stream_data = "";
446
459
  if (opts.doubleScreen && !isEmpty(room.stream_url.pull_datas)) {
@@ -509,6 +522,7 @@ export async function getRoomInfo(webRoomId, opts = {}) {
509
522
  // console.log(JSON.stringify(sources, null, 2), qualities);
510
523
  return {
511
524
  living: data.living,
525
+ isLiveRadio: data.isLiveRadio,
512
526
  roomId: webRoomId,
513
527
  owner: data.nickname,
514
528
  title: room.title,
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ function createRecorder(opts) {
20
20
  useServerTimestamp: opts.useServerTimestamp ?? true,
21
21
  state: "idle",
22
22
  cache: null,
23
+ appendTimeline: null,
23
24
  getChannelURL() {
24
25
  return `https://live.douyin.com/${this.channelId}`;
25
26
  },
@@ -76,6 +77,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
76
77
  return this.recordHandle;
77
78
  }
78
79
  // 获取直播间信息
80
+ let isLiveRadio = false;
79
81
  try {
80
82
  const liveInfo = await getInfo(this.channelId, {
81
83
  auth: this.auth,
@@ -83,10 +85,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
83
85
  uid: this.uid,
84
86
  });
85
87
  this.liveInfo = liveInfo;
86
- this.state = "idle";
88
+ isLiveRadio = liveInfo.isLiveRadio;
89
+ this.emit("stateChange", { state: "idle" });
87
90
  }
88
91
  catch (error) {
89
- this.state = "check-error";
92
+ this.emit("stateChange", {
93
+ state: "check-error",
94
+ msg: `检查失败,` + (error instanceof Error ? error.message : String(error)),
95
+ });
90
96
  throw error;
91
97
  }
92
98
  if (this.liveInfo.liveId && this.liveInfo.liveId === banLiveId) {
@@ -106,7 +112,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
106
112
  const strictQuality = utils.shouldUseStrictQuality(qualityRetryLeft, this.qualityRetry, isManualStart);
107
113
  let res;
108
114
  try {
109
- // TODO: 检查mobile接口处理双屏录播流
110
115
  res = await getStream({
111
116
  channelId: this.channelId,
112
117
  quality: this.quality,
@@ -118,6 +123,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
118
123
  doubleScreen: this.doubleScreen,
119
124
  api: this.api,
120
125
  uid: this.uid,
126
+ isLiveRadio: isLiveRadio,
121
127
  });
122
128
  this.liveInfo.owner = res.owner;
123
129
  this.liveInfo.title = res.title;
@@ -131,11 +137,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
131
137
  catch (err) {
132
138
  if (qualityRetryLeft > 0)
133
139
  await this.cache.set("qualityRetryLeft", qualityRetryLeft - 1);
134
- this.state = "check-error";
140
+ this.emit("stateChange", {
141
+ state: "check-error",
142
+ msg: `检查失败,` + (err instanceof Error ? err.message : String(err)),
143
+ });
135
144
  throw err;
136
145
  }
137
146
  const { owner, title, liveStartTime, recordStartTime } = this.liveInfo;
138
- this.state = "recording";
147
+ this.emit("stateChange", { state: "recording" });
139
148
  const { currentStream: stream, sources: availableSources, streams: availableStreams } = res;
140
149
  this.availableStreams = availableStreams.map((s) => s.desc);
141
150
  this.availableSources = availableSources.map((s) => s.name);
@@ -173,6 +182,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
173
182
  headers: {
174
183
  Cookie: this.auth,
175
184
  },
185
+ proxy: this.proxy,
176
186
  }, onEnd, async () => {
177
187
  const info = await getInfo(this.channelId, {
178
188
  auth: this.auth,
@@ -324,24 +334,26 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
324
334
  giftMessageCache.set(groupId, { gift, timer });
325
335
  });
326
336
  client.on("reconnect", (attempts) => {
337
+ this.appendTimeline({ text: `弹幕连接断开,正在重试: ${attempts}` });
327
338
  this.emit("DebugLog", {
328
339
  type: "common",
329
- text: `douyin ${this.channelId} danma has reconnect ${attempts}`,
340
+ text: `douyin ${this.channelId} danma has reconnect ${attempts}`,
330
341
  });
331
342
  });
332
343
  client.on("error", (err) => {
333
344
  this.emit("DebugLog", {
334
345
  type: "common",
335
- text: `douyin ${this.channelId} danma error: ${String(err)}`,
346
+ text: `douyin ${this.channelId} danma error: ${String(err)}`,
336
347
  });
337
348
  });
338
349
  client.on("init", (url) => {
339
350
  this.emit("DebugLog", {
340
351
  type: "common",
341
- text: `douyin ${this.channelId} danma init ${url}`,
352
+ text: `douyin ${this.channelId} danma init ${url}`,
342
353
  });
343
354
  });
344
355
  client.on("open", () => {
356
+ this.appendTimeline({ text: `弹幕连接已建立` });
345
357
  this.emit("DebugLog", {
346
358
  type: "common",
347
359
  text: `douyin ${this.channelId} danma open`,
@@ -378,7 +390,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
378
390
  const stop = singleton(async (reason) => {
379
391
  if (!this.recordHandle)
380
392
  return;
381
- this.state = "stopping-record";
393
+ this.emit("stateChange", { state: "stopping-record" });
382
394
  try {
383
395
  // 清理所有礼物缓存定时器
384
396
  for (const [_groupId, cached] of giftMessageCache.entries()) {
@@ -405,7 +417,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
405
417
  this.emit("RecordStop", { recordHandle: this.recordHandle, reason });
406
418
  this.recordHandle = undefined;
407
419
  this.liveInfo = undefined;
408
- this.state = "idle";
420
+ this.emit("stateChange", { state: "idle" });
409
421
  this.cache.set("qualityRetryLeft", this.qualityRetry);
410
422
  });
411
423
  this.recordHandle = {
package/lib/stream.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare function getInfo(channelId: string, opts?: {
6
6
  uid?: string | number;
7
7
  }): Promise<{
8
8
  living: boolean;
9
+ isLiveRadio: boolean;
9
10
  owner: string;
10
11
  title: string;
11
12
  roomId: string;
@@ -26,6 +27,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
26
27
  doubleScreen?: boolean;
27
28
  api?: APIType;
28
29
  uid?: string | number;
30
+ isLiveRadio?: boolean;
29
31
  }): Promise<{
30
32
  currentStream: {
31
33
  name: string;
@@ -34,6 +36,7 @@ export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" |
34
36
  onlyAudio: boolean;
35
37
  };
36
38
  living: boolean;
39
+ isLiveRadio?: boolean;
37
40
  roomId: string;
38
41
  owner: string;
39
42
  title: string;
package/lib/stream.js CHANGED
@@ -15,6 +15,7 @@ export async function getInfo(channelId, opts) {
15
15
  const startTime = new Date();
16
16
  return {
17
17
  living: info.living,
18
+ isLiveRadio: info.isLiveRadio,
18
19
  owner: info.owner,
19
20
  title: info.title,
20
21
  roomId: info.roomId,
@@ -30,12 +31,20 @@ export async function getInfo(channelId, opts) {
30
31
  }
31
32
  export async function getStream(opts) {
32
33
  let api = opts.api ?? "web";
33
- if (api === "userHTML") {
34
+ if (api === "userHTML" || api === "webHTML") {
34
35
  // userHTML 接口只能用于状态检测
35
36
  api = "web";
36
37
  }
37
38
  else if (api === "random") {
38
- api = selectRandomAPI(["userHTML"]);
39
+ api = selectRandomAPI(["userHTML", "webHTML"]);
40
+ }
41
+ if (opts.isLiveRadio) {
42
+ // 直播电台可能需要特殊处理,优先使用 userHTML 接口
43
+ api = "userHTML";
44
+ // 如果用户指定了mobile接口,那么优先使用mobile接口获取流信息
45
+ if (opts.api === "mobile") {
46
+ api = "mobile";
47
+ }
39
48
  }
40
49
  const info = await getRoomInfo(opts.channelId, {
41
50
  doubleScreen: opts.doubleScreen ?? true,
package/lib/types.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface RoomInfo {
24
24
  sec_uid: string;
25
25
  avatar: string;
26
26
  api: RealAPIType;
27
+ isLiveRadio: boolean;
27
28
  room: {
28
29
  title: string;
29
30
  cover: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/douyin-recorder",
3
- "version": "1.16.1",
3
+ "version": "1.17.1",
4
4
  "description": "@bililive-tools douyin recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "lodash-es": "^4.17.21",
39
39
  "mitt": "^3.0.1",
40
40
  "sm-crypto": "^0.3.13",
41
- "douyin-danma-listener": "0.4.1",
42
- "@bililive-tools/manager": "^1.16.1"
41
+ "@bililive-tools/manager": "^1.17.0",
42
+ "douyin-danma-listener": "0.4.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "*"