@bililive-tools/douyin-recorder 1.2.0 → 1.4.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
@@ -36,12 +36,15 @@ interface Options {
36
36
  qualityRetry?: number; // 画质匹配重试次数, -1为强制匹配画质,0为自动配置,正整数为最大匹配次数
37
37
  streamPriorities: []; // 废弃
38
38
  sourcePriorities: []; // 废弃
39
+ formatPriorities?: string[]; // 支持,`flv`和`hls` 参数,默认为['flv','hls']
39
40
  disableAutoCheck?: boolean; // 为 true 时 manager 将跳过自动检查
40
41
  segment?: number; // 分段参数,单位分钟
41
42
  disableProvideCommentsWhenRecording?: boolean; // 禁用弹幕录制
42
43
  saveGiftDanma?: boolean; // 保存礼物弹幕
43
44
  saveCover?: boolean; // 保存封面
44
45
  videoFormat?: "auto"; // 视频格式: "auto", "ts", "mkv" ,auto模式下, 分段使用 "ts",不分段使用 "mp4"
46
+ doubleScreen?: boolean; // 是否使用双屏直播流,开启后如果是双屏直播,那么就使用拼接的流,默认为true
47
+ auth?: string; // 传递cookie,用于录制会员视频
45
48
  }
46
49
  ```
47
50
 
@@ -49,14 +52,15 @@ interface Options {
49
52
 
50
53
  遗漏了部分画质,有了解的可以提PR
51
54
 
52
- | 画质 | 值 |
53
- | ------ | ------ |
54
- | 原画 | origin |
55
- | 蓝光 | uhd |
56
- | 超清 | hd |
57
- | 高清 | sd |
58
- | 标清 | ld |
59
- | 音频流 | ao |
55
+ | 画质 | 值 |
56
+ | ---------------------- | ----------- |
57
+ | 原画 | origin |
58
+ | 蓝光 | uhd |
59
+ | 超清 | hd |
60
+ | 高清 | sd |
61
+ | 标清 | ld |
62
+ | 音频流 | ao |
63
+ | 真原画(音频流中获取的) | real_origin |
60
64
 
61
65
  ## 直播间ID解析
62
66
 
@@ -66,6 +70,7 @@ interface Options {
66
70
  import { provider } from "@bililive-tools/douyin-recorder";
67
71
 
68
72
  const url = "https://live.douyin.com/203641303310";
73
+ // 同样支持解析 https://v.douyin.com/DpfoBLAXoHM/
69
74
  const { id } = await provider.resolveChannelInfoFromURL(url);
70
75
  ```
71
76
 
@@ -1,5 +1,15 @@
1
+ /**
2
+ * 从抖音短链接解析得到直播间ID
3
+ * @param shortURL 短链接,如 https://v.douyin.com/DpfoBLAXoHM/
4
+ * @returns webRoomId 直播间ID
5
+ */
6
+ export declare function resolveShortURL(shortURL: string): Promise<string>;
1
7
  export declare const getCookie: () => Promise<string>;
