@tiktool/live 2.6.6 → 2.7.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/dist/index.js CHANGED
@@ -30,12 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- TikTokCaptions: () => TikTokCaptions,
34
- TikTokLive: () => TikTokLive,
35
- fetchFeed: () => fetchFeed,
36
- getLiveFeed: () => getLiveFeed,
37
- getRanklist: () => getRanklist,
38
- getRegionalRanklist: () => getRegionalRanklist
33
+ TikTokLive: () => TikTokLive
39
34
  });
40
35
  module.exports = __toCommonJS(index_exports);
41
36
 
@@ -44,94 +39,9 @@ var import_events = require("events");
44
39
  var http = __toESM(require("http"));
45
40
  var https = __toESM(require("https"));
46
41
  var zlib = __toESM(require("zlib"));
47
- var import_url = require("url");
48
42
  var import_ws = __toESM(require("ws"));
49
43
 
50
44
  // src/proto.ts
51
- var encoder = new TextEncoder();
52
- var decoder = new TextDecoder();
53
- var TIKTOK_EMOTE_MAP = {
54
- // Standard emojis
55
- "happy": "\u{1F60A}",
56
- "angry": "\u{1F621}",
57
- "cry": "\u{1F622}",
58
- "embarrassed": "\u{1F633}",
59
- "surprised": "\u{1F62E}",
60
- "wronged": "\u{1F61E}",
61
- "shout": "\u{1F624}",
62
- "flushed": "\u{1F633}",
63
- "yummy": "\u{1F60B}",
64
- "complacent": "\u{1F60C}",
65
- "drool": "\u{1F924}",
66
- "scream": "\u{1F631}",
67
- "weep": "\u{1F62D}",
68
- "speechless": "\u{1F636}",
69
- "funnyface": "\u{1F92A}",
70
- "laughwithtears": "\u{1F602}",
71
- "wicked": "\u{1F608}",
72
- "facewithrollingeyes": "\u{1F644}",
73
- "sulk": "\u{1F612}",
74
- "thinking": "\u{1F914}",
75
- "lovely": "\u{1F970}",
76
- "greedy": "\u{1F911}",
77
- "wow": "\u{1F62F}",
78
- "joyful": "\u{1F603}",
79
- "hehe": "\u{1F601}",
80
- "slap": "\u{1F44B}",
81
- "tears": "\u{1F63F}",
82
- "stun": "\u{1F635}",
83
- "cute": "\u{1F97A}",
84
- "blink": "\u{1F609}",
85
- "disdain": "\u{1F60F}",
86
- "astonish": "\u{1F632}",
87
- "cool": "\u{1F60E}",
88
- "excited": "\u{1F929}",
89
- "proud": "\u{1F624}",
90
- "smileface": "\u{1F60A}",
91
- "evil": "\u{1F47F}",
92
- "angel": "\u{1F607}",
93
- "laugh": "\u{1F606}",
94
- "pride": "\u{1F981}",
95
- "nap": "\u{1F634}",
96
- "loveface": "\u{1F60D}",
97
- "awkward": "\u{1F62C}",
98
- "shock": "\u{1F628}",
99
- "funny": "\u{1F604}",
100
- "rage": "\u{1F92C}",
101
- // Common aliases used in TikTok LIVE
102
- "laughcry": "\u{1F602}",
103
- "heart": "\u2764\uFE0F",
104
- "like": "\u{1F44D}",
105
- "love": "\u{1F495}",
106
- "shy": "\u{1F648}",
107
- "smile": "\u{1F60A}"
108
- };
109
- function replaceEmotes(text, imageMap) {
110
- return text.replace(/\[([a-zA-Z_]+)\]/g, (match, name) => {
111
- const lower = name.toLowerCase();
112
- const emoji = TIKTOK_EMOTE_MAP[lower];
113
- return emoji || match;
114
- });
115
- }
116
- function concatBytes(...arrays) {
117
- let totalLength = 0;
118
- for (const arr of arrays) totalLength += arr.length;
119
- const result = new Uint8Array(totalLength);
120
- let offset = 0;
121
- for (const arr of arrays) {
122
- result.set(arr, offset);
123
- offset += arr.length;
124
- }
125
- return result;
126
- }
127
- function readInt32LE(buf, offset) {
128
- return buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24;
129
- }
130
- function readBigInt64LE(buf, offset) {
131
- const lo = BigInt(buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24 >>> 0);
132
- const hi = BigInt(buf[offset + 4] | buf[offset + 5] << 8 | buf[offset + 6] << 16 | buf[offset + 7] << 24 >>> 0);
133
- return hi << 32n | lo & 0xFFFFFFFFn;
134
- }
135
45
  function decodeVarint(buf, offset) {
136
46
  let result = 0, shift = 0;
137
47
  while (offset < buf.length) {
@@ -161,15 +71,15 @@ function encodeVarint(v) {
161
71
  if (n > 0n) b |= 128;
162
72
  bytes.push(b);
163
73
  } while (n > 0n);
164
- return new Uint8Array(bytes);
74
+ return Buffer.from(bytes);
165
75
  }
166
76
  function encodeField(fn, wt, value) {
167
77
  const tag = encodeVarint(fn << 3 | wt);
168
78
  if (wt === 0) {
169
- return concatBytes(tag, encodeVarint(typeof value === "number" ? BigInt(value) : value));
79
+ return Buffer.concat([tag, encodeVarint(typeof value === "number" ? BigInt(value) : value)]);
170
80
  }
171
- const data = typeof value === "string" ? encoder.encode(value) : value;
172
- return concatBytes(tag, encodeVarint(data.length), data);
81
+ const data = typeof value === "string" ? Buffer.from(value) : value;
82
+ return Buffer.concat([tag, encodeVarint(data.length), data]);
173
83
  }
174
84
  function decodeProto(buf) {
175
85
  const fields = [];
@@ -190,10 +100,10 @@ function decodeProto(buf) {
190
100
  offset += lenR.value;
191
101
  fields.push({ fn, wt, value: data });
192
102
  } else if (wt === 1) {
193
- fields.push({ fn, wt, value: readBigInt64LE(buf, offset) });
103
+ fields.push({ fn, wt, value: buf.readBigInt64LE(offset) });
194
104
  offset += 8;
195
105
  } else if (wt === 5) {
196
- fields.push({ fn, wt, value: BigInt(readInt32LE(buf, offset)) });
106
+ fields.push({ fn, wt, value: BigInt(buf.readInt32LE(offset)) });
197
107
  offset += 4;
198
108
  } else {
199
109
  break;
@@ -203,7 +113,7 @@ function decodeProto(buf) {
203
113
  }
204
114
  function getStr(fields, fn) {
205
115
  const f = fields.find((x) => x.fn === fn && x.wt === 2);
206
- return f ? decoder.decode(f.value) : "";
116
+ return f ? f.value.toString("utf-8") : "";
207
117
  }
208
118
  function getBytes(fields, fn) {
209
119
  const f = fields.find((x) => x.fn === fn && x.wt === 2);
@@ -215,47 +125,47 @@ function getInt(fields, fn) {
215
125
  }
216
126
  function getIntStr(fields, fn) {
217
127
  const f = fields.find((x) => x.fn === fn && x.wt === 0);
218
- return f ? String(f.value) : "";
128
+ return f ? String(f.value) : "0";
219
129
  }
220
130
  function getAllBytes(fields, fn) {
221
131
  return fields.filter((x) => x.fn === fn && x.wt === 2).map((x) => x.value);
222
132
  }
223
133
  function buildHeartbeat(roomId) {
224
134
  const payload = encodeField(1, 0, BigInt(roomId));
225
- return concatBytes(
135
+ return Buffer.concat([
226
136
  encodeField(6, 2, "pb"),
227
137
  encodeField(7, 2, "hb"),
228
138
  encodeField(8, 2, payload)
229
- );
139
+ ]);
230
140
  }
231
141
  function buildImEnterRoom(roomId) {
232
- const inner = concatBytes(
142
+ const inner = Buffer.concat([
233
143
  encodeField(1, 0, BigInt(roomId)),
234
144
  encodeField(4, 0, 12n),
235
145
  encodeField(5, 2, "audience"),
236
146
  encodeField(6, 2, ""),
237
147
  encodeField(9, 2, ""),
238
148
  encodeField(10, 2, "")
239
- );
240
- return concatBytes(
149
+ ]);
150
+ return Buffer.concat([
241
151
  encodeField(6, 2, "pb"),
242
152
  encodeField(7, 2, "im_enter_room"),
243
153
  encodeField(8, 2, inner)
244
- );
154
+ ]);
245
155
  }
246
156
  function buildAck(id) {
247
- return concatBytes(
157
+ return Buffer.concat([
248
158
  encodeField(2, 0, id),
249
159
  encodeField(6, 2, "pb"),
250
160
  encodeField(7, 2, "ack")
251
- );
161
+ ]);
252
162
  }
253
163
  function looksLikeUsername(s) {
254
164
  return s.length > 0 && s.length <= 50 && !s.includes("://") && !s.includes("\0");
255
165
  }
256
166
  function parseUser(data) {
257
167
  const f = decodeProto(data);
258
- const id = getIntStr(f, 1) || getStr(f, 1);
168
+ const id = getIntStr(f, 1) !== "0" ? getIntStr(f, 1) : getStr(f, 1);
259
169
  const nickname = getStr(f, 3) || getStr(f, 2);
260
170
  let uniqueId = "";
261
171
  const uid4 = getStr(f, 4);
@@ -277,7 +187,7 @@ function parseUser(data) {
277
187
  const avatarFields = decodeProto(avatarBuf);
278
188
  const urlBufs = getAllBytes(avatarFields, 1);
279
189
  for (const urlBuf of urlBufs) {
280
- const url = decoder.decode(urlBuf);
190
+ const url = urlBuf.toString("utf-8");
281
191
  if (url.includes("://")) {
282
192
  profilePicture = url;
283
193
  break;
@@ -286,25 +196,44 @@ function parseUser(data) {
286
196
  } catch {
287
197
  }
288
198
  }
289
- const badges = [];
290
- let payGrade;
291
- const badgeBufs = getAllBytes(f, 64);
292
- for (const badgeBuf of badgeBufs) {
199
+ let avatarLargeUrl;
200
+ const largeBuf = getBytes(f, 11);
201
+ if (largeBuf) {
293
202
  try {
294
- const badgeFields = decodeProto(badgeBuf);
295
- const badgeCategory = getInt(badgeFields, 2);
296
- const badgeSubCat = getInt(badgeFields, 3);
297
- if (badgeCategory === 20 && badgeSubCat === 8 && !payGrade) {
298
- const privBuf = getBytes(badgeFields, 12);
299
- if (privBuf) {
300
- const privFields = decodeProto(privBuf);
301
- const levelStr = getStr(privFields, 5);
302
- if (levelStr) {
303
- const level = parseInt(levelStr, 10);
304
- if (level > 0) payGrade = level;
203
+ const largeFields = decodeProto(largeBuf);
204
+ const urlBufs = getAllBytes(largeFields, 1);
205
+ for (const urlBuf of urlBufs) {
206
+ const url = urlBuf.toString("utf-8");
207
+ if (url.includes("://")) {
208
+ avatarLargeUrl = url;
209
+ break;
210
+ }
211
+ }
212
+ } catch {
213
+ }
214
+ }
215
+ if (!avatarLargeUrl) {
216
+ const medBuf = getBytes(f, 10);
217
+ if (medBuf) {
218
+ try {
219
+ const medFields = decodeProto(medBuf);
220
+ const urlBufs = getAllBytes(medFields, 1);
221
+ for (const urlBuf of urlBufs) {
222
+ const url = urlBuf.toString("utf-8");
223
+ if (url.includes("://")) {
224
+ avatarLargeUrl = url;
225
+ break;
305
226
  }
306
227
  }
228
+ } catch {
307
229
  }
230
+ }
231
+ }
232
+ const badges = [];
233
+ const badgeBuf = getBytes(f, 64);
234
+ if (badgeBuf) {
235
+ try {
236
+ const badgeFields = decodeProto(badgeBuf);
308
237
  const badgeItems = getAllBytes(badgeFields, 21);
309
238
  for (const bi of badgeItems) {
310
239
  const bf = decodeProto(bi);
@@ -318,17 +247,16 @@ function parseUser(data) {
318
247
  id,
319
248
  nickname,
320
249
  uniqueId: uniqueId || nickname || id,
321
- profilePictureUrl: profilePicture,
322
- badges: badges.length > 0 ? badges : void 0,
323
- payGrade
250
+ profilePicture,
251
+ avatarLargeUrl,
252
+ badges: badges.length > 0 ? badges : void 0
324
253
  };
325
254
  }
326
255
  function parseBattleTeamFromArmies(itemBuf) {
327
256
  const f = decodeProto(itemBuf);
328
- const hostUserId = getIntStr(f, 1) || "0";
257
+ const hostUserId = getIntStr(f, 1);
329
258
  let teamScore = 0;
330
259
  const users = [];
331
- let hostUser;
332
260
  const groups = getAllBytes(f, 2);
333
261
  for (const gb of groups) {
334
262
  try {
@@ -338,29 +266,15 @@ function parseBattleTeamFromArmies(itemBuf) {
338
266
  const userBufs = getAllBytes(gf, 1);
339
267
  for (const ub of userBufs) {
340
268
  try {
341
- const userFields = decodeProto(ub);
342
- const individualScore = getInt(userFields, 2);
343
269
  const user = parseUser(ub);
344
- users.push({ user, score: individualScore });
270
+ users.push({ user, score: points });
345
271
  } catch {
346
272
  }
347
273
  }
348
274
  } catch {
349
275
  }
350
276
  }
351
- for (const fieldNum of [3, 4, 5, 6, 7, 8]) {
352
- if (hostUser) break;
353
- const buf = getBytes(f, fieldNum);
354
- if (!buf) continue;
355
- try {
356
- const parsed = parseUser(buf);
357
- if (parsed && (parsed.nickname || parsed.uniqueId) && parsed.uniqueId !== parsed.id && looksLikeUsername(parsed.uniqueId || "")) {
358
- hostUser = parsed;
359
- }
360
- } catch {
361
- }
362
- }
363
- return { hostUserId, score: teamScore, users, hostUser };
277
+ return { hostUserId, score: teamScore, users };
364
278
  }
365
279
  function parseWebcastMessage(method, payload) {
366
280
  const f = decodeProto(payload);
@@ -375,29 +289,42 @@ function parseWebcastMessage(method, payload) {
375
289
  }
376
290
  }
377
291
  switch (method) {
292
+ // Proto: WebcastChatMessage { MessageType type=1, User user=2, string comment=3, ..., repeated WebcastSubEmote emotes=12 }
293
+ // WebcastSubEmote { int32 placeInComment=1, EmoteDetails emote=2 }
294
+ // EmoteDetails { string emoteId=1, EmoteImage image=2 }
295
+ // EmoteImage { string imageUrl=1 }
378
296
  case "WebcastChatMessage": {
379
297
  const userBuf = getBytes(f, 2);
380
298
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
381
- const rawComment = getStr(f, 3);
382
- const emoteImages = {};
383
- for (const field of f) {
384
- if (field.fn === 22 && field.wt === 2) {
385
- try {
386
- const ewi = decodeProto(field.value);
387
- const emoteKey = getStr(ewi, 1);
388
- const imgBuf = getBytes(ewi, 2);
389
- if (imgBuf && emoteKey) {
390
- const imgFields = decodeProto(imgBuf);
391
- const url = getStr(imgFields, 1);
392
- if (url) emoteImages[emoteKey] = url;
299
+ const comment = getStr(f, 3);
300
+ const emotes = [];
301
+ const emoteBufs12 = getAllBytes(f, 12);
302
+ const emoteBufs13 = getAllBytes(f, 13);
303
+ const emoteBufs = emoteBufs13.length > 0 ? emoteBufs13 : emoteBufs12;
304
+ for (const eb of emoteBufs) {
305
+ try {
306
+ const ef = decodeProto(eb);
307
+ const placeInComment = getInt(ef, 1);
308
+ const detailBuf = getBytes(ef, 2);
309
+ if (detailBuf) {
310
+ const df = decodeProto(detailBuf);
311
+ const emoteId = getStr(df, 1);
312
+ let imageUrl = "";
313
+ const imgBuf = getBytes(df, 2);
314
+ if (imgBuf) {
315
+ const imgf = decodeProto(imgBuf);
316
+ imageUrl = getStr(imgf, 1);
317
+ }
318
+ if (emoteId) {
319
+ emotes.push({ emoteId, imageUrl, placeInComment });
393
320
  }
394
- } catch {
395
321
  }
322
+ } catch {
396
323
  }
397
324
  }
398
- const comment = replaceEmotes(rawComment, emoteImages);
399
- return { ...base, type: "chat", user, comment };
325
+ return { ...base, type: "chat", user, comment, emotes };
400
326
  }
327
+ // Proto: WebcastMemberMessage { User user=2, WebcastMessageEvent event=1 }
401
328
  case "WebcastMemberMessage": {
402
329
  const userBuf = getBytes(f, 2);
403
330
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -415,6 +342,7 @@ function parseWebcastMessage(method, payload) {
415
342
  }
416
343
  return { ...base, type: "member", user, action };
417
344
  }
345
+ // Proto: WebcastLikeMessage { User user=5, WebcastMessageEvent event=1, int32 likeCount=2, int32 totalLikeCount=3 }
418
346
  case "WebcastLikeMessage": {
419
347
  const userBuf = getBytes(f, 5);
420
348
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -426,6 +354,13 @@ function parseWebcastMessage(method, payload) {
426
354
  totalLikes: getInt(f, 3)
427
355
  };
428
356
  }
357
+ // Proto: WebcastGiftMessage {
358
+ // User user=7, int32 giftId=2, int32 repeatCount=5, int32 repeatEnd=9,
359
+ // GiftDetails giftDetails=15, GiftExtra giftExtra=23
360
+ // }
361
+ // GiftDetails { GiftImage giftImage=1, string giftName=16, string describe=2,
362
+ // int32 giftType=11, int32 diamondCount=12 }
363
+ // GiftExtra { uint64 timestamp=6, uint64 toUserId=8 }
429
364
  case "WebcastGiftMessage": {
430
365
  const userBuf = getBytes(f, 7);
431
366
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -450,7 +385,7 @@ function parseWebcastMessage(method, payload) {
450
385
  const extraBuf = getBytes(f, 23);
451
386
  if (extraBuf) {
452
387
  const ef = decodeProto(extraBuf);
453
- toUserId = getIntStr(ef, 8) || "";
388
+ toUserId = String(getInt(ef, 8));
454
389
  }
455
390
  const groupId = toUserId || getStr(f, 11);
456
391
  return {
@@ -464,10 +399,10 @@ function parseWebcastMessage(method, payload) {
464
399
  repeatEnd,
465
400
  combo: repeatCount > 1 && !repeatEnd,
466
401
  giftType,
467
- groupId,
468
- giftPictureUrl: giftImageUrl
402
+ groupId
469
403
  };
470
404
  }
405
+ // Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
471
406
  case "WebcastSocialMessage": {
472
407
  const userBuf = getBytes(f, 2);
473
408
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -487,42 +422,19 @@ function parseWebcastMessage(method, payload) {
487
422
  }
488
423
  return { ...base, type: "social", user, action };
489
424
  }
425
+ // Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
490
426
  case "WebcastRoomUserSeqMessage": {
491
427
  const viewerCount = getInt(f, 3) || getInt(f, 2);
492
428
  const totalViewers = getInt(f, 1) || viewerCount;
493
429
  return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
494
430
  }
431
+ // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
432
+ // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
495
433
  case "WebcastLinkMicBattle": {
496
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
497
- const overallStatus = getInt(f, 4);
434
+ const battleId = String(getInt(f, 1) || "");
435
+ const status = getInt(f, 2) || 1;
436
+ const battleDuration = getInt(f, 3);
498
437
  const teams = [];
499
- let battleDuration = 0;
500
- let battleSettings;
501
- const settingsBuf = getBytes(f, 3);
502
- if (settingsBuf) {
503
- try {
504
- const sf = decodeProto(settingsBuf);
505
- const startTimeMs = getInt(sf, 2);
506
- const duration = getInt(sf, 3);
507
- const phase = getInt(sf, 5);
508
- const endTimeMs = getInt(sf, 10);
509
- battleDuration = duration;
510
- battleSettings = {
511
- startTimeMs: startTimeMs || void 0,
512
- duration: duration || void 0,
513
- endTimeMs: endTimeMs || void 0
514
- };
515
- } catch {
516
- }
517
- }
518
- const settingsPhase = settingsBuf ? (() => {
519
- try {
520
- return getInt(decodeProto(settingsBuf), 5);
521
- } catch {
522
- return 0;
523
- }
524
- })() : 0;
525
- const status = settingsPhase || (overallStatus > 0 && overallStatus <= 10 ? overallStatus : 0) || 1;
526
438
  const battleUserBufs = getAllBytes(f, 10);
527
439
  for (const bub of battleUserBufs) {
528
440
  try {
@@ -543,23 +455,6 @@ function parseWebcastMessage(method, payload) {
543
455
  } catch {
544
456
  }
545
457
  }
546
- if (teams.length === 0) {
547
- const teamBufs5 = getAllBytes(f, 5);
548
- for (const tb of teamBufs5) {
549
- try {
550
- const tf = decodeProto(tb);
551
- const userId = getIntStr(tf, 1);
552
- if (userId && userId !== "0") {
553
- teams.push({
554
- hostUserId: userId,
555
- score: 0,
556
- users: []
557
- });
558
- }
559
- } catch {
560
- }
561
- }
562
- }
563
458
  if (teams.length === 0) {
564
459
  const teamBufs7 = getAllBytes(f, 7);
565
460
  for (const tb of teamBufs7) {
@@ -569,48 +464,51 @@ function parseWebcastMessage(method, payload) {
569
464
  }
570
465
  }
571
466
  }
572
- return { ...base, type: "battle", battleId, status, battleDuration, teams, battleSettings };
467
+ return { ...base, type: "battle", battleId, status, battleDuration, teams };
573
468
  }
469
+ // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
470
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
471
+ // battleItems → { hostUserId=1, repeated battleGroups=2 }
472
+ // battleGroups → { repeated users=1, int32 points=2 }
574
473
  case "WebcastLinkMicArmies": {
575
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
474
+ const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
576
475
  const battleStatus = getInt(f, 7);
577
476
  const teams = [];
578
- let battleSettings;
579
- const settingsBuf18 = getBytes(f, 18);
580
- if (settingsBuf18) {
477
+ const itemBufs = getAllBytes(f, 3);
478
+ for (const ib of itemBufs) {
581
479
  try {
582
- const sf = decodeProto(settingsBuf18);
583
- const startTimeMs = getInt(sf, 2);
584
- const duration = getInt(sf, 3);
585
- const endTimeMs = getInt(sf, 10);
586
- battleSettings = {
587
- startTimeMs: startTimeMs || void 0,
588
- duration: duration || void 0,
589
- endTimeMs: endTimeMs || void 0
590
- };
480
+ teams.push(parseBattleTeamFromArmies(ib));
591
481
  } catch {
592
482
  }
593
483
  }
594
- const scoreUpdateTime = getInt(f, 5);
595
- const giftSentTime = getInt(f, 6);
596
- const itemBufs = getAllBytes(f, 3);
597
- for (const ib of itemBufs) {
484
+ let battleStartMs = 0;
485
+ let battleDurationSeconds = 0;
486
+ const configBuf = getBytes(f, 18);
487
+ if (configBuf) {
598
488
  try {
599
- teams.push(parseBattleTeamFromArmies(ib));
489
+ const cf = decodeProto(configBuf);
490
+ battleStartMs = getInt(cf, 2);
491
+ battleDurationSeconds = getInt(cf, 3);
600
492
  } catch {
601
493
  }
602
494
  }
495
+ let timeLeftSeconds = 0;
496
+ if (battleStartMs > 0 && battleDurationSeconds > 0) {
497
+ const elapsedMs = Date.now() - battleStartMs;
498
+ timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
499
+ }
603
500
  return {
604
501
  ...base,
605
502
  type: "battleArmies",
606
503
  battleId,
607
504
  teams,
608
505
  status: battleStatus,
609
- battleSettings,
610
- scoreUpdateTime,
611
- giftSentTime
506
+ battleStartMs,
507
+ battleDurationSeconds,
508
+ timeLeftSeconds
612
509
  };
613
510
  }
511
+ // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
614
512
  case "WebcastSubNotifyMessage": {
615
513
  const userBuf = getBytes(f, 2);
616
514
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -636,6 +534,8 @@ function parseWebcastMessage(method, payload) {
636
534
  const envelopeId = String(getInt(f, 1) || getStr(f, 1));
637
535
  return { ...base, type: "envelope", envelopeId, diamondCount: getInt(f, 3) };
638
536
  }
537
+ // Proto: WebcastQuestionNewMessage { MessageType type=1, QuestionDetails questionDetails=2 }
538
+ // QuestionDetails { string questionText=2, User user=5 }
639
539
  case "WebcastQuestionNewMessage": {
640
540
  let questionText = "", user = { id: "0", nickname: "", uniqueId: "" };
641
541
  const detailBuf = getBytes(f, 2);
@@ -665,17 +565,20 @@ function parseWebcastMessage(method, payload) {
665
565
  }
666
566
  return { ...base, type: "rankUpdate", rankType, rankList };
667
567
  }
568
+ // Proto: WebcastControlMessage { int32 action=2 }
668
569
  case "WebcastControlMessage":
669
570
  return { ...base, type: "control", action: getInt(f, 2) || getInt(f, 1) };
670
571
  case "WebcastRoomMessage":
671
572
  case "RoomMessage":
672
573
  return { ...base, type: "room", status: getInt(f, 2) };
574
+ // Proto: WebcastLiveIntroMessage { uint64 id=2, string description=4, User user=5 }
673
575
  case "WebcastLiveIntroMessage": {
674
576
  const roomId = String(getInt(f, 2));
675
577
  const title = getStr(f, 4) || getStr(f, 2);
676
578
  return { ...base, type: "liveIntro", roomId, title };
677
579
  }
678
- case "WebcastLinkMicMethod": {
580
+ case "WebcastLinkMicMethod":
581
+ case "WebcastLinkmicBattleTaskMessage": {
679
582
  const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
680
583
  const users = [];
681
584
  const userBufs = getAllBytes(f, 2);
@@ -687,152 +590,6 @@ function parseWebcastMessage(method, payload) {
687
590
  }
688
591
  return { ...base, type: "linkMic", action, users };
689
592
  }
690
- case "WebcastLinkmicBattleTaskMessage": {
691
- const taskAction = getInt(f, 2);
692
- const battleRefId = getIntStr(f, 20) || "";
693
- let timerType = 0;
694
- let remainingSeconds = 0;
695
- let endTimestampS = 0;
696
- const timerBuf = getBytes(f, 5);
697
- if (timerBuf) {
698
- try {
699
- const tf = decodeProto(timerBuf);
700
- timerType = getInt(tf, 1);
701
- remainingSeconds = getInt(tf, 2);
702
- endTimestampS = getInt(tf, 3);
703
- } catch {
704
- }
705
- }
706
- let multiplier = 0;
707
- let missionDuration = 0;
708
- let missionTarget = 0;
709
- let missionType = "";
710
- const bonusBuf = getBytes(f, 3);
711
- if (bonusBuf) {
712
- try {
713
- const bf = decodeProto(bonusBuf);
714
- const missionBuf = getBytes(bf, 1);
715
- if (missionBuf) {
716
- const mf = decodeProto(missionBuf);
717
- missionTarget = getInt(mf, 1);
718
- const items = getAllBytes(mf, 2);
719
- for (const item of items) {
720
- try {
721
- const itemFields = decodeProto(item);
722
- const descBuf = getBytes(itemFields, 2);
723
- if (descBuf) {
724
- const descFields = decodeProto(descBuf);
725
- const paramBufs = getAllBytes(descFields, 2);
726
- for (const pb of paramBufs) {
727
- try {
728
- const pf = decodeProto(pb);
729
- const key = getStr(pf, 1);
730
- const val = getStr(pf, 2);
731
- if (key === "multi" && val) multiplier = parseInt(val) || 0;
732
- if (key === "dur" && val) missionDuration = parseInt(val) || 0;
733
- if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
734
- } catch {
735
- }
736
- }
737
- const typeStr = getStr(descFields, 1);
738
- if (typeStr) missionType = typeStr;
739
- }
740
- } catch {
741
- }
742
- }
743
- const countdownBuf = getBytes(mf, 3);
744
- if (countdownBuf) {
745
- try {
746
- const cf = decodeProto(countdownBuf);
747
- if (!missionDuration) missionDuration = getInt(cf, 2);
748
- } catch {
749
- }
750
- }
751
- }
752
- } catch {
753
- }
754
- }
755
- if (!missionType) {
756
- const descBuf6 = getBytes(f, 6);
757
- if (descBuf6) {
758
- try {
759
- const d6 = decodeProto(descBuf6);
760
- const innerBuf = getBytes(d6, 1);
761
- if (innerBuf) {
762
- const innerF = decodeProto(innerBuf);
763
- const typeStr = getStr(innerF, 1);
764
- if (typeStr) missionType = typeStr;
765
- const paramBufs = getAllBytes(innerF, 2);
766
- for (const pb of paramBufs) {
767
- try {
768
- const pf = decodeProto(pb);
769
- const key = getStr(pf, 1);
770
- const val = getStr(pf, 2);
771
- if (key === "multi" && val) multiplier = parseInt(val) || 0;
772
- if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
773
- } catch {
774
- }
775
- }
776
- }
777
- } catch {
778
- }
779
- }
780
- }
781
- return {
782
- ...base,
783
- type: "battleTask",
784
- taskAction,
785
- battleRefId,
786
- missionType,
787
- multiplier,
788
- missionDuration,
789
- missionTarget,
790
- remainingSeconds,
791
- endTimestampS,
792
- timerType
793
- };
794
- }
795
- case "WebcastBarrageMessage": {
796
- const msgType = getInt(f, 3);
797
- const duration = getInt(f, 4);
798
- const displayType = getInt(f, 5);
799
- const subType = getInt(f, 6);
800
- let defaultPattern = "";
801
- let content = "";
802
- const contentBuf = getBytes(f, 2);
803
- if (contentBuf) {
804
- try {
805
- const cf = decodeProto(contentBuf);
806
- defaultPattern = getStr(cf, 1) || "";
807
- const paramBufs = getAllBytes(cf, 2);
808
- const params = [];
809
- for (const pb of paramBufs) {
810
- try {
811
- const pf = decodeProto(pb);
812
- const key = getStr(pf, 1);
813
- const val = getStr(pf, 2);
814
- if (key && val) params.push(`${key}=${val}`);
815
- } catch {
816
- }
817
- }
818
- if (params.length > 0) content = params.join(", ");
819
- } catch {
820
- }
821
- }
822
- if (!defaultPattern) {
823
- defaultPattern = getStr(f, 1) || "";
824
- }
825
- return {
826
- ...base,
827
- type: "barrage",
828
- msgType,
829
- subType,
830
- displayType,
831
- duration,
832
- defaultPattern,
833
- content
834
- };
835
- }
836
593
  default:
837
594
  return { ...base, type: "unknown", method };
838
595
  }
@@ -857,12 +614,10 @@ function parseWebcastResponse(payload) {
857
614
  // src/client.ts
858
615
  var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
859
616
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
860
- function httpGet(url, headers, agent) {
617
+ function httpGet(url, headers) {
861
618
  return new Promise((resolve, reject) => {
862
619
  const mod = url.startsWith("https") ? https : http;
863
- const opts = { headers, agent };
864
- const parsed = new import_url.URL(url);
865
- const req = mod.get(parsed, opts, (res) => {
620
+ const req = mod.get(url, { headers }, (res) => {
866
621
  const chunks = [];
867
622
  const enc = res.headers["content-encoding"];
868
623
  const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
@@ -888,15 +643,21 @@ function getWsHost(clusterRegion) {
888
643
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
889
644
  return "webcast-ws.tiktok.com";
890
645
  }
891
- var TikTokLive = class extends import_events.EventEmitter {
646
+ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
892
647
  ws = null;
893
648
  heartbeatTimer = null;
894
649
  reconnectAttempts = 0;
895
650
  intentionalClose = false;
896
651
  _connected = false;
652
+ _destroyed = false;
897
653
  _eventCount = 0;
898
654
  _roomId = "";
899
- _ownerUserId = "";
655
+ // Cache host identities from battle events for enriching battleArmies
656
+ // Bounded to MAX_BATTLE_HOSTS entries to prevent unbounded memory growth
657
+ static MAX_BATTLE_HOSTS = 50;
658
+ _battleHosts = /* @__PURE__ */ new Map();
659
+ // Track in-flight host resolution requests to avoid duplicate API calls
660
+ _pendingHostResolves = /* @__PURE__ */ new Set();
900
661
  uniqueId;
901
662
  signServerUrl;
902
663
  apiKey;
@@ -904,13 +665,11 @@ var TikTokLive = class extends import_events.EventEmitter {
904
665
  maxReconnectAttempts;
905
666
  heartbeatInterval;
906
667
  debug;
907
- _sessionId;
908
- _ttTargetIdc;
909
- proxyAgent;
910
668
  _presetRoomId;
911
- _presetTtwid;
669
+ _presetSessionId;
912
670
  constructor(options) {
913
671
  super();
672
+ this.setMaxListeners(20);
914
673
  this.uniqueId = options.uniqueId.replace(/^@/, "");
915
674
  this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
916
675
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
@@ -919,35 +678,60 @@ var TikTokLive = class extends import_events.EventEmitter {
919
678
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
920
679
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
921
680
  this.debug = options.debug ?? false;
922
- this._sessionId = options.sessionId;
923
- this._ttTargetIdc = options.ttTargetIdc;
924
- if (options.agent) {
925
- this.proxyAgent = options.agent;
926
- }
927
- this._presetRoomId = options.roomId;
928
- this._presetTtwid = options.ttwid;
681
+ this._presetRoomId = options.roomId || "";
682
+ this._presetSessionId = options.sessionId || "";
929
683
  }
930
684
  async connect() {
685
+ if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
931
686
  this.intentionalClose = false;
932
- let ttwid = this._presetTtwid || "";
933
- let roomId = this._presetRoomId || "";
934
- let ownerUserId = "";
687
+ let ttwid = this._presetSessionId;
688
+ let roomId = this._presetRoomId;
935
689
  let clusterRegion = "";
936
- if (!roomId || !ttwid) {
937
- const targetUrl = `https://www.tiktok.com/@${this.uniqueId}/live`;
938
- const resp = await httpGet(targetUrl, {
690
+ if (roomId && ttwid) {
691
+ } else if (roomId && !ttwid) {
692
+ try {
693
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
694
+ method: "POST",
695
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
696
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
697
+ });
698
+ if (credResp.ok) {
699
+ const credData = await credResp.json();
700
+ if (credData.data?.cookies) {
701
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
702
+ if (m) ttwid = m[1];
703
+ }
704
+ }
705
+ } catch {
706
+ }
707
+ if (!ttwid) {
708
+ try {
709
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
710
+ "User-Agent": DEFAULT_UA,
711
+ "Accept": "text/html"
712
+ });
713
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
714
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
715
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
716
+ break;
717
+ }
718
+ }
719
+ } catch {
720
+ }
721
+ }
722
+ } else {
723
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
939
724
  "User-Agent": DEFAULT_UA,
940
725
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
941
726
  "Accept-Encoding": "gzip, deflate, br",
942
727
  "Accept-Language": "en-US,en;q=0.9"
943
- }, this.proxyAgent);
728
+ });
944
729
  for (const sc of [resp.headers["set-cookie"] || []].flat()) {
945
730
  if (typeof sc === "string" && sc.startsWith("ttwid=")) {
946
731
  ttwid = sc.split(";")[0].split("=").slice(1).join("=");
947
732
  break;
948
733
  }
949
734
  }
950
- if (!ttwid) throw new Error("Failed to obtain session cookie");
951
735
  const html = resp.body.toString();
952
736
  const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
953
737
  if (sigiMatch) {
@@ -956,19 +740,15 @@ var TikTokLive = class extends import_events.EventEmitter {
956
740
  const jsonStr = JSON.stringify(json);
957
741
  const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
958
742
  if (m) roomId = m[1];
959
- const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
960
- if (ownerUser?.id) {
961
- ownerUserId = String(ownerUser.id);
962
- }
963
743
  } catch {
964
744
  }
965
745
  }
966
- if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
967
746
  const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
968
747
  clusterRegion = crMatch ? crMatch[1] : "";
969
748
  }
749
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
750
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
970
751
  this._roomId = roomId;
971
- this._ownerUserId = ownerUserId;
972
752
  const wsHost = getWsHost(clusterRegion);
973
753
  const wsParams = new URLSearchParams({
974
754
  version_code: "270000",
@@ -1022,21 +802,12 @@ var TikTokLive = class extends import_events.EventEmitter {
1022
802
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
1023
803
  }
1024
804
  return new Promise((resolve, reject) => {
1025
- let cookieHeader = ttwid ? `ttwid=${ttwid}` : "";
1026
- if (this._sessionId) {
1027
- const sessionCookies = `sessionid=${this._sessionId}; sessionid_ss=${this._sessionId}; sid_tt=${this._sessionId}`;
1028
- cookieHeader = cookieHeader ? `${cookieHeader}; ${sessionCookies}` : sessionCookies;
1029
- if (this._ttTargetIdc) {
1030
- cookieHeader += `; tt-target-idc=${this._ttTargetIdc}`;
1031
- }
1032
- }
1033
805
  this.ws = new import_ws.default(wsUrl, {
1034
806
  headers: {
1035
807
  "User-Agent": DEFAULT_UA,
1036
- "Cookie": cookieHeader,
808
+ "Cookie": `ttwid=${ttwid}`,
1037
809
  "Origin": "https://www.tiktok.com"
1038
- },
1039
- agent: this.proxyAgent
810
+ }
1040
811
  });
1041
812
  this.ws.on("open", () => {
1042
813
  this._connected = true;
@@ -1048,8 +819,7 @@ var TikTokLive = class extends import_events.EventEmitter {
1048
819
  roomId,
1049
820
  wsHost,
1050
821
  clusterRegion,
1051
- connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1052
- ownerUserId: ownerUserId || void 0
822
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
1053
823
  };
1054
824
  this.emit("connected");
1055
825
  this.emit("roomInfo", roomInfo);
@@ -1079,42 +849,76 @@ var TikTokLive = class extends import_events.EventEmitter {
1079
849
  this.intentionalClose = true;
1080
850
  this.stopHeartbeat();
1081
851
  if (this.ws) {
852
+ this.ws.removeAllListeners();
1082
853
  this.ws.close(1e3);
1083
854
  this.ws = null;
1084
855
  }
1085
856
  this._connected = false;
1086
857
  }
858
+ /**
859
+ * Fully destroy the client, releasing all resources and listeners.
860
+ * After calling destroy(), the instance cannot be reused — create a new one.
861
+ */
862
+ destroy() {
863
+ this.disconnect();
864
+ this._destroyed = true;
865
+ this._battleHosts.clear();
866
+ this._eventCount = 0;
867
+ this._roomId = "";
868
+ this.removeAllListeners();
869
+ }
1087
870
  get connected() {
1088
871
  return this._connected;
1089
872
  }
873
+ get destroyed() {
874
+ return this._destroyed;
875
+ }
1090
876
  get eventCount() {
1091
877
  return this._eventCount;
1092
878
  }
1093
879
  get roomId() {
1094
880
  return this._roomId;
1095
881
  }
1096
- /** Get the stored session ID (if any) */
1097
- get sessionId() {
1098
- return this._sessionId;
1099
- }
1100
- /** Update the session ID at runtime (e.g. after TikTok login) */
1101
- setSession(sessionId, ttTargetIdc) {
1102
- this._sessionId = sessionId;
1103
- if (ttTargetIdc) this._ttTargetIdc = ttTargetIdc;
1104
- }
1105
882
  /**
1106
- * Build a cookie header string for authenticated API requests (e.g. ranklist).
1107
- * Returns undefined if no session is set.
883
+ * Get live stream video URLs for a TikTok user.
884
+ * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
885
+ * This is a standalone method — no WebSocket connection required.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * const stream = await TikTokLive.getStreamUrl({
890
+ * uniqueId: 'username',
891
+ * apiKey: 'your-api-key',
892
+ * });
893
+ * if (stream.alive) {
894
+ * console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
895
+ * console.log(stream.flvPullUrl); // Best FLV URL
896
+ * }
897
+ * ```
1108
898
  */
1109
- buildSessionCookieHeader() {
1110
- if (!this._sessionId) return void 0;
1111
- const parts = [
1112
- `sessionid=${this._sessionId}`,
1113
- `sessionid_ss=${this._sessionId}`,
1114
- `sid_tt=${this._sessionId}`
1115
- ];
1116
- if (this._ttTargetIdc) parts.push(`tt-target-idc=${this._ttTargetIdc}`);
1117
- return parts.join("; ");
899
+ static async getStreamUrl(options) {
900
+ const serverUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
901
+ const resp = await fetch(`${serverUrl}/webcast/room_video`, {
902
+ method: "POST",
903
+ headers: {
904
+ "Content-Type": "application/json",
905
+ "x-api-key": options.apiKey
906
+ },
907
+ body: JSON.stringify({ unique_id: options.uniqueId.replace(/^@/, "") })
908
+ });
909
+ const data = await resp.json();
910
+ if (data.status_code !== 0) {
911
+ throw new Error(data.error || "Failed to get stream URL");
912
+ }
913
+ const d = data.data;
914
+ return {
915
+ roomId: d.room_id,
916
+ alive: d.alive ?? false,
917
+ streamUrls: d.stream_urls || {},
918
+ defaultQuality: d.default_quality || "",
919
+ flvPullUrl: d.flv_pull_url || void 0,
920
+ hlsPullUrl: d.hls_pull_url || void 0
921
+ };
1118
922
  }
1119
923
  on(event, listener) {
1120
924
  return super.on(event, listener);
@@ -1149,567 +953,120 @@ var TikTokLive = class extends import_events.EventEmitter {
1149
953
  const events = parseWebcastResponse(inner);
1150
954
  for (const evt of events) {
1151
955
  this._eventCount++;
1152
- this.emit("event", evt);
1153
- this.emit(evt.type, evt);
1154
- }
1155
- }
1156
- } catch {
1157
- }
1158
- }
1159
- startHeartbeat(roomId) {
1160
- this.stopHeartbeat();
1161
- this.heartbeatTimer = setInterval(() => {
1162
- if (this.ws?.readyState === import_ws.default.OPEN) {
1163
- this.ws.send(buildHeartbeat(roomId));
1164
- }
1165
- }, this.heartbeatInterval);
1166
- }
1167
- stopHeartbeat() {
1168
- if (this.heartbeatTimer) {
1169
- clearInterval(this.heartbeatTimer);
1170
- this.heartbeatTimer = null;
1171
- }
1172
- }
1173
- };
1174
-
1175
- // src/captions.ts
1176
- var import_events2 = require("events");
1177
- var import_ws2 = __toESM(require("ws"));
1178
- var FLV_TAG_AUDIO = 8;
1179
- var FLV_HEADER_SIZE = 9;
1180
- var FLV_PREV_TAG_SIZE = 4;
1181
- var FlvAudioExtractor = class {
1182
- buffer = new Uint8Array(0);
1183
- headerParsed = false;
1184
- onAudio;
1185
- aacProfile = 2;
1186
- sampleRateIndex = 4;
1187
- channelConfig = 2;
1188
- ascParsed = false;
1189
- constructor(onAudio) {
1190
- this.onAudio = onAudio;
1191
- }
1192
- parseASC(asc) {
1193
- if (asc.length < 2) return;
1194
- this.aacProfile = asc[0] >> 3 & 31;
1195
- this.sampleRateIndex = (asc[0] & 7) << 1 | asc[1] >> 7 & 1;
1196
- this.channelConfig = asc[1] >> 3 & 15;
1197
- this.ascParsed = true;
1198
- }
1199
- buildAdtsHeader(frameLength) {
1200
- const adts = new Uint8Array(7);
1201
- const fullLength = frameLength + 7;
1202
- const profile = this.aacProfile - 1;
1203
- adts[0] = 255;
1204
- adts[1] = 241;
1205
- adts[2] = (profile & 3) << 6 | (this.sampleRateIndex & 15) << 2 | this.channelConfig >> 2 & 1;
1206
- adts[3] = (this.channelConfig & 3) << 6 | fullLength >> 11 & 3;
1207
- adts[4] = fullLength >> 3 & 255;
1208
- adts[5] = (fullLength & 7) << 5 | 31;
1209
- adts[6] = 252;
1210
- return adts;
1211
- }
1212
- push(chunk) {
1213
- const newBuf = new Uint8Array(this.buffer.length + chunk.length);
1214
- newBuf.set(this.buffer, 0);
1215
- newBuf.set(chunk, this.buffer.length);
1216
- this.buffer = newBuf;
1217
- if (!this.headerParsed) {
1218
- if (this.buffer.length < FLV_HEADER_SIZE + FLV_PREV_TAG_SIZE) return;
1219
- if (this.buffer[0] !== 70 || this.buffer[1] !== 76 || this.buffer[2] !== 86) return;
1220
- const dv = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
1221
- const dataOffset = dv.getUint32(5);
1222
- this.buffer = this.buffer.subarray(dataOffset + FLV_PREV_TAG_SIZE);
1223
- this.headerParsed = true;
1224
- }
1225
- while (this.buffer.length >= 11) {
1226
- const tagType = this.buffer[0] & 31;
1227
- const dataSize = this.buffer[1] << 16 | this.buffer[2] << 8 | this.buffer[3];
1228
- const totalTagSize = 11 + dataSize + FLV_PREV_TAG_SIZE;
1229
- if (this.buffer.length < totalTagSize) break;
1230
- if (tagType === FLV_TAG_AUDIO) {
1231
- const audioData = this.buffer.subarray(11, 11 + dataSize);
1232
- if (audioData.length > 0) {
1233
- const soundFormat = audioData[0] >> 4 & 15;
1234
- if (soundFormat === 10 && audioData.length > 2) {
1235
- const aacPacketType = audioData[1];
1236
- if (aacPacketType === 0) {
1237
- this.parseASC(audioData.subarray(2));
1238
- } else if (aacPacketType === 1 && this.ascParsed) {
1239
- const rawFrame = audioData.subarray(2);
1240
- const adtsHeader = this.buildAdtsHeader(rawFrame.length);
1241
- const adtsFrame = new Uint8Array(adtsHeader.length + rawFrame.length);
1242
- adtsFrame.set(adtsHeader, 0);
1243
- adtsFrame.set(rawFrame, adtsHeader.length);
1244
- this.onAudio(adtsFrame);
956
+ if (evt.type === "battle") {
957
+ for (const team of evt.teams) {
958
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
959
+ if (host) {
960
+ this._battleHosts.set(team.hostUserId, host.user);
961
+ team.hostUser = host.user;
962
+ }
963
+ }
964
+ if (this._battleHosts.size > _TikTokLive.MAX_BATTLE_HOSTS) {
965
+ const excess = this._battleHosts.size - _TikTokLive.MAX_BATTLE_HOSTS;
966
+ const keys = [...this._battleHosts.keys()].slice(0, excess);
967
+ for (const key of keys) this._battleHosts.delete(key);
1245
968
  }
1246
969
  }
1247
- }
1248
- }
1249
- this.buffer = this.buffer.subarray(totalTagSize);
1250
- }
1251
- }
1252
- reset() {
1253
- this.buffer = new Uint8Array(0);
1254
- this.headerParsed = false;
1255
- this.ascParsed = false;
1256
- }
1257
- };
1258
- var DEFAULT_CAPTIONS_SERVER = "wss://api.tik.tools";
1259
- var TikTokCaptions = class extends import_events2.EventEmitter {
1260
- ws = null;
1261
- _connected = false;
1262
- intentionalClose = false;
1263
- reconnectAttempts = 0;
1264
- uniqueId;
1265
- apiKey;
1266
- serverUrl;
1267
- autoReconnect;
1268
- maxReconnectAttempts;
1269
- debug;
1270
- _translate;
1271
- _diarization;
1272
- _maxDurationMinutes;
1273
- _language;
1274
- streamAbortController = null;
1275
- flvExtractor = null;
1276
- streamUrl = null;
1277
- constructor(options) {
1278
- super();
1279
- this.uniqueId = options.uniqueId.replace(/^@/, "");
1280
- if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
1281
- this.apiKey = options.apiKey;
1282
- this._language = options.language || "";
1283
- this._translate = options.translate || "";
1284
- this._diarization = options.diarization ?? true;
1285
- this._maxDurationMinutes = options.maxDurationMinutes ?? 60;
1286
- this.serverUrl = (options.signServerUrl || DEFAULT_CAPTIONS_SERVER).replace(/\/$/, "");
1287
- this.autoReconnect = options.autoReconnect ?? true;
1288
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
1289
- this.debug = options.debug ?? false;
1290
- }
1291
- /**
1292
- * Start real-time captions for the configured TikTok user.
1293
- * Connects to the captions WebSocket relay and begins transcription
1294
- * once the user goes live (or immediately if already live).
1295
- */
1296
- async start() {
1297
- this.intentionalClose = false;
1298
- const wsUrl = this.buildWsUrl();
1299
- if (this.debug) console.log(`[Captions] Connecting to ${wsUrl}`);
1300
- return new Promise((resolve, reject) => {
1301
- this.ws = new import_ws2.default(wsUrl);
1302
- this.ws.on("open", () => {
1303
- this._connected = true;
1304
- this.reconnectAttempts = 0;
1305
- if (this.debug) console.log("[Captions] Connected");
1306
- this.emit("connected");
1307
- resolve();
1308
- });
1309
- this.ws.on("message", (data) => {
1310
- this.handleMessage(typeof data === "string" ? data : data.toString());
1311
- });
1312
- this.ws.on("close", (code, reason) => {
1313
- this._connected = false;
1314
- const reasonStr = reason?.toString() || "";
1315
- if (this.debug) console.log(`[Captions] Disconnected: ${code} ${reasonStr}`);
1316
- this.emit("disconnected", code, reasonStr);
1317
- if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1318
- this.reconnectAttempts++;
1319
- const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
1320
- if (this.debug) console.log(`[Captions] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
1321
- setTimeout(() => this.start().catch((e) => this.emit("error", { code: "RECONNECT_FAILED", message: e.message })), delay);
1322
- }
1323
- });
1324
- this.ws.on("error", (err) => {
1325
- this.emit("error", { code: "WS_ERROR", message: err.message });
1326
- if (!this._connected) reject(err);
1327
- });
1328
- });
1329
- }
1330
- /**
1331
- * Stop captions and disconnect.
1332
- */
1333
- stop() {
1334
- this.intentionalClose = true;
1335
- if (this.streamAbortController) {
1336
- this.streamAbortController.abort();
1337
- this.streamAbortController = null;
1338
- }
1339
- if (this.flvExtractor) {
1340
- this.flvExtractor.reset();
1341
- this.flvExtractor = null;
1342
- }
1343
- if (this.ws) {
1344
- this.send({ action: "stop" });
1345
- this.ws.close(1e3);
1346
- this.ws = null;
1347
- }
1348
- this._connected = false;
1349
- }
1350
- /**
1351
- * Switch the translation target language on-the-fly.
1352
- * Causes a brief interruption while the transcription engine reconfigures.
1353
- */
1354
- setLanguage(language) {
1355
- this._language = language;
1356
- this.send({ action: "set_language", language });
1357
- }
1358
- /**
1359
- * Request a credit balance update from the server.
1360
- */
1361
- getCredits() {
1362
- this.send({ action: "get_credits" });
1363
- }
1364
- /** Whether the WebSocket is currently connected */
1365
- get connected() {
1366
- return this._connected;
1367
- }
1368
- /** The current target language */
1369
- get language() {
1370
- return this._language;
1371
- }
1372
- on(event, listener) {
1373
- return super.on(event, listener);
1374
- }
1375
- once(event, listener) {
1376
- return super.once(event, listener);
1377
- }
1378
- off(event, listener) {
1379
- return super.off(event, listener);
1380
- }
1381
- emit(event, ...args) {
1382
- return super.emit(event, ...args);
1383
- }
1384
- buildWsUrl() {
1385
- const base = this.serverUrl.replace(/^http/, "ws");
1386
- const params = new URLSearchParams({
1387
- uniqueId: this.uniqueId,
1388
- apiKey: this.apiKey
1389
- });
1390
- if (this._language) params.set("language", this._language);
1391
- if (this._translate) params.set("translate", this._translate);
1392
- if (this._diarization !== void 0) params.set("diarization", String(this._diarization));
1393
- if (this._maxDurationMinutes) params.set("max_duration_minutes", String(this._maxDurationMinutes));
1394
- return `${base}/captions?${params}`;
1395
- }
1396
- send(msg) {
1397
- if (this.ws?.readyState === import_ws2.default.OPEN) {
1398
- this.ws.send(JSON.stringify(msg));
1399
- }
1400
- }
1401
- handleMessage(raw) {
1402
- try {
1403
- const msg = JSON.parse(raw);
1404
- switch (msg.type) {
1405
- case "stream_info":
1406
- if (this.debug) console.log(`[Captions] Received stream_info: flv=${!!msg.flvUrl}, hls=${!!msg.hlsUrl}, ao=${!!msg.audioOnlyUrl}`);
1407
- this.connectToStream(msg);
1408
- break;
1409
- case "caption":
1410
- this.emit("caption", {
1411
- text: msg.text,
1412
- language: msg.language,
1413
- isFinal: msg.isFinal,
1414
- confidence: msg.confidence,
1415
- speaker: msg.speaker,
1416
- startMs: msg.startMs,
1417
- endMs: msg.endMs
1418
- });
1419
- break;
1420
- case "translation":
1421
- this.emit("translation", {
1422
- text: msg.text,
1423
- language: msg.language,
1424
- isFinal: msg.isFinal,
1425
- confidence: msg.confidence,
1426
- speaker: msg.speaker
1427
- });
1428
- break;
1429
- case "status":
1430
- this.emit("status", {
1431
- status: msg.status,
1432
- uniqueId: msg.uniqueId,
1433
- roomId: msg.roomId,
1434
- language: msg.language,
1435
- message: msg.message
1436
- });
1437
- break;
1438
- case "credits":
1439
- this.emit("credits", {
1440
- remaining: msg.remaining,
1441
- total: msg.total,
1442
- used: msg.used,
1443
- warning: msg.warning
1444
- });
1445
- break;
1446
- case "credits_low":
1447
- this.emit("credits_low", {
1448
- remaining: msg.remaining,
1449
- total: msg.total,
1450
- percent: msg.percent
1451
- });
1452
- break;
1453
- case "error":
1454
- this.emit("error", {
1455
- code: msg.code,
1456
- message: msg.message
1457
- });
1458
- break;
1459
- // Handle interim/final captions from server (sentence-level accumulation)
1460
- case "interim":
1461
- this.emit("caption", {
1462
- text: msg.text,
1463
- language: msg.language,
1464
- isFinal: false,
1465
- confidence: msg.confidence || 0,
1466
- speaker: msg.speaker
1467
- });
1468
- break;
1469
- case "final":
1470
- this.emit("caption", {
1471
- text: msg.text,
1472
- language: msg.language,
1473
- isFinal: true,
1474
- confidence: msg.confidence || 0,
1475
- speaker: msg.speaker
1476
- });
1477
- break;
1478
- case "translation_interim":
1479
- this.emit("translation", {
1480
- text: msg.text,
1481
- language: msg.language,
1482
- isFinal: false,
1483
- confidence: msg.confidence || 0,
1484
- speaker: msg.speaker
1485
- });
1486
- break;
1487
- case "translation_final":
1488
- this.emit("translation", {
1489
- text: msg.text,
1490
- language: msg.language,
1491
- isFinal: true,
1492
- confidence: msg.confidence || 0,
1493
- speaker: msg.speaker
1494
- });
1495
- break;
1496
- default:
1497
- if (this.debug) {
1498
- if (this.debug) console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
970
+ if (evt.type === "battleArmies") {
971
+ const unresolvedIds = [];
972
+ for (const team of evt.teams) {
973
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
974
+ if (host) {
975
+ team.hostUser = host.user;
976
+ this._battleHosts.set(team.hostUserId, host.user);
977
+ } else {
978
+ const cached = this._battleHosts.get(team.hostUserId);
979
+ if (cached) {
980
+ team.hostUser = cached;
981
+ } else if (team.hostUserId && team.hostUserId !== "0") {
982
+ unresolvedIds.push(team.hostUserId);
983
+ }
984
+ }
985
+ }
986
+ if (unresolvedIds.length > 0) {
987
+ this.resolveHostUsers(unresolvedIds, evt).catch(() => {
988
+ });
989
+ continue;
990
+ }
1499
991
  }
992
+ this.emit("event", evt);
993
+ this.emit(evt.type, evt);
994
+ }
1500
995
  }
1501
996
  } catch {
1502
- if (this.debug) console.error("[Captions] Failed to parse message:", raw);
1503
997
  }
1504
998
  }
1505
999
  /**
1506
- * Connect to the TikTok FLV stream and extract audio.
1507
- * Sends binary audio buffers to the server via WebSocket.
1000
+ * Resolve unknown host user IDs via the sign server API.
1001
+ * Caches results and re-emits the battleArmies event with enriched hostUser data.
1508
1002
  */
1509
- async connectToStream(streamInfo) {
1510
- const url = streamInfo.audioOnlyUrl || streamInfo.flvUrl;
1511
- if (!url) {
1512
- this.emit("error", { code: "NO_STREAM_URL", message: "Server did not provide a usable stream URL" });
1003
+ async resolveHostUsers(userIds, originalEvt) {
1004
+ const toResolve = userIds.filter((id) => !this._pendingHostResolves.has(id));
1005
+ if (toResolve.length === 0) {
1006
+ this.emit("event", originalEvt);
1007
+ this.emit(originalEvt.type, originalEvt);
1513
1008
  return;
1514
1009
  }
1515
- this.streamUrl = url;
1516
- if (this.debug) console.log(`[Captions] connectToStream: URL selected: ${url.substring(0, 80)}...`);
1517
- if (this.streamAbortController) {
1518
- this.streamAbortController.abort();
1519
- }
1520
- this.streamAbortController = new AbortController();
1521
- let audioFramesSent = 0;
1522
- let audioBytesSent = 0;
1523
- this.flvExtractor = new FlvAudioExtractor((adtsFrame) => {
1524
- if (this.ws?.readyState === import_ws2.default.OPEN) {
1525
- this.ws.send(adtsFrame);
1526
- audioFramesSent++;
1527
- audioBytesSent += adtsFrame.length;
1528
- if (this.debug && (audioFramesSent <= 3 || audioFramesSent % 100 === 0)) {
1529
- if (this.debug) console.log(`[Captions] Audio frame #${audioFramesSent}: ${adtsFrame.length}b (total: ${audioBytesSent}b)`);
1530
- }
1531
- } else if (this.debug && audioFramesSent === 0) {
1532
- if (this.debug) console.log(`[Captions] WARNING: WS not open (readyState=${this.ws?.readyState}), cannot send audio`);
1533
- }
1534
- });
1010
+ for (const id of toResolve) this._pendingHostResolves.add(id);
1535
1011
  try {
1536
- if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
1537
- const resp = await fetch(url, {
1538
- signal: this.streamAbortController.signal,
1012
+ const url = `${this.signServerUrl}/webcast/resolve_user_ids`;
1013
+ const res = await fetch(url, {
1014
+ method: "POST",
1539
1015
  headers: {
1540
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1541
- }
1016
+ "Content-Type": "application/json",
1017
+ "x-api-key": this.apiKey
1018
+ },
1019
+ body: JSON.stringify({ user_ids: toResolve }),
1020
+ signal: AbortSignal.timeout(5e3)
1542
1021
  });
1543
- if (this.debug) console.log(`[Captions] connectToStream: fetch returned status=${resp.status}, hasBody=${!!resp.body}`);
1544
- if (!resp.ok || !resp.body) {
1545
- throw new Error(`FLV stream HTTP ${resp.status}`);
1546
- }
1547
- if (this.debug) console.log(`[Captions] FLV stream connected (${resp.status})`);
1548
- const reader = resp.body.getReader ? resp.body.getReader() : null;
1549
- if (this.debug) console.log(`[Captions] connectToStream: hasReader=${!!reader}, hasAsyncIterator=${typeof resp.body[Symbol.asyncIterator] === "function"}`);
1550
- if (reader) {
1551
- const processStream = async () => {
1552
- let chunks = 0;
1553
- try {
1554
- while (true) {
1555
- const { done, value } = await reader.read();
1556
- if (done || this.intentionalClose) {
1557
- if (this.debug) console.log(`[Captions] FLV stream ended (done=${done}, intentionalClose=${this.intentionalClose}), chunks=${chunks}, audioFrames=${audioFramesSent}`);
1558
- break;
1559
- }
1560
- chunks++;
1561
- if (value && this.flvExtractor) {
1562
- this.flvExtractor.push(value);
1563
- }
1564
- if (this.debug && chunks <= 3) {
1565
- if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${value?.length || 0}b`);
1566
- }
1567
- }
1568
- } catch (err) {
1569
- if (err.name !== "AbortError" && !this.intentionalClose) {
1570
- if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
1571
- this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
1572
- } else if (this.debug) {
1573
- if (this.debug) console.log(`[Captions] FLV stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
1022
+ if (res.ok) {
1023
+ const json = await res.json();
1024
+ if (json.data) {
1025
+ for (const [uid, info] of Object.entries(json.data)) {
1026
+ if (info.username) {
1027
+ const hostUser = {
1028
+ id: uid,
1029
+ uniqueId: info.username,
1030
+ nickname: info.nickname || info.username,
1031
+ profilePicture: info.avatarUrl || void 0
1032
+ };
1033
+ this._battleHosts.set(uid, hostUser);
1574
1034
  }
1575
1035
  }
1576
- };
1577
- processStream();
1578
- } else if (typeof resp.body[Symbol.asyncIterator] === "function") {
1579
- const processNodeStream = async () => {
1580
- let chunks = 0;
1581
- try {
1582
- for await (const chunk of resp.body) {
1583
- if (this.intentionalClose) break;
1584
- chunks++;
1585
- const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
1586
- if (this.flvExtractor) {
1587
- this.flvExtractor.push(u8);
1036
+ if (originalEvt.type === "battleArmies") {
1037
+ for (const team of originalEvt.teams) {
1038
+ if (!team.hostUser) {
1039
+ const cached = this._battleHosts.get(team.hostUserId);
1040
+ if (cached) team.hostUser = cached;
1588
1041
  }
1589
- if (this.debug && chunks <= 3) {
1590
- if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
1591
- }
1592
- }
1593
- if (this.debug) console.log(`[Captions] Node stream ended, chunks=${chunks}, audioFrames=${audioFramesSent}`);
1594
- } catch (err) {
1595
- if (err.name !== "AbortError" && !this.intentionalClose) {
1596
- if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
1597
- this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
1598
- } else if (this.debug) {
1599
- if (this.debug) console.log(`[Captions] FLV node stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
1600
1042
  }
1601
1043
  }
1602
- };
1603
- processNodeStream();
1604
- } else {
1605
- if (this.debug) console.error(`[Captions] ERROR: resp.body has no getReader() and no asyncIterator!`);
1606
- }
1607
- } catch (err) {
1608
- if (err.name !== "AbortError" && !this.intentionalClose) {
1609
- if (this.debug) console.error("[Captions] FLV stream connect error:", err.message);
1610
- this.emit("error", { code: "STREAM_CONNECT_ERROR", message: err.message });
1044
+ }
1611
1045
  }
1046
+ } catch {
1047
+ } finally {
1048
+ for (const id of toResolve) this._pendingHostResolves.delete(id);
1049
+ this.emit("event", originalEvt);
1050
+ this.emit(originalEvt.type, originalEvt);
1612
1051
  }
1613
1052
  }
1614
- };
1615
-
1616
- // src/api.ts
1617
- var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
1618
- var PAGE_CACHE_TTL = 5 * 60 * 1e3;
1619
- async function getRanklist(opts) {
1620
- const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1621
- const ak = encodeURIComponent(opts.apiKey);
1622
- const body = {};
1623
- if (opts.uniqueId) body.unique_id = opts.uniqueId;
1624
- if (opts.roomId) body.room_id = opts.roomId;
1625
- if (opts.anchorId) body.anchor_id = opts.anchorId;
1626
- if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1627
- if (opts.type) body.type = opts.type;
1628
- if (opts.rankType) body.rank_type = opts.rankType;
1629
- const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
1630
- method: "POST",
1631
- headers: { "Content-Type": "application/json" },
1632
- body: JSON.stringify(body)
1633
- });
1634
- const data = await resp.json();
1635
- if (data.status_code === 20003) {
1636
- throw new Error(
1637
- data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
1638
- );
1639
- }
1640
- if (data.status_code !== 0) {
1641
- throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
1642
- }
1643
- if (data.sign_and_return) {
1644
- return data;
1645
- }
1646
- return data.data;
1647
- }
1648
- async function getRegionalRanklist(opts) {
1649
- const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1650
- const ak = encodeURIComponent(opts.apiKey);
1651
- const body = {};
1652
- if (opts.uniqueId) body.unique_id = opts.uniqueId;
1653
- if (opts.roomId) body.room_id = opts.roomId;
1654
- if (opts.anchorId) body.anchor_id = opts.anchorId;
1655
- if (opts.rankType) body.rank_type = opts.rankType;
1656
- if (opts.type) body.type = opts.type;
1657
- if (opts.gapInterval) body.gap_interval = opts.gapInterval;
1658
- if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1659
- if (opts.clientIp) body.client_ip = opts.clientIp;
1660
- const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
1661
- method: "POST",
1662
- headers: { "Content-Type": "application/json" },
1663
- body: JSON.stringify(body)
1664
- });
1665
- const data = await resp.json();
1666
- if (data.status_code !== 0) {
1667
- throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
1668
- }
1669
- return data;
1670
- }
1671
- async function getLiveFeed(opts) {
1672
- const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1673
- const params = new URLSearchParams();
1674
- params.set("apiKey", opts.apiKey);
1675
- if (opts.region) params.set("region", opts.region);
1676
- if (opts.channelId) params.set("channel_id", opts.channelId);
1677
- if (opts.count !== void 0) params.set("count", String(Math.min(opts.count, 50)));
1678
- if (opts.maxTime) params.set("max_time", opts.maxTime);
1679
- if (opts.sessionId) params.set("session_id", opts.sessionId);
1680
- if (opts.ttwid) params.set("ttwid", opts.ttwid);
1681
- if (opts.msToken) params.set("ms_token", opts.msToken);
1682
- const resp = await fetch(`${base}/webcast/feed?${params.toString()}`);
1683
- const data = await resp.json();
1684
- if (resp.status === 429) {
1685
- throw new Error(data.error || "Feed daily limit reached. Upgrade your plan for more calls.");
1686
- }
1687
- if (!resp.ok) {
1688
- throw new Error(data.error || `Feed request failed (HTTP ${resp.status})`);
1689
- }
1690
- return data;
1691
- }
1692
- async function fetchFeed(opts) {
1693
- const signed = await getLiveFeed(opts);
1694
- const headers = { ...signed.headers || {} };
1695
- if (signed.cookies) {
1696
- headers["Cookie"] = signed.cookies;
1053
+ startHeartbeat(roomId) {
1054
+ this.stopHeartbeat();
1055
+ this.heartbeatTimer = setInterval(() => {
1056
+ if (this.ws?.readyState === import_ws.default.OPEN) {
1057
+ this.ws.send(buildHeartbeat(roomId));
1058
+ }
1059
+ }, this.heartbeatInterval);
1697
1060
  }
1698
- const resp = await fetch(signed.signed_url, { headers, redirect: "follow" });
1699
- const text = await resp.text();
1700
- try {
1701
- return JSON.parse(text);
1702
- } catch {
1703
- return null;
1061
+ stopHeartbeat() {
1062
+ if (this.heartbeatTimer) {
1063
+ clearInterval(this.heartbeatTimer);
1064
+ this.heartbeatTimer = null;
1065
+ }
1704
1066
  }
1705
- }
1067
+ };
1706
1068
  // Annotate the CommonJS export names for ESM import in node:
1707
1069
  0 && (module.exports = {
1708
- TikTokCaptions,
1709
- TikTokLive,
1710
- fetchFeed,
1711
- getLiveFeed,
1712
- getRanklist,
1713
- getRegionalRanklist
1070
+ TikTokLive
1714
1071
  });
1715
1072
  //# sourceMappingURL=index.js.map