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