2
- export declare function getRoomInfo(webRoomId: string, retryOnSpecialCode?: boolean): Promise<{
8
+ export declare function getRoomInfo(webRoomId: string, opts?: {
9
+ retryOnSpecialCode?: boolean;
10
+ auth?: string;
11
+ doubleScreen?: boolean;
12
+ }): Promise<{
3
13
  living: boolean;
4
14
  roomId: string;
5
15
  owner: string;
@@ -15,9 +25,16 @@ export interface StreamProfile {
15
25
  key: string;
16
26
  bitRate: number;
17
27
  }
28
+ export interface StreamInfo {
29
+ quality: string;
30
+ name: string;
31
+ flv?: string;
32
+ hls?: string;
33
+ }
18
34
  export interface SourceProfile {
19
35
  name: string;
20
36
  streamMap: StreamData["data"];
37
+ streams: StreamInfo[];
21
38
  }
22
39
  interface StreamData {
23
40
  common: unknown;
package/lib/douyin_api.js CHANGED
@@ -1,16 +1,65 @@
1
1
  import axios from "axios";
2
+ import { isEmpty } from "lodash-es";
2
3
  import { assert } from "./utils.js";
3
4
  const requester = axios.create({
4
5
  timeout: 10e3,
5
6
  // axios 会自动读取环境变量中的 http_proxy 和 https_proxy 并应用,这会让请求发往代理的 host。
6
7
  // 所以这里需要主动禁用代理功能。
7
8
  proxy: false,
9
+ headers: {
10
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
11
+ },
8
12
  });
13
+ /**
14
+ * 从抖音短链接解析得到直播间ID
15
+ * @param shortURL 短链接,如 https://v.douyin.com/DpfoBLAXoHM/
16
+ * @returns webRoomId 直播间ID
17
+ */
18
+ export async function resolveShortURL(shortURL) {
19
+ // 获取跳转后的页面内容
20
+ const response = await requester.get(shortURL);
21
+ // 尝试从页面内容中提取webRid
22
+ const webRidMatch = response.data.match(/"webRid\\":\\"(\d+)\\"/);
23
+ if (webRidMatch) {
24
+ return webRidMatch[1];
25
+ }
26
+ throw new Error("无法从短链接解析出直播间ID");
27
+ }
28
+ const qualityList = [
29
+ {
30
+ key: "origin",
31
+ desc: "原画",
32
+ },
33
+ {
34
+ key: "uhd",
35
+ desc: "蓝光",
36
+ },
37
+ {
38
+ key: "hd",
39
+ desc: "超清",
40
+ },
41
+ {
42
+ key: "sd",
43
+ desc: "高清",
44
+ },
45
+ {
46
+ key: "ld",
47
+ desc: "标清",
48
+ },
49
+ {
50
+ key: "ao",
51
+ desc: "音频流",
52
+ },
53
+ {
54
+ key: "real_origin",
55
+ desc: "真原画",
56
+ },
57
+ ];
9
58
  let cookieCache;
10
59
  export const getCookie = async () => {
11
60
  const now = new Date().getTime();
12
- // 缓存24小时
13
- if (cookieCache?.startTimestamp && now - cookieCache.startTimestamp < 24 * 60 * 60 * 1000) {
61
+ // 缓存6小时
62
+ if (cookieCache?.startTimestamp && now - cookieCache.startTimestamp < 6 * 60 * 60 * 1000) {
14
63
  return cookieCache.cookies;
15
64
  }
16
65
  const res = await requester.get("https://live.douyin.com/");
@@ -28,10 +77,16 @@ export const getCookie = async () => {
28
77
  };
29
78
  return cookies;
30
79
  };
31
- export async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
32
- // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,
33
- // 所以在这里请求一次自动设置。
34
- const cookies = await getCookie();
80
+ export async function getRoomInfo(webRoomId, opts = {}) {
81
+ let cookies = undefined;
82
+ if (opts.auth) {
83
+ cookies = opts.auth;
84
+ }
85
+ else {
86
+ // 抖音的 'webcast/room/web/enter' api 会需要 ttwid 的 cookie,这个 cookie 是由这个请求的响应头设置的,
87
+ // 所以在这里请求一次自动设置。
88
+ cookies = await getCookie();
89
+ }
35
90
  const res = await requester.get("https://live.douyin.com/webcast/room/web/enter/", {
36
91
  params: {
37
92
  aid: 6383,
@@ -55,9 +110,8 @@ export async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
55
110
  cookie: cookies,
56
111
  },
57
112
  });
58
- // console.log(JSON.stringify(res.data, null, 2));
59
113
  // 无 cookie 时 code 为 10037
60
- if (res.data.status_code === 10037 && retryOnSpecialCode) {
114
+ if (res.data.status_code === 10037 && opts.retryOnSpecialCode) {
61
115
  // resp 自动设置 cookie
62
116
  // const cookieRes = await requester.get("https://live.douyin.com/favicon.ico");
63
117
  // const cookies = cookieRes.headers["set-cookie"]
@@ -66,9 +120,12 @@ export async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
66
120
  // })
67
121
  // .join("; ");
68
122
  // console.log("cookies", cookies);
69
- return getRoomInfo(webRoomId, false);
123
+ return getRoomInfo(webRoomId, {
124
+ retryOnSpecialCode: false,
125
+ doubleScreen: opts.doubleScreen,
126
+ });
70
127
  }
71
- assert(res.data.status_code === 0, `Unexpected resp, code ${res.data.status_code}, msg ${res.data.data}, id ${webRoomId}`);
128
+ assert(res.data.status_code === 0, `Unexpected resp, code ${res.data.status_code}, msg ${JSON.stringify(res.data.data)}, id ${webRoomId}`);
72
129
  const data = res.data.data;
73
130
  const room = data.data[0];
74
131
  assert(room, `No room data, id ${webRoomId}`);
@@ -85,23 +142,72 @@ export async function getRoomInfo(webRoomId, retryOnSpecialCode = true) {
85
142
  liveId: room.id_str,
86
143
  };
87
144
  }
88
- const { options: { qualities }, stream_data, } = room.stream_url.live_core_sdk_data.pull_data;
145
+ let qualities = [];
146
+ let stream_data = "";
147
+ if (opts.doubleScreen && !isEmpty(room.stream_url.pull_datas)) {
148
+ const pull_data = Object.values(room.stream_url.pull_datas)[0] ?? {
149
+ options: {
150
+ qualities: [],
151
+ },
152
+ stream_data: "",
153
+ };
154
+ qualities = pull_data.options.qualities;
155
+ stream_data = pull_data.stream_data;
156
+ }
157
+ if (!stream_data) {
158
+ qualities = room.stream_url.live_core_sdk_data.pull_data.options.qualities;
159
+ stream_data = room.stream_url.live_core_sdk_data.pull_data.stream_data;
160
+ }
89
161
  const streamData = JSON.parse(stream_data).data;
90
162
  const streams = qualities.map((info) => ({
91
163
  desc: info.name,
92
164
  key: info.sdk_key,
93
165
  bitRate: info.v_bit_rate,
94
166
  }));
167
+ // 转换流数据结构
168
+ const streamList = Object.entries(streamData)
169
+ .map(([quality, info]) => {
170
+ const stream = info?.main;
171
+ const name = qualityList.find((item) => item.key === quality)?.desc;
172
+ return {
173
+ quality: quality,
174
+ name: name ?? "未知",
175
+ flv: stream?.flv,
176
+ hls: stream?.hls,
177
+ };
178
+ })
179
+ .filter((stream) => stream.flv || stream.hls);
180
+ const aoStream = streamList.find((stream) => stream.quality === "ao");
181
+ if (!!aoStream) {
182
+ // 真原画流是在ao流中拿到的
183
+ streamList.push({
184
+ quality: "real_origin",
185
+ name: "真原画",
186
+ flv: (aoStream?.flv ?? "").replace("&only_audio=1", ""),
187
+ hls: (aoStream?.hls ?? "").replace("&only_audio=1", ""),
188
+ });
189
+ }
190
+ streamList.sort((a, b) => {
191
+ const aIndex = qualityList.findIndex((item) => item.key === a.quality);
192
+ const bIndex = qualityList.findIndex((item) => item.key === b.quality);
193
+ // 如果找不到对应的质量等级,将其排在最后
194
+ if (aIndex === -1)
195
+ return 1;
196
+ if (bIndex === -1)
197
+ return -1;
198
+ return aIndex - bIndex;
199
+ });
95
200
  // 看起来抖音是自动切换 cdn 的,所以这里固定返回一个默认的 source。
96
201
  const sources = [
97
202
  {
98
203
  name: "自动",
99
204
  streamMap: streamData,
205
+ streams: streamList,
100
206
  },
101
207
  ];
208
+ // console.log(JSON.stringify(sources, null, 2), qualities);
102
209
  return {
103
210
  living: data.room_status === 0,
104
- // 接口里不会再返回 web room id,只能直接用入参原路返回了。
105
211
  roomId: webRoomId,
106
212
  owner: data.user.nickname,
107
213
  title: room.title,
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ import mitt from "mitt";
3
3
  import { defaultFromJSON, defaultToJSON, genRecorderUUID, genRecordUUID, FFMPEGRecorder, } from "@bililive-tools/manager";
4
4
  import { getInfo, getStream } from "./stream.js";
5
5
  import { ensureFolderExist, singleton } from "./utils.js";
6
+ import { resolveShortURL } from "./douyin_api.js";
6
7
  import DouYinDanmaClient from "douyin-danma-listener";
7
8
  function createRecorder(opts) {
8
9
  // 内部实现时,应该只有 proxy 包裹的那一层会使用这个 recorder 标识符,不应该有直接通过
@@ -60,13 +61,23 @@ const ffmpegOutputOptions = [
60
61
  "-movflags",
61
62
  "faststart+frag_keyframe+empty_moov",
62
63
  "-min_frag_duration",
63
- "60000000",
64
+ "10000000",
65
+ ];
66
+ const ffmpegInputOptions = [
67
+ "-reconnect",
68
+ "1",
69
+ "-reconnect_streamed",
70
+ "1",
71
+ "-reconnect_delay_max",
72
+ "10",
73
+ "-rw_timeout",
74
+ "15000000",
64
75
  ];
65
76
  const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isManualStart, }) {
66
77
  if (this.recordHandle != null)
67
78
  return this.recordHandle;
68
79
  const liveInfo = await getInfo(this.channelId);
69
- const { living, owner, title, liveId } = liveInfo;
80
+ const { living, owner, title } = liveInfo;
70
81
  this.liveInfo = liveInfo;
71
82
  if (liveInfo.liveId === banLiveId) {
72
83
  this.tempStopIntervalCheck = true;
@@ -78,7 +89,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
78
89
  return null;
79
90
  if (!living)
80
91
  return null;
81
- this.emit("LiveStart", { liveId });
82
92
  let res;
83
93
  try {
84
94
  let strictQuality = false;
@@ -97,6 +107,9 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
97
107
  streamPriorities: this.streamPriorities,
98
108
  sourcePriorities: this.sourcePriorities,
99
109
  strictQuality: strictQuality,
110
+ cookie: this.auth,
111
+ formatPriorities: this.formatPriorities,
112
+ doubleScreen: this.doubleScreen,
100
113
  });
101
114
  }
102
115
  catch (err) {
@@ -111,7 +124,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
111
124
  this.usedSource = stream.source;
112
125
  // TODO: emit update event
113
126
  let isEnded = false;
127
+ let isCutting = false;
114
128
  const onEnd = (...args) => {
129
+ if (isCutting) {
130
+ isCutting = false;
131
+ return;
132
+ }
115
133
  if (isEnded)
116
134
  return;
117
135
  isEnded = true;
@@ -125,11 +143,18 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
125
143
  const recorder = new FFMPEGRecorder({
126
144
  url: stream.url,
127
145
  outputOptions: ffmpegOutputOptions,
146
+ inputOptions: ffmpegInputOptions,
128
147
  segment: this.segment ?? 0,
129
- getSavePath: (opts) => getSavePath({ owner, title, startTime: opts.startTime }),
148
+ getSavePath: (opts) => getSavePath({ owner, title: opts.title ?? title, startTime: opts.startTime }),
130
149
  disableDanma: this.disableProvideCommentsWhenRecording,
131
150
  videoFormat: this.videoFormat ?? "auto",
132
- }, onEnd);
151
+ headers: {
152
+ Cookie: this.auth,
153
+ },
154
+ }, onEnd, async () => {
155
+ const info = await getInfo(this.channelId);
156
+ return info;
157
+ });
133
158
  const savePath = getSavePath({
134
159
  owner,
135
160
  title,
@@ -141,8 +166,14 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
141
166
  this.state = "idle";
142
167
  throw err;
143
168
  }
144
- const handleVideoCreated = async ({ filename }) => {
145
- this.emit("videoFileCreated", { filename });
169
+ const handleVideoCreated = async ({ filename, title, cover }) => {
170
+ this.emit("videoFileCreated", { filename, cover });
171
+ if (title && this?.liveInfo) {
172
+ this.liveInfo.title = title;
173
+ }
174
+ if (cover && this?.liveInfo) {
175
+ this.liveInfo.cover = cover;
176
+ }
146
177
  const extraDataController = recorder.getExtraDataController();
147
178
  extraDataController?.setMeta({
148
179
  room_id: this.channelId,
@@ -166,14 +197,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
166
197
  }
167
198
  this.emit("progress", progress);
168
199
  });
169
- const client = new DouYinDanmaClient(liveInfo.liveId);
200
+ const client = new DouYinDanmaClient(liveInfo.liveId, {
201
+ cookie: this.auth,
202
+ });
170
203
  client.on("chat", (msg) => {
171
204
  const extraDataController = recorder.getExtraDataController();
172
205
  if (!extraDataController)
173
206
  return;
174
207
  const comment = {
175
208
  type: "comment",
176
- timestamp: Date.now(),
209
+ timestamp: Number(msg.eventTime) * 1000,
177
210
  text: msg.content,
178
211
  color: "#ffffff",
179
212
  sender: {
@@ -185,7 +218,6 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
185
218
  // },
186
219
  },
187
220
  };
188
- // console.log("comment", comment);
189
221
  this.emit("Message", comment);
190
222
  extraDataController.addMessage(comment);
191
223
  });
@@ -197,10 +229,12 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
197
229
  return;
198
230
  const gift = {
199
231
  type: "give_gift",
200
- timestamp: Number(msg.sendTime),
232
+ timestamp: Number(msg.common.createTime) > 9999999999
233
+ ? Number(msg.common.createTime)
234
+ : Number(msg.common.createTime) * 1000,
201
235
  name: msg.gift.name,
202
236
  price: 1,
203
- count: Number(msg.totalCount),
237
+ count: Number(msg.totalCount ?? 1),
204
238
  color: "#ffffff",
205
239
  sender: {
206
240
  uid: msg.user.id,
@@ -211,10 +245,15 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
211
245
  // },
212
246
  },
213
247
  };
214
- // console.log("gift", gift);
215
248
  this.emit("Message", gift);
216
249
  extraDataController.addMessage(gift);
217
250
  });
251
+ client.on("reconnect", (attempts) => {
252
+ this.emit("DebugLog", {
253
+ type: "common",
254
+ text: `danma has reconnect ${attempts}`,
255
+ });
256
+ });
218
257
  // client.on("open", () => {
219
258
  // console.log("open");
220
259
  // });
@@ -232,6 +271,16 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
232
271
  }
233
272
  const ffmpegArgs = recorder.getArguments();
234
273
  recorder.run();
274
+ const cut = singleton(async () => {
275
+ if (!this.recordHandle)
276
+ return;
277
+ if (isCutting)
278
+ return;
279
+ isCutting = true;
280
+ await recorder.stop();
281
+ recorder.createCommand();
282
+ recorder.run();
283
+ });
235
284
  const stop = singleton(async (reason) => {
236
285
  if (!this.recordHandle)
237
286
  return;
@@ -261,6 +310,7 @@ const checkLiveStatusAndRecord = async function ({ getSavePath, banLiveId, isMan
261
310
  ffmpegArgs,
262
311
  savePath: savePath,
263
312
  stop,
313
+ cut,
264
314
  };
265
315
  this.emit("RecordStart", this.recordHandle);
266
316
  return this.recordHandle;
@@ -270,13 +320,26 @@ export const provider = {
270
320
  name: "抖音",
271
321
  siteURL: "https://live.douyin.com/",
272
322
  matchURL(channelURL) {
273
- // TODO: 暂时不支持 v.douyin.com
274
- return /https?:\/\/live\.douyin\.com\//.test(channelURL);
323
+ // 支持 v.douyin.com 和 live.douyin.com
324
+ return /https?:\/\/(live|v)\.douyin\.com\//.test(channelURL);
275
325
  },
276
326
  async resolveChannelInfoFromURL(channelURL) {
277
327
  if (!this.matchURL(channelURL))
278
328
  return null;
279
- const id = path.basename(new URL(channelURL).pathname);
329
+ let id;
330
+ if (channelURL.includes("v.douyin.com")) {
331
+ // 处理短链接
332
+ try {
333
+ id = await resolveShortURL(channelURL);
334
+ }
335
+ catch (err) {
336
+ throw new Error(`解析抖音短链接失败: ${err?.message}`);
337
+ }
338
+ }
339
+ else {
340
+ // 处理常规直播链接
341
+ id = path.basename(new URL(channelURL).pathname);
342
+ }
280
343
  const info = await getInfo(id);
281
344
  return {
282
345
  id: info.roomId,
package/lib/stream.d.ts CHANGED
@@ -12,6 +12,9 @@ export declare function getInfo(channelId: string): Promise<{
12
12
  export declare function getStream(opts: Pick<Recorder, "channelId" | "quality" | "streamPriorities" | "sourcePriorities"> & {
13
13
  rejectCache?: boolean;
14
14
  strictQuality?: boolean;
15
+ cookie?: string;
16
+ formatPriorities?: Array<"flv" | "hls">;
17
+ doubleScreen?: boolean;
15
18
  }): Promise<{
16
19
  currentStream: {
17
20
  name: string;
package/lib/stream.js CHANGED
@@ -13,48 +13,40 @@ export async function getInfo(channelId) {
13
13
  };
14
14
  }
15
15
  export async function getStream(opts) {
16
- const info = await getRoomInfo(opts.channelId);
16
+ const info = await getRoomInfo(opts.channelId, {
17
+ retryOnSpecialCode: true,
18
+ doubleScreen: opts.doubleScreen ?? true,
19
+ auth: opts.cookie,
20
+ });
17
21
  if (!info.living) {
18
22
  throw new Error("It must be called getStream when living");
19
23
  }
20
- const qualityMap = [
21
- {
22
- key: "origin",
23
- desc: "原画",
24
- },
25
- {
26
- key: "uhd",
27
- desc: "蓝光",
28
- },
29
- {
30
- key: "hd",
31
- desc: "超清",
32
- },
33
- {
34
- key: "sd",
35
- desc: "高清",
36
- },
37
- {
38
- key: "标清",
39
- desc: "ld",
40
- },
41
- ];
24
+ // 抖音为自动cdn,所以指定选择第一个
42
25
  const sources = info.sources[0];
43
- let url = sources.streamMap[opts.quality]?.main?.flv;
44
- let qualityName = qualityMap.find((q) => q.key === opts.quality)?.desc ?? "未知";
45
- if (!url && opts.strictQuality) {
26
+ const formatPriorities = opts.formatPriorities || ["flv", "hls"];
27
+ // 查找指定质量的流
28
+ let targetStream = sources.streams.find((s) => s.quality === opts.quality);
29
+ let qualityName = targetStream?.name ?? "未知";
30
+ if (!targetStream && opts.strictQuality) {
46
31
  throw new Error("Can not get expect quality because of strictQuality");
47
32
  }
48
- // 如果url不存在,那么按照优先级选择
49
- if (!url) {
50
- for (const quality of qualityMap) {
51
- url = sources.streamMap[quality.key]?.main?.flv;
52
- if (url) {
53
- qualityName = quality.desc;
54
- break;
55
- }
33
+ // 如果找不到指定质量的流,按照流顺序选择第一个可用的流
34
+ if (!targetStream) {
35
+ targetStream = sources.streams.find((stream) => stream.flv || stream.hls);
36
+ if (targetStream) {
37
+ qualityName = targetStream.name;
56
38
  }
57
39
  }
40
+ if (!targetStream) {
41
+ throw new Error("未找到对应的流");
42
+ }
43
+ // 根据格式优先级选择 URL
44
+ let url;
45
+ for (const format of formatPriorities) {
46
+ url = targetStream[format];
47
+ if (url)
48
+ break;
49
+ }
58
50
  if (!url) {
59
51
  throw new Error("未找到对应的流");
60
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bililive-tools/douyin-recorder",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "@bililive-tools douyin recorder implemention",
5
5
  "main": "./lib/index.js",
6
6
  "type": "module",
@@ -37,8 +37,8 @@
37
37
  "axios": "^1.7.8",
38
38
  "lodash-es": "^4.17.21",
39
39
  "mitt": "^3.0.1",
40
- "@bililive-tools/manager": "^1.2.0",
41
- "douyin-danma-listener": "0.1.1"
40
+ "douyin-danma-listener": "0.2.0",
41
+ "@bililive-tools/manager": "^1.3.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "*"