@tiktool/live 2.6.6 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 {
@@ -297,29 +230,15 @@ function parseBattleTeamFromArmies(itemBuf) {
297
230
  const userBufs = getAllBytes(gf, 1);
298
231
  for (const ub of userBufs) {
299
232
  try {
300
- const userFields = decodeProto(ub);
301
- const individualScore = getInt(userFields, 2);
302
233
  const user = parseUser(ub);
303
- users.push({ user, score: individualScore });
234
+ users.push({ user, score: points });
304
235
  } catch {
305
236
  }
306
237
  }
307
238
  } catch {
308
239
  }
309
240
  }
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 };
241
+ return { hostUserId, score: teamScore, users };
323
242
  }
324
243
  function parseWebcastMessage(method, payload) {
325
244
  const f = decodeProto(payload);
@@ -334,29 +253,42 @@ function parseWebcastMessage(method, payload) {
334
253
  }
335
254
  }
336
255
  switch (method) {
256
+ // Proto: WebcastChatMessage { MessageType type=1, User user=2, string comment=3, ..., repeated WebcastSubEmote emotes=12 }
257
+ // WebcastSubEmote { int32 placeInComment=1, EmoteDetails emote=2 }
258
+ // EmoteDetails { string emoteId=1, EmoteImage image=2 }
259
+ // EmoteImage { string imageUrl=1 }
337
260
  case "WebcastChatMessage": {
338
261
  const userBuf = getBytes(f, 2);
339
262
  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;
263
+ const comment = getStr(f, 3);
264
+ const emotes = [];
265
+ const emoteBufs12 = getAllBytes(f, 12);
266
+ const emoteBufs13 = getAllBytes(f, 13);
267
+ const emoteBufs = emoteBufs13.length > 0 ? emoteBufs13 : emoteBufs12;
268
+ for (const eb of emoteBufs) {
269
+ try {
270
+ const ef = decodeProto(eb);
271
+ const placeInComment = getInt(ef, 1);
272
+ const detailBuf = getBytes(ef, 2);
273
+ if (detailBuf) {
274
+ const df = decodeProto(detailBuf);
275
+ const emoteId = getStr(df, 1);
276
+ let imageUrl = "";
277
+ const imgBuf = getBytes(df, 2);
278
+ if (imgBuf) {
279
+ const imgf = decodeProto(imgBuf);
280
+ imageUrl = getStr(imgf, 1);
281
+ }
282
+ if (emoteId) {
283
+ emotes.push({ emoteId, imageUrl, placeInComment });
352
284
  }
353
- } catch {
354
285
  }
286
+ } catch {
355
287
  }
356
288
  }
357
- const comment = replaceEmotes(rawComment, emoteImages);
358
- return { ...base, type: "chat", user, comment };
289
+ return { ...base, type: "chat", user, comment, emotes };
359
290
  }
291
+ // Proto: WebcastMemberMessage { User user=2, WebcastMessageEvent event=1 }
360
292
  case "WebcastMemberMessage": {
361
293
  const userBuf = getBytes(f, 2);
362
294
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -374,6 +306,7 @@ function parseWebcastMessage(method, payload) {
374
306
  }
375
307
  return { ...base, type: "member", user, action };
376
308
  }
309
+ // Proto: WebcastLikeMessage { User user=5, WebcastMessageEvent event=1, int32 likeCount=2, int32 totalLikeCount=3 }
377
310
  case "WebcastLikeMessage": {
378
311
  const userBuf = getBytes(f, 5);
379
312
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -385,6 +318,13 @@ function parseWebcastMessage(method, payload) {
385
318
  totalLikes: getInt(f, 3)
386
319
  };
387
320
  }
321
+ // Proto: WebcastGiftMessage {
322
+ // User user=7, int32 giftId=2, int32 repeatCount=5, int32 repeatEnd=9,
323
+ // GiftDetails giftDetails=15, GiftExtra giftExtra=23
324
+ // }
325
+ // GiftDetails { GiftImage giftImage=1, string giftName=16, string describe=2,
326
+ // int32 giftType=11, int32 diamondCount=12 }
327
+ // GiftExtra { uint64 timestamp=6, uint64 toUserId=8 }
388
328
  case "WebcastGiftMessage": {
389
329
  const userBuf = getBytes(f, 7);
390
330
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -409,7 +349,7 @@ function parseWebcastMessage(method, payload) {
409
349
  const extraBuf = getBytes(f, 23);
410
350
  if (extraBuf) {
411
351
  const ef = decodeProto(extraBuf);
412
- toUserId = getIntStr(ef, 8) || "";
352
+ toUserId = String(getInt(ef, 8));
413
353
  }
414
354
  const groupId = toUserId || getStr(f, 11);
415
355
  return {
@@ -423,10 +363,10 @@ function parseWebcastMessage(method, payload) {
423
363
  repeatEnd,
424
364
  combo: repeatCount > 1 && !repeatEnd,
425
365
  giftType,
426
- groupId,
427
- giftPictureUrl: giftImageUrl
366
+ groupId
428
367
  };
429
368
  }
369
+ // Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
430
370
  case "WebcastSocialMessage": {
431
371
  const userBuf = getBytes(f, 2);
432
372
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -446,42 +386,19 @@ function parseWebcastMessage(method, payload) {
446
386
  }
447
387
  return { ...base, type: "social", user, action };
448
388
  }
389
+ // Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
449
390
  case "WebcastRoomUserSeqMessage": {
450
391
  const viewerCount = getInt(f, 3) || getInt(f, 2);
451
392
  const totalViewers = getInt(f, 1) || viewerCount;
452
393
  return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
453
394
  }
395
+ // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
396
+ // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
454
397
  case "WebcastLinkMicBattle": {
455
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
456
- const overallStatus = getInt(f, 4);
398
+ const battleId = String(getInt(f, 1) || "");
399
+ const status = getInt(f, 2) || 1;
400
+ const battleDuration = getInt(f, 3);
457
401
  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
402
  const battleUserBufs = getAllBytes(f, 10);
486
403
  for (const bub of battleUserBufs) {
487
404
  try {
@@ -502,23 +419,6 @@ function parseWebcastMessage(method, payload) {
502
419
  } catch {
503
420
  }
504
421
  }
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
422
  if (teams.length === 0) {
523
423
  const teamBufs7 = getAllBytes(f, 7);
524
424
  for (const tb of teamBufs7) {
@@ -528,48 +428,51 @@ function parseWebcastMessage(method, payload) {
528
428
  }
529
429
  }
530
430
  }
531
- return { ...base, type: "battle", battleId, status, battleDuration, teams, battleSettings };
431
+ return { ...base, type: "battle", battleId, status, battleDuration, teams };
532
432
  }
433
+ // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
434
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
435
+ // battleItems → { hostUserId=1, repeated battleGroups=2 }
436
+ // battleGroups → { repeated users=1, int32 points=2 }
533
437
  case "WebcastLinkMicArmies": {
534
- const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
438
+ const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
535
439
  const battleStatus = getInt(f, 7);
536
440
  const teams = [];
537
- let battleSettings;
538
- const settingsBuf18 = getBytes(f, 18);
539
- if (settingsBuf18) {
441
+ const itemBufs = getAllBytes(f, 3);
442
+ for (const ib of itemBufs) {
540
443
  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
- };
444
+ teams.push(parseBattleTeamFromArmies(ib));
550
445
  } catch {
551
446
  }
552
447
  }
553
- const scoreUpdateTime = getInt(f, 5);
554
- const giftSentTime = getInt(f, 6);
555
- const itemBufs = getAllBytes(f, 3);
556
- for (const ib of itemBufs) {
448
+ let battleStartMs = 0;
449
+ let battleDurationSeconds = 0;
450
+ const configBuf = getBytes(f, 18);
451
+ if (configBuf) {
557
452
  try {
558
- teams.push(parseBattleTeamFromArmies(ib));
453
+ const cf = decodeProto(configBuf);
454
+ battleStartMs = getInt(cf, 2);
455
+ battleDurationSeconds = getInt(cf, 3);
559
456
  } catch {
560
457
  }
561
458
  }
459
+ let timeLeftSeconds = 0;
460
+ if (battleStartMs > 0 && battleDurationSeconds > 0) {
461
+ const elapsedMs = Date.now() - battleStartMs;
462
+ timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
463
+ }
562
464
  return {
563
465
  ...base,
564
466
  type: "battleArmies",
565
467
  battleId,
566
468
  teams,
567
469
  status: battleStatus,
568
- battleSettings,
569
- scoreUpdateTime,
570
- giftSentTime
470
+ battleStartMs,
471
+ battleDurationSeconds,
472
+ timeLeftSeconds
571
473
  };
572
474
  }
475
+ // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
573
476
  case "WebcastSubNotifyMessage": {
574
477
  const userBuf = getBytes(f, 2);
575
478
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
@@ -595,6 +498,8 @@ function parseWebcastMessage(method, payload) {
595
498
  const envelopeId = String(getInt(f, 1) || getStr(f, 1));
596
499
  return { ...base, type: "envelope", envelopeId, diamondCount: getInt(f, 3) };
597
500
  }
501
+ // Proto: WebcastQuestionNewMessage { MessageType type=1, QuestionDetails questionDetails=2 }
502
+ // QuestionDetails { string questionText=2, User user=5 }
598
503
  case "WebcastQuestionNewMessage": {
599
504
  let questionText = "", user = { id: "0", nickname: "", uniqueId: "" };
600
505
  const detailBuf = getBytes(f, 2);
@@ -624,17 +529,20 @@ function parseWebcastMessage(method, payload) {
624
529
  }
625
530
  return { ...base, type: "rankUpdate", rankType, rankList };
626
531
  }
532
+ // Proto: WebcastControlMessage { int32 action=2 }
627
533
  case "WebcastControlMessage":
628
534
  return { ...base, type: "control", action: getInt(f, 2) || getInt(f, 1) };
629
535
  case "WebcastRoomMessage":
630
536
  case "RoomMessage":
631
537
  return { ...base, type: "room", status: getInt(f, 2) };
538
+ // Proto: WebcastLiveIntroMessage { uint64 id=2, string description=4, User user=5 }
632
539
  case "WebcastLiveIntroMessage": {
633
540
  const roomId = String(getInt(f, 2));
634
541
  const title = getStr(f, 4) || getStr(f, 2);
635
542
  return { ...base, type: "liveIntro", roomId, title };
636
543
  }
637
- case "WebcastLinkMicMethod": {
544
+ case "WebcastLinkMicMethod":
545
+ case "WebcastLinkmicBattleTaskMessage": {
638
546
  const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
639
547
  const users = [];
640
548
  const userBufs = getAllBytes(f, 2);
@@ -646,152 +554,6 @@ function parseWebcastMessage(method, payload) {
646
554
  }
647
555
  return { ...base, type: "linkMic", action, users };
648
556
  }
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
557
  default:
796
558
  return { ...base, type: "unknown", method };
797
559
  }
@@ -816,12 +578,10 @@ function parseWebcastResponse(payload) {
816
578
  // src/client.ts
817
579
  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
580
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
819
- function httpGet(url, headers, agent) {
581
+ function httpGet(url, headers) {
820
582
  return new Promise((resolve, reject) => {
821
583
  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) => {
584
+ const req = mod.get(url, { headers }, (res) => {
825
585
  const chunks = [];
826
586
  const enc = res.headers["content-encoding"];
827
587
  const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
@@ -847,15 +607,21 @@ function getWsHost(clusterRegion) {
847
607
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
848
608
  return "webcast-ws.tiktok.com";
849
609
  }
850
- var TikTokLive = class extends EventEmitter {
610
+ var TikTokLive = class _TikTokLive extends EventEmitter {
851
611
  ws = null;
852
612
  heartbeatTimer = null;
853
613
  reconnectAttempts = 0;
854
614
  intentionalClose = false;
855
615
  _connected = false;
616
+ _destroyed = false;
856
617
  _eventCount = 0;
857
618
  _roomId = "";
858
- _ownerUserId = "";
619
+ // Cache host identities from battle events for enriching battleArmies
620
+ // Bounded to MAX_BATTLE_HOSTS entries to prevent unbounded memory growth
621
+ static MAX_BATTLE_HOSTS = 50;
622
+ _battleHosts = /* @__PURE__ */ new Map();
623
+ // Track in-flight host resolution requests to avoid duplicate API calls
624
+ _pendingHostResolves = /* @__PURE__ */ new Set();
859
625
  uniqueId;
860
626
  signServerUrl;
861
627
  apiKey;
@@ -863,13 +629,11 @@ var TikTokLive = class extends EventEmitter {
863
629
  maxReconnectAttempts;
864
630
  heartbeatInterval;
865
631
  debug;
866
- _sessionId;
867
- _ttTargetIdc;
868
- proxyAgent;
869
632
  _presetRoomId;
870
- _presetTtwid;
633
+ _presetSessionId;
871
634
  constructor(options) {
872
635
  super();
636
+ this.setMaxListeners(20);
873
637
  this.uniqueId = options.uniqueId.replace(/^@/, "");
874
638
  this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
875
639
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
@@ -878,35 +642,60 @@ var TikTokLive = class extends EventEmitter {
878
642
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
879
643
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
880
644
  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;
645
+ this._presetRoomId = options.roomId || "";
646
+ this._presetSessionId = options.sessionId || "";
888
647
  }
889
648
  async connect() {
649
+ if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
890
650
  this.intentionalClose = false;
891
- let ttwid = this._presetTtwid || "";
892
- let roomId = this._presetRoomId || "";
893
- let ownerUserId = "";
651
+ let ttwid = this._presetSessionId;
652
+ let roomId = this._presetRoomId;
894
653
  let clusterRegion = "";
895
- if (!roomId || !ttwid) {
896
- const targetUrl = `https://www.tiktok.com/@${this.uniqueId}/live`;
897
- const resp = await httpGet(targetUrl, {
654
+ if (roomId && ttwid) {
655
+ } else if (roomId && !ttwid) {
656
+ try {
657
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
658
+ method: "POST",
659
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
660
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
661
+ });
662
+ if (credResp.ok) {
663
+ const credData = await credResp.json();
664
+ if (credData.data?.cookies) {
665
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
666
+ if (m) ttwid = m[1];
667
+ }
668
+ }
669
+ } catch {
670
+ }
671
+ if (!ttwid) {
672
+ try {
673
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
674
+ "User-Agent": DEFAULT_UA,
675
+ "Accept": "text/html"
676
+ });
677
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
678
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
679
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
680
+ break;
681
+ }
682
+ }
683
+ } catch {
684
+ }
685
+ }
686
+ } else {
687
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
898
688
  "User-Agent": DEFAULT_UA,
899
689
  "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
900
690
  "Accept-Encoding": "gzip, deflate, br",
901
691
  "Accept-Language": "en-US,en;q=0.9"
902
- }, this.proxyAgent);
692
+ });
903
693
  for (const sc of [resp.headers["set-cookie"] || []].flat()) {
904
694
  if (typeof sc === "string" && sc.startsWith("ttwid=")) {
905
695
  ttwid = sc.split(";")[0].split("=").slice(1).join("=");
906
696
  break;
907
697
  }
908
698
  }
909
- if (!ttwid) throw new Error("Failed to obtain session cookie");
910
699
  const html = resp.body.toString();
911
700
  const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
912
701
  if (sigiMatch) {
@@ -915,19 +704,15 @@ var TikTokLive = class extends EventEmitter {
915
704
  const jsonStr = JSON.stringify(json);
916
705
  const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
917
706
  if (m) roomId = m[1];
918
- const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
919
- if (ownerUser?.id) {
920
- ownerUserId = String(ownerUser.id);
921
- }
922
707
  } catch {
923
708
  }
924
709
  }
925
- if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
926
710
  const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
927
711
  clusterRegion = crMatch ? crMatch[1] : "";
928
712
  }
713
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
714
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
929
715
  this._roomId = roomId;
930
- this._ownerUserId = ownerUserId;
931
716
  const wsHost = getWsHost(clusterRegion);
932
717
  const wsParams = new URLSearchParams({
933
718
  version_code: "270000",
@@ -981,21 +766,12 @@ var TikTokLive = class extends EventEmitter {
981
766
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
982
767
  }
983
768
  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
769
  this.ws = new WebSocket(wsUrl, {
993
770
  headers: {
994
771
  "User-Agent": DEFAULT_UA,
995
- "Cookie": cookieHeader,
772
+ "Cookie": `ttwid=${ttwid}`,
996
773
  "Origin": "https://www.tiktok.com"
997
- },
998
- agent: this.proxyAgent
774
+ }
999
775
  });
1000
776
  this.ws.on("open", () => {
1001
777
  this._connected = true;
@@ -1007,8 +783,7 @@ var TikTokLive = class extends EventEmitter {
1007
783
  roomId,
1008
784
  wsHost,
1009
785
  clusterRegion,
1010
- connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1011
- ownerUserId: ownerUserId || void 0
786
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
1012
787
  };
1013
788
  this.emit("connected");
1014
789
  this.emit("roomInfo", roomInfo);
@@ -1038,42 +813,76 @@ var TikTokLive = class extends EventEmitter {
1038
813
  this.intentionalClose = true;
1039
814
  this.stopHeartbeat();
1040
815
  if (this.ws) {
816
+ this.ws.removeAllListeners();
1041
817
  this.ws.close(1e3);
1042
818
  this.ws = null;
1043
819
  }
1044
820
  this._connected = false;
1045
821
  }
822
+ /**
823
+ * Fully destroy the client, releasing all resources and listeners.
824
+ * After calling destroy(), the instance cannot be reused — create a new one.
825
+ */
826
+ destroy() {
827
+ this.disconnect();
828
+ this._destroyed = true;
829
+ this._battleHosts.clear();
830
+ this._eventCount = 0;
831
+ this._roomId = "";
832
+ this.removeAllListeners();
833
+ }
1046
834
  get connected() {
1047
835
  return this._connected;
1048
836
  }
837
+ get destroyed() {
838
+ return this._destroyed;
839
+ }
1049
840
  get eventCount() {
1050
841
  return this._eventCount;
1051
842
  }
1052
843
  get roomId() {
1053
844
  return this._roomId;
1054
845
  }
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
846
  /**
1065
- * Build a cookie header string for authenticated API requests (e.g. ranklist).
1066
- * Returns undefined if no session is set.
847
+ * Get live stream video URLs for a TikTok user.
848
+ * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
849
+ * This is a standalone method — no WebSocket connection required.
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * const stream = await TikTokLive.getStreamUrl({
854
+ * uniqueId: 'username',
855
+ * apiKey: 'your-api-key',
856
+ * });
857
+ * if (stream.alive) {
858
+ * console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
859
+ * console.log(stream.flvPullUrl); // Best FLV URL
860
+ * }
861
+ * ```
1067
862
  */
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("; ");
863
+ static async getStreamUrl(options) {
864
+ const serverUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
865
+ const resp = await fetch(`${serverUrl}/webcast/room_video`, {
866
+ method: "POST",
867
+ headers: {
868
+ "Content-Type": "application/json",
869
+ "x-api-key": options.apiKey
870
+ },
871
+ body: JSON.stringify({ unique_id: options.uniqueId.replace(/^@/, "") })
872
+ });
873
+ const data = await resp.json();
874
+ if (data.status_code !== 0) {
875
+ throw new Error(data.error || "Failed to get stream URL");
876
+ }
877
+ const d = data.data;
878
+ return {
879
+ roomId: d.room_id,
880
+ alive: d.alive ?? false,
881
+ streamUrls: d.stream_urls || {},
882
+ defaultQuality: d.default_quality || "",
883
+ flvPullUrl: d.flv_pull_url || void 0,
884
+ hlsPullUrl: d.hls_pull_url || void 0
885
+ };
1077
886
  }
1078
887
  on(event, listener) {
1079
888
  return super.on(event, listener);
@@ -1108,566 +917,119 @@ var TikTokLive = class extends EventEmitter {
1108
917
  const events = parseWebcastResponse(inner);
1109
918
  for (const evt of events) {
1110
919
  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);
920
+ if (evt.type === "battle") {
921
+ for (const team of evt.teams) {
922
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
923
+ if (host) {
924
+ this._battleHosts.set(team.hostUserId, host.user);
925
+ team.hostUser = host.user;
926
+ }
927
+ }
928
+ if (this._battleHosts.size > _TikTokLive.MAX_BATTLE_HOSTS) {
929
+ const excess = this._battleHosts.size - _TikTokLive.MAX_BATTLE_HOSTS;
930
+ const keys = [...this._battleHosts.keys()].slice(0, excess);
931
+ for (const key of keys) this._battleHosts.delete(key);
1204
932
  }
1205
933
  }
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);
934
+ if (evt.type === "battleArmies") {
935
+ const unresolvedIds = [];
936
+ for (const team of evt.teams) {
937
+ const host = team.users.find((u) => u.user.id === team.hostUserId);
938
+ if (host) {
939
+ team.hostUser = host.user;
940
+ this._battleHosts.set(team.hostUserId, host.user);
941
+ } else {
942
+ const cached = this._battleHosts.get(team.hostUserId);
943
+ if (cached) {
944
+ team.hostUser = cached;
945
+ } else if (team.hostUserId && team.hostUserId !== "0") {
946
+ unresolvedIds.push(team.hostUserId);
947
+ }
948
+ }
949
+ }
950
+ if (unresolvedIds.length > 0) {
951
+ this.resolveHostUsers(unresolvedIds, evt).catch(() => {
952
+ });
953
+ continue;
954
+ }
1458
955
  }
956
+ this.emit("event", evt);
957
+ this.emit(evt.type, evt);
958
+ }
1459
959
  }
1460
960
  } catch {
1461
- if (this.debug) console.error("[Captions] Failed to parse message:", raw);
1462
961
  }
1463
962
  }
1464
963
  /**
1465
- * Connect to the TikTok FLV stream and extract audio.
1466
- * Sends binary audio buffers to the server via WebSocket.
964
+ * Resolve unknown host user IDs via the sign server API.
965
+ * Caches results and re-emits the battleArmies event with enriched hostUser data.
1467
966
  */
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" });
967
+ async resolveHostUsers(userIds, originalEvt) {
968
+ const toResolve = userIds.filter((id) => !this._pendingHostResolves.has(id));
969
+ if (toResolve.length === 0) {
970
+ this.emit("event", originalEvt);
971
+ this.emit(originalEvt.type, originalEvt);
1472
972
  return;
1473
973
  }
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
- });
974
+ for (const id of toResolve) this._pendingHostResolves.add(id);
1494
975
  try {
1495
- if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
1496
- const resp = await fetch(url, {
1497
- signal: this.streamAbortController.signal,
976
+ const url = `${this.signServerUrl}/webcast/resolve_user_ids`;
977
+ const res = await fetch(url, {
978
+ method: "POST",
1498
979
  headers: {
1499
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
1500
- }
980
+ "Content-Type": "application/json",
981
+ "x-api-key": this.apiKey
982
+ },
983
+ body: JSON.stringify({ user_ids: toResolve }),
984
+ signal: AbortSignal.timeout(5e3)
1501
985
  });
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`);
986
+ if (res.ok) {
987
+ const json = await res.json();
988
+ if (json.data) {
989
+ for (const [uid, info] of Object.entries(json.data)) {
990
+ if (info.username) {
991
+ const hostUser = {
992
+ id: uid,
993
+ uniqueId: info.username,
994
+ nickname: info.nickname || info.username,
995
+ profilePicture: info.avatarUrl || void 0
996
+ };
997
+ this._battleHosts.set(uid, hostUser);
1533
998
  }
1534
999
  }
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);
1000
+ if (originalEvt.type === "battleArmies") {
1001
+ for (const team of originalEvt.teams) {
1002
+ if (!team.hostUser) {
1003
+ const cached = this._battleHosts.get(team.hostUserId);
1004
+ if (cached) team.hostUser = cached;
1547
1005
  }
1548
- if (this.debug && chunks <= 3) {
1549
- if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
1550
- }
1551
- }
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
1006
  }
1560
1007
  }
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 });
1008
+ }
1570
1009
  }
1010
+ } catch {
1011
+ } finally {
1012
+ for (const id of toResolve) this._pendingHostResolves.delete(id);
1013
+ this.emit("event", originalEvt);
1014
+ this.emit(originalEvt.type, originalEvt);
1571
1015
  }
1572
1016
  }
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;
1017
+ startHeartbeat(roomId) {
1018
+ this.stopHeartbeat();
1019
+ this.heartbeatTimer = setInterval(() => {
1020
+ if (this.ws?.readyState === WebSocket.OPEN) {
1021
+ this.ws.send(buildHeartbeat(roomId));
1022
+ }
1023
+ }, this.heartbeatInterval);
1656
1024
  }
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;
1025
+ stopHeartbeat() {
1026
+ if (this.heartbeatTimer) {
1027
+ clearInterval(this.heartbeatTimer);
1028
+ this.heartbeatTimer = null;
1029
+ }
1663
1030
  }
1664
- }
1031
+ };
1665
1032
  export {
1666
- TikTokCaptions,
1667
- TikTokLive,
1668
- fetchFeed,
1669
- getLiveFeed,
1670
- getRanklist,
1671
- getRegionalRanklist
1033
+ TikTokLive
1672
1034
  };
1673
1035
  //# sourceMappingURL=index.mjs.map