@tiktool/live 2.6.7 → 2.7.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.
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 {
@@ -336,10 +264,9 @@ function parseBattleTeamFromArmies(itemBuf) {
336
264
  const points = getInt(gf, 2);
337
265
  teamScore += points;
338
266
  const userBufs = getAllBytes(gf, 1);
267
+ const individualScore = userBufs.length === 1 ? points : -1;
339
268
  for (const ub of userBufs) {
340
269
  try {
341
- const userFields = decodeProto(ub);
342
- const individualScore = getInt(userFields, 2);
343
270
  const user = parseUser(ub);
344
271
  users.push({ user, score: individualScore });
345
272
  } catch {
@@ -348,19 +275,7 @@ function parseBattleTeamFromArmies(itemBuf) {
348
275
  } catch {
349
276
  }
350
277
  }
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 };
278
+ return { hostUserId, score: teamScore, users };
364
279
  }
365
280
  function parseWebcastMessage(method, payload) {
366
281
  const f = decodeProto(payload);
@@ -375,29 +290,42 @@ function parseWebcastMessage(method, payload) {
375
290
  }
376
291
  }
377
292
  switch (method) {
293
+ // Proto: WebcastChatMessage { MessageType type=1, User user=2, string comment=3, ..., repeated WebcastSubEmote emotes=12 }
294
+ // WebcastSubEmote { int32 placeInComment=1, EmoteDetails emote=2 }
295
+ // EmoteDetails { string emoteId=1, EmoteImage image=2 }
296
+ // EmoteImage { string imageUrl=1 }
378
297
  case "WebcastChatMessage": {
379
298
  const userBuf = getBytes(f, 2);
380
299
  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;
300
+ const comment = getStr(f, 3);
301
+ const emotes = [];
302
+ const emoteBufs12 = getAllBytes(f, 12);
303
+ const emoteBufs13 = getAllBytes(f, 13);
304
+ const emoteBufs = emoteBufs13.length > 0 ? emoteBufs13 : emoteBufs12;
305
+ for (const eb of emoteBufs) {
306
+ try {
307
+ const ef = decodeProto(eb);
308
+ const placeInComment = getInt(ef, 1);
309
+ const detailBuf = getBytes(ef, 2);
310
+ if (detailBuf) {
311
+ const df = decodeProto(detailBuf);
312
+ const emoteId = getStr(df, 1);
313
+ let imageUrl = "";
314
+ const imgBuf = getBytes(df, 2);
315
+ if (imgBuf) {
316
+ const imgf = decodeProto(imgBuf);
317
+ imageUrl = getStr(imgf, 1);
318
+ }
319
+ if (emoteId) {
320
+ emotes.push({ emoteId, imageUrl, placeInComment });
393
321
  }
394
- } catch {
395
322
  }
323
+ } catch {
396
324
  }
397
325
  }
398
- const comment = replaceEmotes(rawComment, emoteImages);
399
- return { ...base, type: "chat", user, comment };
326
+ return { ...base, type: "chat", user, comment, emotes };
400
327
  }
328
+ // Proto: WebcastMemberMessage { User user=2, WebcastMessageEvent event=1 }
401
329
  case "WebcastMemberMessage": {
402
330
  const userBuf = getBytes(f, 2);
403
331
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -415,6 +343,7 @@ function parseWebcastMessage(method, payload) {
415
343
  }
416
344
  return { ...base, type: "member", user, action };
417
345
  }
346
+ // Proto: WebcastLikeMessage { User user=5, WebcastMessageEvent event=1, int32 likeCount=2, int32 totalLikeCount=3 }
418
347
  case "WebcastLikeMessage": {
419
348
  const userBuf = getBytes(f, 5);
420
349
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -426,6 +355,13 @@ function parseWebcastMessage(method, payload) {
426
355
  totalLikes: getInt(f, 3)
427
356
  };
428
357
  }
358
+ // Proto: WebcastGiftMessage {
359
+ // User user=7, int32 giftId=2, int32 repeatCount=5, int32 repeatEnd=9,
360
+ // GiftDetails giftDetails=15, GiftExtra giftExtra=23
361
+ // }
362
+ // GiftDetails { GiftImage giftImage=1, string giftName=16, string describe=2,
363
+ // int32 giftType=11, int32 diamondCount=12 }
364
+ // GiftExtra { uint64 timestamp=6, uint64 toUserId=8 }
429
365
  case "WebcastGiftMessage": {
430
366
  const userBuf = getBytes(f, 7);
431
367
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -450,7 +386,7 @@ function parseWebcastMessage(method, payload) {
450
386
  const extraBuf = getBytes(f, 23);
451
387
  if (extraBuf) {
452
388
  const ef = decodeProto(extraBuf);
453
- toUserId = getIntStr(ef, 8) || "";
389
+ toUserId = String(getInt(ef, 8));
454
390
  }
455
391
  const groupId = toUserId || getStr(f, 11);
456
392
  return {
@@ -464,10 +400,10 @@ function parseWebcastMessage(method, payload) {
464
400
  repeatEnd,
465
401
  combo: repeatCount > 1 && !repeatEnd,
466
402
  giftType,
467
- groupId,
468
- giftPictureUrl: giftImageUrl
403
+ groupId
469
404
  };
470
405
  }
406
+ // Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
471
407
  case "WebcastSocialMessage": {
472
408
  const userBuf = getBytes(f, 2);
473
409
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -487,42 +423,19 @@ function parseWebcastMessage(method, payload) {
487
423
  }
488
424
  return { ...base, type: "social", user, action };
489
425
  }
426
+ // Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
490
427
  case "WebcastRoomUserSeqMessage": {
491
428
  const viewerCount = getInt(f, 3) || getInt(f, 2);
492
429
  const totalViewers = getInt(f, 1) || viewerCount;
493
430
  return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
494
431
  }
432
+ // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
433
+ // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
495
434
  case "WebcastLinkMicBattle": {
496
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
497
- const overallStatus = getInt(f, 4);
435
+ const battleId = String(getInt(f, 1) || "");
436
+ const status = getInt(f, 2) || 1;
437
+ const battleDuration = getInt(f, 3);
498
438
  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
439
  const battleUserBufs = getAllBytes(f, 10);
527
440
  for (const bub of battleUserBufs) {
528
441
  try {
@@ -543,23 +456,6 @@ function parseWebcastMessage(method, payload) {
543
456
  } catch {
544
457
  }
545
458
  }
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
459
  if (teams.length === 0) {
564
460
  const teamBufs7 = getAllBytes(f, 7);
565
461
  for (const tb of teamBufs7) {
@@ -569,48 +465,54 @@ function parseWebcastMessage(method, payload) {
569
465
  }
570
466
  }
571
467
  }
572
- return { ...base, type: "battle", battleId, status, battleDuration, teams, battleSettings };
468
+ return { ...base, type: "battle", battleId, status, battleDuration, teams };
573
469
  }
470
+ // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
471
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
472
+ // battleItems → { hostUserId=1, repeated battleGroups=2 }
473
+ // battleGroups → { repeated users=1, int32 points=2 }
574
474
  case "WebcastLinkMicArmies": {
575
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
475
+ const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
576
476
  const battleStatus = getInt(f, 7);
577
477
  const teams = [];
578
- let battleSettings;
579
- const settingsBuf18 = getBytes(f, 18);
580
- if (settingsBuf18) {
478
+ const itemBufs = getAllBytes(f, 3);
479
+ for (const ib of itemBufs) {
581
480
  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
- };
481
+ teams.push(parseBattleTeamFromArmies(ib));
591
482
  } catch {
592
483
  }
593
484
  }
594
- const scoreUpdateTime = getInt(f, 5);
595
- const giftSentTime = getInt(f, 6);
596
- const itemBufs = getAllBytes(f, 3);
597
- for (const ib of itemBufs) {
485
+ let battleStartMs = 0;
486
+ let battleDurationSeconds = 0;
487
+ const configBuf = getBytes(f, 18);
488
+ if (configBuf) {
598
489
  try {
599
- teams.push(parseBattleTeamFromArmies(ib));
490
+ const cf = decodeProto(configBuf);
491
+ battleStartMs = getInt(cf, 2);
492
+ battleDurationSeconds = getInt(cf, 3);
600
493
  } catch {
601
494
  }
602
495
  }
496
+ let endTimeMs = 0;
497
+ let timeLeftSeconds = 0;
498
+ if (battleStartMs > 0 && battleDurationSeconds > 0) {
499
+ endTimeMs = battleStartMs + battleDurationSeconds * 1e3;
500
+ const elapsedMs = Date.now() - battleStartMs;
501
+ timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
502
+ }
603
503
  return {
604
504
  ...base,
605
505
  type: "battleArmies",
606
506
  battleId,
607
507
  teams,
608
508
  status: battleStatus,
609
- battleSettings,
610
- scoreUpdateTime,
611
- giftSentTime
509
+ battleStartMs,
510
+ battleDurationSeconds,
511
+ timeLeftSeconds,
512
+ endTimeMs
612
513
  };
613
514
  }
515
+ // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
614
516
  case "WebcastSubNotifyMessage": {
615
517
  const userBuf = getBytes(f, 2);
616
518
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -636,6 +538,8 @@ function parseWebcastMessage(method, payload) {
636
538
  const envelopeId = String(getInt(f, 1) || getStr(f, 1));
637
539
  return { ...base, type: "envelope", envelopeId, diamondCount: getInt(f, 3) };
638
540
  }
541
+ // Proto: WebcastQuestionNewMessage { MessageType type=1, QuestionDetails questionDetails=2 }
542
+ // QuestionDetails { string questionText=2, User user=5 }
639
543
  case "WebcastQuestionNewMessage": {
640
544
  let questionText = "", user = { id: "0", nickname: "", uniqueId: "" };
641
545
  const detailBuf = getBytes(f, 2);
@@ -665,17 +569,20 @@ function parseWebcastMessage(method, payload) {
665
569
  }
666
570
  return { ...base, type: "rankUpdate", rankType, rankList };
667
571
  }
572
+ // Proto: WebcastControlMessage { int32 action=2 }
668
573
  case "WebcastControlMessage":
669
574
  return { ...base, type: "control", action: getInt(f, 2) || getInt(f, 1) };
670
575
  case "WebcastRoomMessage":
671
576
  case "RoomMessage":
672
577
  return { ...base, type: "room", status: getInt(f, 2) };
578
+ // Proto: WebcastLiveIntroMessage { uint64 id=2, string description=4, User user=5 }
673
579
  case "WebcastLiveIntroMessage": {
674
580
  const roomId = String(getInt(f, 2));
675
581
  const title = getStr(f, 4) || getStr(f, 2);
676
582
  return { ...base, type: "liveIntro", roomId, title };
677
583
  }
678
- case "WebcastLinkMicMethod": {
584
+ case "WebcastLinkMicMethod":
585
+ case "WebcastLinkmicBattleTaskMessage": {
679
586
  const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
680
587
  const users = [];
681
588
  const userBufs = getAllBytes(f, 2);
@@ -687,152 +594,6 @@ function parseWebcastMessage(method, payload) {
687
594
  }
688
595
  return { ...base, type: "linkMic", action, users };
689
596
  }
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
597
  default:
837
598
  return { ...base, type: "unknown", method };
838
599
  }
@@ -857,12 +618,10 @@ function parseWebcastResponse(payload) {
857
618
  // src/client.ts
858
619
  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
620
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
860
- function httpGet(url, headers, agent) {
621
+ function httpGet(url, headers) {
861
622
  return new Promise((resolve, reject) => {
862
623
  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) => {
624
+ const req = mod.get(url, { headers }, (res) => {
866
625
  const chunks = [];
867
626
  const enc = res.headers["content-encoding"];
868
627
  const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
@@ -888,15 +647,21 @@ function getWsHost(clusterRegion) {
888
647
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
889
648
  return "webcast-ws.tiktok.com";
890
649
  }
891
- var TikTokLive = class extends import_events.EventEmitter {
650
+ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
892
651
  ws = null;
893
652
  heartbeatTimer = null;
894
653
  reconnectAttempts = 0;
895
654
  intentionalClose = false;
896
655
  _connected = false;
656
+ _destroyed = false;
897
657
  _eventCount = 0;
898
658
  _roomId = "";
899
- _ownerUserId = "";
659
+ // Cache host identities from battle events for enriching battleArmies
660
+ // Bounded to MAX_BATTLE_HOSTS entries to prevent unbounded memory growth
661
+ static MAX_BATTLE_HOSTS = 50;
662
+ _battleHosts = /* @__PURE__ */ new Map();
663
+ // Track in-flight host resolution requests to avoid duplicate API calls
664
+ _pendingHostResolves = /* @__PURE__ */ new Set();
900
665
  uniqueId;
901
666
  signServerUrl;
902
667
  apiKey;
@@ -904,13 +669,11 @@ var TikTokLive = class extends import_events.EventEmitter {
904
669
  maxReconnectAttempts;
905
670
  heartbeatInterval;
906
671
  debug;
907
- _sessionId;
908
- _ttTargetIdc;
909
- proxyAgent;
910
672
  _presetRoomId;
911
- _presetTtwid;
673
+ _presetSessionId;
912
674
  constructor(options) {
913
675
  super();
676
+ this.setMaxListeners(20);
914
677
  this.uniqueId = options.uniqueId.replace(/^@/, "");
915
678
  this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
916
679
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
@@ -919,56 +682,148 @@ var TikTokLive = class extends import_events.EventEmitter {
919
682
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
920
683
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
921
684
  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;
685
+ this._presetRoomId = options.roomId || "";
686
+ this._presetSessionId = options.sessionId || "";
929
687
  }
930
688
  async connect() {
689
+ if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
931
690
  this.intentionalClose = false;
932
- let ttwid = this._presetTtwid || "";
933
- let roomId = this._presetRoomId || "";
934
- let ownerUserId = "";
691
+ let ttwid = this._presetSessionId;
692
+ let roomId = this._presetRoomId;
935
693
  let clusterRegion = "";
936
- if (!roomId || !ttwid) {
937
- const targetUrl = `https://www.tiktok.com/@${this.uniqueId}/live`;
938
- const resp = await httpGet(targetUrl, {
939
- "User-Agent": DEFAULT_UA,
940
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
941
- "Accept-Encoding": "gzip, deflate, br",
942
- "Accept-Language": "en-US,en;q=0.9"
943
- }, this.proxyAgent);
944
- for (const sc of [resp.headers["set-cookie"] || []].flat()) {
945
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
946
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
947
- break;
694
+ let preSignedUrl = "";
695
+ let preSignedCookies = "";
696
+ let preSignedUA = "";
697
+ if (roomId && ttwid) {
698
+ console.log(`[TikTokLive] Using preset roomId=${roomId} + sessionId (skip TikTok fetch)`);
699
+ } else if (roomId && !ttwid) {
700
+ console.log(`[TikTokLive] Have roomId=${roomId}, fetching ttwid from sign server...`);
701
+ try {
702
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
703
+ method: "POST",
704
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
705
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
706
+ });
707
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
708
+ if (credResp.ok) {
709
+ const credData = await credResp.json();
710
+ if (credData.status_code === 0 && credData.data?.signed_url) {
711
+ preSignedUrl = credData.data.signed_url;
712
+ preSignedCookies = credData.data.cookies || "";
713
+ preSignedUA = credData.data.user_agent || DEFAULT_UA;
714
+ ttwid = "__presigned__";
715
+ console.log(`[TikTokLive] \u2705 Got pre-signed URL from sign server`);
716
+ } else {
717
+ console.log(`[TikTokLive] \u26A0\uFE0F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
718
+ }
948
719
  }
720
+ } catch (e) {
721
+ console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
949
722
  }
950
- if (!ttwid) throw new Error("Failed to obtain session cookie");
951
- const html = resp.body.toString();
952
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
953
- if (sigiMatch) {
723
+ if (!ttwid) {
724
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
954
725
  try {
955
- const json = JSON.parse(sigiMatch[1]);
956
- const jsonStr = JSON.stringify(json);
957
- const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
958
- if (m) roomId = m[1];
959
- const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
960
- if (ownerUser?.id) {
961
- ownerUserId = String(ownerUser.id);
726
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
727
+ "User-Agent": DEFAULT_UA,
728
+ "Accept": "text/html"
729
+ });
730
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
731
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
732
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
733
+ break;
734
+ }
962
735
  }
963
- } catch {
736
+ console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\u2705" : "\u274C none"}`);
737
+ } catch (e) {
738
+ console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
739
+ }
740
+ }
741
+ } else {
742
+ console.log(`[TikTokLive] No presets for @${this.uniqueId}. Resolving via sign server (${this.signServerUrl})...`);
743
+ try {
744
+ const roomResp = await fetch(`${this.signServerUrl}/webcast/room_id`, {
745
+ method: "POST",
746
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
747
+ body: JSON.stringify({ unique_id: this.uniqueId })
748
+ });
749
+ console.log(`[TikTokLive] room_id response: ${roomResp.status}`);
750
+ if (roomResp.ok) {
751
+ const roomData = await roomResp.json();
752
+ if (roomData.status_code === 0 && roomData.data?.room_id) {
753
+ roomId = roomData.data.room_id;
754
+ console.log(`[TikTokLive] \u2705 Got roomId from sign server: ${roomId}`);
755
+ } else {
756
+ console.log(`[TikTokLive] \u26A0\uFE0F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
757
+ }
758
+ }
759
+ } catch (e) {
760
+ console.log(`[TikTokLive] \u274C room_id request failed: ${e.message}`);
761
+ }
762
+ if (roomId) {
763
+ try {
764
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
767
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
768
+ });
769
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
770
+ if (credResp.ok) {
771
+ const credData = await credResp.json();
772
+ if (credData.data?.cookies) {
773
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
774
+ if (m) {
775
+ ttwid = m[1];
776
+ console.log(`[TikTokLive] \u2705 Got ttwid from sign server`);
777
+ } else console.log(`[TikTokLive] \u26A0\uFE0F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
778
+ } else {
779
+ console.log(`[TikTokLive] \u26A0\uFE0F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
780
+ }
781
+ }
782
+ } catch (e) {
783
+ console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
784
+ }
785
+ }
786
+ if (!roomId || !ttwid) {
787
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch (roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"})...`);
788
+ try {
789
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
790
+ "User-Agent": DEFAULT_UA,
791
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
792
+ "Accept-Encoding": "gzip, deflate, br",
793
+ "Accept-Language": "en-US,en;q=0.9"
794
+ });
795
+ if (!ttwid) {
796
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
797
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
798
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
799
+ break;
800
+ }
801
+ }
802
+ }
803
+ if (!roomId) {
804
+ const html = resp.body.toString();
805
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
806
+ if (sigiMatch) {
807
+ try {
808
+ const json = JSON.parse(sigiMatch[1]);
809
+ const jsonStr = JSON.stringify(json);
810
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
811
+ if (m) roomId = m[1];
812
+ } catch {
813
+ }
814
+ }
815
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
816
+ clusterRegion = crMatch ? crMatch[1] : "";
817
+ }
818
+ console.log(`[TikTokLive] Direct fetch result: roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"}`);
819
+ } catch (e) {
820
+ console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
964
821
  }
965
822
  }
966
- if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
967
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
968
- clusterRegion = crMatch ? crMatch[1] : "";
969
823
  }
824
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
825
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
970
826
  this._roomId = roomId;
971
- this._ownerUserId = ownerUserId;
972
827
  const wsHost = getWsHost(clusterRegion);
973
828
  const wsParams = new URLSearchParams({
974
829
  version_code: "270000",
@@ -1022,21 +877,11 @@ var TikTokLive = class extends import_events.EventEmitter {
1022
877
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
1023
878
  }
1024
879
  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
- this.ws = new import_ws.default(wsUrl, {
1034
- headers: {
1035
- "User-Agent": DEFAULT_UA,
1036
- "Cookie": cookieHeader,
1037
- "Origin": "https://www.tiktok.com"
1038
- },
1039
- agent: this.proxyAgent
880
+ const finalWsUrl = preSignedUrl ? preSignedUrl.replace(/^https:\/\//, "wss://") : wsUrl;
881
+ const finalHeaders = preSignedUrl ? { "User-Agent": preSignedUA, "Cookie": preSignedCookies, "Origin": "https://www.tiktok.com" } : { "User-Agent": DEFAULT_UA, "Cookie": `ttwid=${ttwid}`, "Origin": "https://www.tiktok.com" };
882
+ console.log(`[TikTokLive] Connecting WS: ${preSignedUrl ? "pre-signed URL" : "self-built URL"}`);
883
+ this.ws = new import_ws.default(finalWsUrl, {
884
+ headers: finalHeaders
1040
885
  });
1041
886
  this.ws.on("open", () => {
1042
887
  this._connected = true;
@@ -1048,8 +893,7 @@ var TikTokLive = class extends import_events.EventEmitter {
1048
893
  roomId,
1049
894
  wsHost,
1050
895
  clusterRegion,
1051
- connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1052
- ownerUserId: ownerUserId || void 0
896
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
1053
897
  };
1054
898
  this.emit("connected");
1055
899
  this.emit("roomInfo", roomInfo);
@@ -1079,42 +923,76 @@ var TikTokLive = class extends import_events.EventEmitter {
1079
923
  this.intentionalClose = true;
1080
924
  this.stopHeartbeat();
1081
925
  if (this.ws) {
926
+ this.ws.removeAllListeners();
1082
927
  this.ws.close(1e3);
1083
928
  this.ws = null;
1084
929
  }
1085
930
  this._connected = false;
1086
931
  }
932
+ /**
933
+ * Fully destroy the client, releasing all resources and listeners.
934
+ * After calling destroy(), the instance cannot be reused — create a new one.
935
+ */
936
+ destroy() {
937
+ this.disconnect();
938
+ this._destroyed = true;
939
+ this._battleHosts.clear();
940
+ this._eventCount = 0;
941
+ this._roomId = "";
942
+ this.removeAllListeners();
943
+ }
1087
944
  get connected() {
1088
945
  return this._connected;
1089
946
  }
947
+ get destroyed() {
948
+ return this._destroyed;
949
+ }
1090
950
  get eventCount() {
1091
951
  return this._eventCount;
1092
952
  }
1093
953
  get roomId() {
1094
954
  return this._roomId;
1095
955
  }
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
956
  /**
1106
- * Build a cookie header string for authenticated API requests (e.g. ranklist).
1107
- * Returns undefined if no session is set.
957
+ * Get live stream video URLs for a TikTok user.
958
+ * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
959
+ * This is a standalone method — no WebSocket connection required.
960
+ *
961
+ * @example
962
+ * ```ts
963
+ * const stream = await TikTokLive.getStreamUrl({
964
+ * uniqueId: 'username',
965
+ * apiKey: 'your-api-key',
966
+ * });
967
+ * if (stream.alive) {
968
+ * console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
969
+ * console.log(stream.flvPullUrl); // Best FLV URL
970
+ * }
971
+ * ```
1108
972
  */
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("; ");
973
+ static async getStreamUrl(options) {
974
+ const serverUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
975
+ const resp = await fetch(`${serverUrl}/webcast/room_video`, {
976
+ method: "POST",
977
+ headers: {
978
+ "Content-Type": "application/json",
979
+ "x-api-key": options.apiKey
980
+ },
981
+ body: JSON.stringify({ unique_id: options.uniqueId.replace(/^@/, "") })
982
+ });
983
+ const data = await resp.json();
984
+ if (data.status_code !== 0) {
985
+ throw new Error(data.error || "Failed to get stream URL");
986
+ }
987
+ const d = data.data;
988
+ return {
989
+ roomId: d.room_id,
990
+ alive: d.alive ?? false,
991
+ streamUrls: d.stream_urls || {},
992
+ defaultQuality: d.default_quality || "",
993
+ flvPullUrl: d.flv_pull_url || void 0,
994
+ hlsPullUrl: d.hls_pull_url || void 0
995
+ };
1118
996
  }
1119
997
  on(event, listener) {
1120
998
  return super.on(event, listener);
@@ -1149,567 +1027,120 @@ var TikTokLive = class extends import_events.EventEmitter {
1149
1027
  const events = parseWebcastResponse(inner);
1150
1028
  for (const evt of events) {
1151
1029
  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);
1030
+ if (evt.type === "battle") {
1031
+ for (const team of evt.teams) {
1032
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
1033
+ if (host) {
1034
+ this._battleHosts.set(team.hostUserId, host.user);
1035
+ team.hostUser = host.user;
1036
+ }
1037
+ }
1038
+ if (this._battleHosts.size > _TikTokLive.MAX_BATTLE_HOSTS) {
1039
+ const excess = this._battleHosts.size - _TikTokLive.MAX_BATTLE_HOSTS;
1040
+ const keys = [...this._battleHosts.keys()].slice(0, excess);
1041
+ for (const key of keys) this._battleHosts.delete(key);
1245
1042
  }
1246
1043
  }
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);
1044
+ if (evt.type === "battleArmies") {
1045
+ const unresolvedIds = [];
1046
+ for (const team of evt.teams) {
1047
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
1048
+ if (host) {
1049
+ team.hostUser = host.user;
1050
+ this._battleHosts.set(team.hostUserId, host.user);
1051
+ } else {
1052
+ const cached = this._battleHosts.get(team.hostUserId);
1053
+ if (cached) {
1054
+ team.hostUser = cached;
1055
+ } else if (team.hostUserId && team.hostUserId !== "0") {
1056
+ unresolvedIds.push(team.hostUserId);
1057
+ }
1058
+ }
1059
+ }
1060
+ if (unresolvedIds.length > 0) {
1061
+ this.resolveHostUsers(unresolvedIds, evt).catch(() => {
1062
+ });
1063
+ continue;
1064
+ }
1499
1065
  }
1066
+ this.emit("event", evt);
1067
+ this.emit(evt.type, evt);
1068
+ }
1500
1069
  }
1501
1070
  } catch {
1502
- if (this.debug) console.error("[Captions] Failed to parse message:", raw);
1503
1071
  }
1504
1072
  }
1505
1073
  /**
1506
- * Connect to the TikTok FLV stream and extract audio.
1507
- * Sends binary audio buffers to the server via WebSocket.
1074
+ * Resolve unknown host user IDs via the sign server API.
1075
+ * Caches results and re-emits the battleArmies event with enriched hostUser data.
1508
1076
  */
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" });
1077
+ async resolveHostUsers(userIds, originalEvt) {
1078
+ const toResolve = userIds.filter((id) => !this._pendingHostResolves.has(id));
1079
+ if (toResolve.length === 0) {
1080
+ this.emit("event", originalEvt);
1081
+ this.emit(originalEvt.type, originalEvt);
1513
1082
  return;
1514
1083
  }
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
- });
1084
+ for (const id of toResolve) this._pendingHostResolves.add(id);
1535
1085
  try {
1536
- if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
1537
- const resp = await fetch(url, {
1538
- signal: this.streamAbortController.signal,
1086
+ const url = `${this.signServerUrl}/webcast/resolve_user_ids`;
1087
+ const res = await fetch(url, {
1088
+ method: "POST",
1539
1089
  headers: {
1540
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1541
- }
1090
+ "Content-Type": "application/json",
1091
+ "x-api-key": this.apiKey
1092
+ },
1093
+ body: JSON.stringify({ user_ids: toResolve }),
1094
+ signal: AbortSignal.timeout(5e3)
1542
1095
  });
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`);
1096
+ if (res.ok) {
1097
+ const json = await res.json();
1098
+ if (json.data) {
1099
+ for (const [uid, info] of Object.entries(json.data)) {
1100
+ if (info.username) {
1101
+ const hostUser = {
1102
+ id: uid,
1103
+ uniqueId: info.username,
1104
+ nickname: info.nickname || info.username,
1105
+ profilePicture: info.avatarUrl || void 0
1106
+ };
1107
+ this._battleHosts.set(uid, hostUser);
1574
1108
  }
1575
1109
  }
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);
1588
- }
1589
- if (this.debug && chunks <= 3) {
1590
- if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
1110
+ if (originalEvt.type === "battleArmies") {
1111
+ for (const team of originalEvt.teams) {
1112
+ if (!team.hostUser) {
1113
+ const cached = this._battleHosts.get(team.hostUserId);
1114
+ if (cached) team.hostUser = cached;
1591
1115
  }
1592
1116
  }
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
- }
1601
1117
  }
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 });
1118
+ }
1611
1119
  }
1120
+ } catch {
1121
+ } finally {
1122
+ for (const id of toResolve) this._pendingHostResolves.delete(id);
1123
+ this.emit("event", originalEvt);
1124
+ this.emit(originalEvt.type, originalEvt);
1612
1125
  }
1613
1126
  }
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;
1127
+ startHeartbeat(roomId) {
1128
+ this.stopHeartbeat();
1129
+ this.heartbeatTimer = setInterval(() => {
1130
+ if (this.ws?.readyState === import_ws.default.OPEN) {
1131
+ this.ws.send(buildHeartbeat(roomId));
1132
+ }
1133
+ }, this.heartbeatInterval);
1697
1134
  }
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;
1135
+ stopHeartbeat() {
1136
+ if (this.heartbeatTimer) {
1137
+ clearInterval(this.heartbeatTimer);
1138
+ this.heartbeatTimer = null;
1139
+ }
1704
1140
  }
1705
- }
1141
+ };
1706
1142
  // Annotate the CommonJS export names for ESM import in node:
1707
1143
  0 && (module.exports = {
1708
- TikTokCaptions,
1709
- TikTokLive,
1710
- fetchFeed,
1711
- getLiveFeed,
1712
- getRanklist,
1713
- getRegionalRanklist
1144
+ TikTokLive
1714
1145
  });
1715
1146
  //# sourceMappingURL=index.js.map