@tiktool/live 2.7.1 → 2.8.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.d.mts CHANGED
@@ -74,6 +74,24 @@ interface BattleEvent extends BaseEvent {
74
74
  battleDuration: number;
75
75
  teams: BattleTeam[];
76
76
  }
77
+ interface BattleContributor {
78
+ /** TikTok userId of the gifter */
79
+ userId: string;
80
+ /** Diamond score this gifter contributed to the host's team */
81
+ score: number;
82
+ /** Display nickname (may be empty if not set on the account) */
83
+ nickname: string;
84
+ }
85
+ interface BattleHost {
86
+ /** TikTok userId of the host on this PK side */
87
+ hostUserId: string;
88
+ /** Total diamonds for this host's team */
89
+ teamTotalScore: number;
90
+ /** Side index (0 = left side, 1 = right side, …) */
91
+ teamIdx: number;
92
+ /** Per-gifter breakdown, sorted MVP first (highest score → lowest). */
93
+ contributors: BattleContributor[];
94
+ }
77
95
  interface BattleArmiesEvent extends BaseEvent {
78
96
  type: 'battleArmies';
79
97
  battleId: string;
@@ -87,6 +105,58 @@ interface BattleArmiesEvent extends BaseEvent {
87
105
  timeLeftSeconds: number;
88
106
  /** Battle end timestamp in milliseconds (battleStartMs + duration*1000). Use for clock-independent timer: Math.max(0, (endTimeMs - Date.now()) / 1000) */
89
107
  endTimeMs: number;
108
+ /** Stable match ID across multi-round PK (from top-level field 2). */
109
+ matchId?: string;
110
+ /** Per-round session ID (from top-level field 4). */
111
+ sessionId?: string;
112
+ /** TikTok server-side clock at frame emit (ms epoch). */
113
+ serverTsMs?: number;
114
+ /** Hex string tag used by TikTok for per-session matching (~34 chars). */
115
+ sessionTag?: string;
116
+ /** Total battle duration in seconds (from f18.f3). Alias for battleDurationSeconds. */
117
+ durationSec?: number;
118
+ /** Seconds remaining computed using TikTok server clock (no VPS drift). */
119
+ secsRemaining?: number;
120
+ /**
121
+ * Multi-guest host breakdown — one entry per host on a PK side.
122
+ * Each entry includes per-gifter contributors sorted MVP first.
123
+ */
124
+ hosts?: BattleHost[];
125
+ }
126
+ /**
127
+ * Battle Item Card event — booster multipliers (x2/x3), gloves, mist, thunder,
128
+ * extra-time, match-guide, etc. Emitted on `battleItemCard`.
129
+ */
130
+ interface BattleItemCardEvent extends BaseEvent {
131
+ type: 'battleItemCard';
132
+ battleId: string;
133
+ /** 2=gloves/crit, 3=mist, 4=match_guide, 10=x2?, 11=x3, … */
134
+ cardType: number;
135
+ /** 'gloves' | 'mist' | 'booster_x2' | 'booster_x3' | 'match_guide' | 'thunder' | 'extra_time' | raw resource key */
136
+ effect: string;
137
+ /** Raw TikTok resource key (e.g. 'pm_mt_boost_crit_name'). */
138
+ effectKey: string;
139
+ /** 2 or 3 for booster_x2/x3, otherwise 0. */
140
+ multiplier: number;
141
+ senderUserId: string;
142
+ senderNickname: string;
143
+ senderUniqueId: string;
144
+ /** First CDN URL for sender's avatar (best-resolution). */
145
+ senderAvatarUrl: string;
146
+ /** Unix seconds when the buff activated. */
147
+ activatedAtSec: number;
148
+ /** Total active duration in seconds. */
149
+ durationSec: number;
150
+ /** Unix seconds when the buff ends. */
151
+ endsAtSec: number;
152
+ /** Comment template, e.g. "{0:user} sent 1 magic mist". */
153
+ commentTemplate: string;
154
+ /** Full TikTok CDN URL for the card art (webp/jpeg). */
155
+ iconUrl: string;
156
+ /** Short identifier e.g. 'card_mist_v3' / 'card_crit_v3' / 'top3_buffer'. */
157
+ iconKey: string;
158
+ /** Hex e.g. '#BCD9E0' (mist blue), '#E0D4BC' (gloves tan). */
159
+ accentColor: string;
90
160
  }
91
161
  interface SubscribeEvent extends BaseEvent {
92
162
  type: 'subscribe';
@@ -140,7 +210,7 @@ interface UnknownEvent extends BaseEvent {
140
210
  type: 'unknown';
141
211
  method: string;
142
212
  }
143
- type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent | SubscribeEvent | EmoteChatEvent | EnvelopeEvent | QuestionEvent | ControlEvent | RoomEvent | LiveIntroEvent | RankUpdateEvent | LinkMicEvent | UnknownEvent;
213
+ type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent | BattleItemCardEvent | SubscribeEvent | EmoteChatEvent | EnvelopeEvent | QuestionEvent | ControlEvent | RoomEvent | LiveIntroEvent | RankUpdateEvent | LinkMicEvent | UnknownEvent;
144
214
  interface TikTokLiveEvents {
145
215
  connected: () => void;
146
216
  disconnected: (code: number, reason: string) => void;
@@ -154,6 +224,7 @@ interface TikTokLiveEvents {
154
224
  roomUserSeq: (event: RoomUserSeqEvent) => void;
155
225
  battle: (event: BattleEvent) => void;
156
226
  battleArmies: (event: BattleArmiesEvent) => void;
227
+ battleItemCard: (event: BattleItemCardEvent) => void;
157
228
  subscribe: (event: SubscribeEvent) => void;
158
229
  emoteChat: (event: EmoteChatEvent) => void;
159
230
  envelope: (event: EnvelopeEvent) => void;
@@ -236,7 +307,7 @@ declare class TikTokLive extends EventEmitter {
236
307
  disconnect(): void;
237
308
  /**
238
309
  * Fully destroy the client, releasing all resources and listeners.
239
- * After calling destroy(), the instance cannot be reused — create a new one.
310
+ * After calling destroy(), the instance cannot be reused — create a new one.
240
311
  */
241
312
  destroy(): void;
242
313
  get connected(): boolean;
@@ -246,7 +317,7 @@ declare class TikTokLive extends EventEmitter {
246
317
  /**
247
318
  * Get live stream video URLs for a TikTok user.
248
319
  * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
249
- * This is a standalone method — no WebSocket connection required.
320
+ * This is a standalone method — no WebSocket connection required.
250
321
  *
251
322
  * @example
252
323
  * ```ts
@@ -280,4 +351,4 @@ declare class TikTokLive extends EventEmitter {
280
351
  private stopHeartbeat;
281
352
  }
282
353
 
283
- export { type BaseEvent, type BattleArmiesEvent, type BattleEvent, type BattleTeam, type BattleTeamUser, type ChatEvent, type ControlEvent, type EmoteChatEvent, type EnvelopeEvent, type GiftEvent, type LikeEvent, type LinkMicEvent, type LiveEvent, type LiveIntroEvent, type MemberEvent, type QuestionEvent, type RankUpdateEvent, type RoomEvent, type RoomInfo, type RoomUserSeqEvent, type SocialEvent, type StreamInfo, type StreamQuality, type StreamUrls, type SubscribeEvent, TikTokLive, type TikTokLiveEvents, type TikTokLiveOptions, type TikTokUser, type UnknownEvent };
354
+ export { type BaseEvent, type BattleArmiesEvent, type BattleContributor, type BattleEvent, type BattleHost, type BattleItemCardEvent, type BattleTeam, type BattleTeamUser, type ChatEvent, type ControlEvent, type EmoteChatEvent, type EnvelopeEvent, type GiftEvent, type LikeEvent, type LinkMicEvent, type LiveEvent, type LiveIntroEvent, type MemberEvent, type QuestionEvent, type RankUpdateEvent, type RoomEvent, type RoomInfo, type RoomUserSeqEvent, type SocialEvent, type StreamInfo, type StreamQuality, type StreamUrls, type SubscribeEvent, TikTokLive, type TikTokLiveEvents, type TikTokLiveOptions, type TikTokUser, type UnknownEvent };
package/dist/index.d.ts CHANGED
@@ -74,6 +74,24 @@ interface BattleEvent extends BaseEvent {
74
74
  battleDuration: number;
75
75
  teams: BattleTeam[];
76
76
  }
77
+ interface BattleContributor {
78
+ /** TikTok userId of the gifter */
79
+ userId: string;
80
+ /** Diamond score this gifter contributed to the host's team */
81
+ score: number;
82
+ /** Display nickname (may be empty if not set on the account) */
83
+ nickname: string;
84
+ }
85
+ interface BattleHost {
86
+ /** TikTok userId of the host on this PK side */
87
+ hostUserId: string;
88
+ /** Total diamonds for this host's team */
89
+ teamTotalScore: number;
90
+ /** Side index (0 = left side, 1 = right side, …) */
91
+ teamIdx: number;
92
+ /** Per-gifter breakdown, sorted MVP first (highest score → lowest). */
93
+ contributors: BattleContributor[];
94
+ }
77
95
  interface BattleArmiesEvent extends BaseEvent {
78
96
  type: 'battleArmies';
79
97
  battleId: string;
@@ -87,6 +105,58 @@ interface BattleArmiesEvent extends BaseEvent {
87
105
  timeLeftSeconds: number;
88
106
  /** Battle end timestamp in milliseconds (battleStartMs + duration*1000). Use for clock-independent timer: Math.max(0, (endTimeMs - Date.now()) / 1000) */
89
107
  endTimeMs: number;
108
+ /** Stable match ID across multi-round PK (from top-level field 2). */
109
+ matchId?: string;
110
+ /** Per-round session ID (from top-level field 4). */
111
+ sessionId?: string;
112
+ /** TikTok server-side clock at frame emit (ms epoch). */
113
+ serverTsMs?: number;
114
+ /** Hex string tag used by TikTok for per-session matching (~34 chars). */
115
+ sessionTag?: string;
116
+ /** Total battle duration in seconds (from f18.f3). Alias for battleDurationSeconds. */
117
+ durationSec?: number;
118
+ /** Seconds remaining computed using TikTok server clock (no VPS drift). */
119
+ secsRemaining?: number;
120
+ /**
121
+ * Multi-guest host breakdown — one entry per host on a PK side.
122
+ * Each entry includes per-gifter contributors sorted MVP first.
123
+ */
124
+ hosts?: BattleHost[];
125
+ }
126
+ /**
127
+ * Battle Item Card event — booster multipliers (x2/x3), gloves, mist, thunder,
128
+ * extra-time, match-guide, etc. Emitted on `battleItemCard`.
129
+ */
130
+ interface BattleItemCardEvent extends BaseEvent {
131
+ type: 'battleItemCard';
132
+ battleId: string;
133
+ /** 2=gloves/crit, 3=mist, 4=match_guide, 10=x2?, 11=x3, … */
134
+ cardType: number;
135
+ /** 'gloves' | 'mist' | 'booster_x2' | 'booster_x3' | 'match_guide' | 'thunder' | 'extra_time' | raw resource key */
136
+ effect: string;
137
+ /** Raw TikTok resource key (e.g. 'pm_mt_boost_crit_name'). */
138
+ effectKey: string;
139
+ /** 2 or 3 for booster_x2/x3, otherwise 0. */
140
+ multiplier: number;
141
+ senderUserId: string;
142
+ senderNickname: string;
143
+ senderUniqueId: string;
144
+ /** First CDN URL for sender's avatar (best-resolution). */
145
+ senderAvatarUrl: string;
146
+ /** Unix seconds when the buff activated. */
147
+ activatedAtSec: number;
148
+ /** Total active duration in seconds. */
149
+ durationSec: number;
150
+ /** Unix seconds when the buff ends. */
151
+ endsAtSec: number;
152
+ /** Comment template, e.g. "{0:user} sent 1 magic mist". */
153
+ commentTemplate: string;
154
+ /** Full TikTok CDN URL for the card art (webp/jpeg). */
155
+ iconUrl: string;
156
+ /** Short identifier e.g. 'card_mist_v3' / 'card_crit_v3' / 'top3_buffer'. */
157
+ iconKey: string;
158
+ /** Hex e.g. '#BCD9E0' (mist blue), '#E0D4BC' (gloves tan). */
159
+ accentColor: string;
90
160
  }
91
161
  interface SubscribeEvent extends BaseEvent {
92
162
  type: 'subscribe';
@@ -140,7 +210,7 @@ interface UnknownEvent extends BaseEvent {
140
210
  type: 'unknown';
141
211
  method: string;
142
212
  }
143
- type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent | SubscribeEvent | EmoteChatEvent | EnvelopeEvent | QuestionEvent | ControlEvent | RoomEvent | LiveIntroEvent | RankUpdateEvent | LinkMicEvent | UnknownEvent;
213
+ type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent | BattleItemCardEvent | SubscribeEvent | EmoteChatEvent | EnvelopeEvent | QuestionEvent | ControlEvent | RoomEvent | LiveIntroEvent | RankUpdateEvent | LinkMicEvent | UnknownEvent;
144
214
  interface TikTokLiveEvents {
145
215
  connected: () => void;
146
216
  disconnected: (code: number, reason: string) => void;
@@ -154,6 +224,7 @@ interface TikTokLiveEvents {
154
224
  roomUserSeq: (event: RoomUserSeqEvent) => void;
155
225
  battle: (event: BattleEvent) => void;
156
226
  battleArmies: (event: BattleArmiesEvent) => void;
227
+ battleItemCard: (event: BattleItemCardEvent) => void;
157
228
  subscribe: (event: SubscribeEvent) => void;
158
229
  emoteChat: (event: EmoteChatEvent) => void;
159
230
  envelope: (event: EnvelopeEvent) => void;
@@ -236,7 +307,7 @@ declare class TikTokLive extends EventEmitter {
236
307
  disconnect(): void;
237
308
  /**
238
309
  * Fully destroy the client, releasing all resources and listeners.
239
- * After calling destroy(), the instance cannot be reused — create a new one.
310
+ * After calling destroy(), the instance cannot be reused — create a new one.
240
311
  */
241
312
  destroy(): void;
242
313
  get connected(): boolean;
@@ -246,7 +317,7 @@ declare class TikTokLive extends EventEmitter {
246
317
  /**
247
318
  * Get live stream video URLs for a TikTok user.
248
319
  * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
249
- * This is a standalone method — no WebSocket connection required.
320
+ * This is a standalone method — no WebSocket connection required.
250
321
  *
251
322
  * @example
252
323
  * ```ts
@@ -280,4 +351,4 @@ declare class TikTokLive extends EventEmitter {
280
351
  private stopHeartbeat;
281
352
  }
282
353
 
283
- export { type BaseEvent, type BattleArmiesEvent, type BattleEvent, type BattleTeam, type BattleTeamUser, type ChatEvent, type ControlEvent, type EmoteChatEvent, type EnvelopeEvent, type GiftEvent, type LikeEvent, type LinkMicEvent, type LiveEvent, type LiveIntroEvent, type MemberEvent, type QuestionEvent, type RankUpdateEvent, type RoomEvent, type RoomInfo, type RoomUserSeqEvent, type SocialEvent, type StreamInfo, type StreamQuality, type StreamUrls, type SubscribeEvent, TikTokLive, type TikTokLiveEvents, type TikTokLiveOptions, type TikTokUser, type UnknownEvent };
354
+ export { type BaseEvent, type BattleArmiesEvent, type BattleContributor, type BattleEvent, type BattleHost, type BattleItemCardEvent, type BattleTeam, type BattleTeamUser, type ChatEvent, type ControlEvent, type EmoteChatEvent, type EnvelopeEvent, type GiftEvent, type LikeEvent, type LinkMicEvent, type LiveEvent, type LiveIntroEvent, type MemberEvent, type QuestionEvent, type RankUpdateEvent, type RoomEvent, type RoomInfo, type RoomUserSeqEvent, type SocialEvent, type StreamInfo, type StreamQuality, type StreamUrls, type SubscribeEvent, TikTokLive, type TikTokLiveEvents, type TikTokLiveOptions, type TikTokUser, type UnknownEvent };
package/dist/index.js CHANGED
@@ -432,27 +432,41 @@ function parseWebcastMessage(method, payload) {
432
432
  // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
433
433
  // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
434
434
  case "WebcastLinkMicBattle": {
435
- const battleId = String(getInt(f, 1) || "");
436
- const status = getInt(f, 2) || 1;
435
+ const battleId = String(getInt(f, 4) || getInt(f, 1) || "");
436
+ let status = getInt(f, 7);
437
+ if (!status) {
438
+ const fallback = getInt(f, 2);
439
+ status = fallback > 0 && fallback < 100 ? fallback : 1;
440
+ }
437
441
  const battleDuration = getInt(f, 3);
438
442
  const teams = [];
439
- const battleUserBufs = getAllBytes(f, 10);
440
- for (const bub of battleUserBufs) {
443
+ const hostBufs = getAllBytes(f, 14);
444
+ for (const hb of hostBufs) {
441
445
  try {
442
- const bf = decodeProto(bub);
443
- const groupBuf = getBytes(bf, 2);
444
- if (groupBuf) {
445
- const gf = decodeProto(groupBuf);
446
- const linkUserBuf = getBytes(gf, 1);
447
- if (linkUserBuf) {
448
- const user = parseUser(linkUserBuf);
449
- teams.push({
450
- hostUserId: user.id,
451
- score: 0,
452
- users: [{ user, score: 0 }]
453
- });
446
+ const hf = decodeProto(hb);
447
+ const hostUserId = String(getInt(hf, 1) || "");
448
+ const teamTotalScore = getInt(hf, 3);
449
+ const users = [];
450
+ const containerBuf = getBytes(hf, 4);
451
+ if (containerBuf) {
452
+ const cf = decodeProto(containerBuf);
453
+ const userBufs = getAllBytes(cf, 1);
454
+ for (const ub of userBufs) {
455
+ try {
456
+ const uf = decodeProto(ub);
457
+ users.push({
458
+ user: {
459
+ id: String(getInt(uf, 1) || ""),
460
+ nickname: getStr(uf, 3) || "",
461
+ uniqueId: getStr(uf, 3) || ""
462
+ },
463
+ score: getInt(uf, 2)
464
+ });
465
+ } catch {
466
+ }
454
467
  }
455
468
  }
469
+ teams.push({ hostUserId, score: teamTotalScore, users });
456
470
  } catch {
457
471
  }
458
472
  }
@@ -465,15 +479,42 @@ function parseWebcastMessage(method, payload) {
465
479
  }
466
480
  }
467
481
  }
482
+ if (teams.length === 0) {
483
+ const battleUserBufs = getAllBytes(f, 10);
484
+ for (const bub of battleUserBufs) {
485
+ try {
486
+ const bf = decodeProto(bub);
487
+ const groupBuf = getBytes(bf, 2);
488
+ if (groupBuf) {
489
+ const gf = decodeProto(groupBuf);
490
+ const linkUserBuf = getBytes(gf, 1);
491
+ if (linkUserBuf) {
492
+ const user = parseUser(linkUserBuf);
493
+ teams.push({
494
+ hostUserId: user.id,
495
+ score: 0,
496
+ users: [{ user, score: 0 }]
497
+ });
498
+ }
499
+ }
500
+ } catch {
501
+ }
502
+ }
503
+ }
468
504
  return { ...base, type: "battle", battleId, status, battleDuration, teams };
469
505
  }
470
- // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
471
- // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
472
- // battleItems → { hostUserId=1, repeated battleGroups=2 }
473
- // battleGroups → { repeated users=1, int32 points=2 }
506
+ // Proto: WebcastLinkMicArmies { msg_header=1, matchId=2, session_id=4, startedAtMs=5,
507
+ // serverTsMs=6, repeated battleItems=3, status=7, sessionTag=17,
508
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 },
509
+ // repeated hosts=14 { hostUserId=1, teamTotalScore=3, teamIdx=5,
510
+ // userContainer=4 { repeated users=1 { userId=1, score=2, nickname=3 } } }
474
511
  case "WebcastLinkMicArmies": {
475
- const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
512
+ const sessionTag = getStr(f, 17);
513
+ const matchId = getIntStr(f, 2);
514
+ const sessionId = getIntStr(f, 4);
515
+ const battleId = sessionTag || matchId;
476
516
  const battleStatus = getInt(f, 7);
517
+ const serverTsMs = getInt(f, 6);
477
518
  const teams = [];
478
519
  const itemBufs = getAllBytes(f, 3);
479
520
  for (const ib of itemBufs) {
@@ -493,6 +534,10 @@ function parseWebcastMessage(method, payload) {
493
534
  } catch {
494
535
  }
495
536
  }
537
+ let secsRemaining = 0;
538
+ if (battleStartMs > 0 && battleDurationSeconds > 0 && serverTsMs > 0) {
539
+ secsRemaining = Math.max(0, battleDurationSeconds - Math.floor((serverTsMs - battleStartMs) / 1e3));
540
+ }
496
541
  let endTimeMs = 0;
497
542
  let timeLeftSeconds = 0;
498
543
  if (battleStartMs > 0 && battleDurationSeconds > 0) {
@@ -500,6 +545,36 @@ function parseWebcastMessage(method, payload) {
500
545
  const elapsedMs = Date.now() - battleStartMs;
501
546
  timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
502
547
  }
548
+ const hosts = [];
549
+ const hostBufs = getAllBytes(f, 14);
550
+ for (const hb of hostBufs) {
551
+ try {
552
+ const hf = decodeProto(hb);
553
+ const hostUserId = getIntStr(hf, 1);
554
+ const teamTotalScore = getInt(hf, 3);
555
+ const teamIdx = getInt(hf, 5);
556
+ const contributors = [];
557
+ const containerBuf = getBytes(hf, 4);
558
+ if (containerBuf) {
559
+ const cf = decodeProto(containerBuf);
560
+ const userBufs = getAllBytes(cf, 1);
561
+ for (const ub of userBufs) {
562
+ try {
563
+ const uf = decodeProto(ub);
564
+ contributors.push({
565
+ userId: getIntStr(uf, 1),
566
+ score: getInt(uf, 2),
567
+ nickname: getStr(uf, 3) || ""
568
+ });
569
+ } catch {
570
+ }
571
+ }
572
+ contributors.sort((a, b) => b.score - a.score);
573
+ }
574
+ hosts.push({ hostUserId, teamTotalScore, teamIdx, contributors });
575
+ } catch {
576
+ }
577
+ }
503
578
  return {
504
579
  ...base,
505
580
  type: "battleArmies",
@@ -509,7 +584,162 @@ function parseWebcastMessage(method, payload) {
509
584
  battleStartMs,
510
585
  battleDurationSeconds,
511
586
  timeLeftSeconds,
512
- endTimeMs
587
+ endTimeMs,
588
+ matchId,
589
+ sessionId,
590
+ serverTsMs,
591
+ sessionTag,
592
+ durationSec: battleDurationSeconds,
593
+ secsRemaining,
594
+ hosts
595
+ };
596
+ }
597
+ // Proto: WebcastLinkMicBattleItemCard
598
+ // #2 battleId
599
+ // #3 cardType (2=gloves/crit, 3=mist, 4=match_guide, 10=x2?, 11=x3, ...)
600
+ // #5/#6/#7/#14 inner (location varies with cardType — scan numerically):
601
+ // #1.{
602
+ // #1 effectKey
603
+ // #2 icon block { #1 repeated CDN urls, #2 iconKey, #5 accentColor }
604
+ // #3 activatedAtSec (unix seconds)
605
+ // #4 sender block: #1 sub { #1 userId, #2 nickname, #3 avatar block, #4 uniqueId }
606
+ // #5 durationSec
607
+ // #8 senderUserId (new boosters)
608
+ // #11 endsAtSec
609
+ // #12 senderUserId (old gloves/mist)
610
+ // }
611
+ // #3 comment template msg → #2 commentText
612
+ case "WebcastLinkMicBattleItemCard": {
613
+ const battleId = getIntStr(f, 2);
614
+ const cardType = getInt(f, 3);
615
+ let inner = null;
616
+ for (const fn of [5, 6, 7, 14, 15, 16, 8, 9, 10, 11, 12, 13]) {
617
+ const buf = getBytes(f, fn);
618
+ if (buf) {
619
+ inner = decodeProto(buf);
620
+ break;
621
+ }
622
+ }
623
+ let effectKey = "";
624
+ let effect = "unknown";
625
+ let multiplier = 0;
626
+ let activatedAtSec = 0;
627
+ let durationSec = 0;
628
+ let endsAtSec = 0;
629
+ let senderUserId = "";
630
+ let senderNickname = "";
631
+ let senderUniqueId = "";
632
+ let senderAvatarUrl = "";
633
+ let commentTemplate = "";
634
+ let iconUrl = "";
635
+ let iconKey = "";
636
+ let accentColor = "";
637
+ if (inner) {
638
+ let nameBuf = getBytes(inner, 1);
639
+ let nf = nameBuf ? decodeProto(nameBuf) : [];
640
+ let probedKey = nf.length ? getStr(nf, 1) || "" : "";
641
+ if (!probedKey) {
642
+ const altBuf = getBytes(inner, 3);
643
+ if (altBuf) {
644
+ const altFields = decodeProto(altBuf);
645
+ const altKey = getStr(altFields, 1) || "";
646
+ if (altKey) {
647
+ nameBuf = altBuf;
648
+ nf = altFields;
649
+ probedKey = altKey;
650
+ }
651
+ }
652
+ }
653
+ if (nameBuf && nf.length) {
654
+ effectKey = probedKey;
655
+ if (effectKey === "pm_mt_boost_crit_name") effect = "hammer";
656
+ else if (effectKey === "pm_mt_boost_glove_name" || effectKey === "pm_mt_boost_gloves_name") effect = "gloves";
657
+ else if (effectKey === "pm_mt_boost_mist_name") effect = "mist";
658
+ else if (effectKey === "pm_mt_match_guide_gotPowerUps") effect = "match_guide";
659
+ else if (effectKey.includes("userSentBooster_no2") || effectKey.includes("gotPowerUp_panel_no2")) {
660
+ effect = "booster_x2";
661
+ multiplier = 2;
662
+ } else if (effectKey.includes("userSentBooster_no3") || effectKey.includes("gotPowerUp_panel_no3")) {
663
+ effect = "booster_x3";
664
+ multiplier = 3;
665
+ } else if (effectKey.includes("thunder")) effect = "thunder";
666
+ else if (effectKey.includes("extra_time") || effectKey.includes("extra_round")) effect = "extra_time";
667
+ else if (effectKey) effect = effectKey;
668
+ activatedAtSec = getInt(nf, 3);
669
+ durationSec = getInt(nf, 5);
670
+ endsAtSec = getInt(nf, 11) || getInt(nf, 7);
671
+ senderUserId = getIntStr(nf, 12);
672
+ if (senderUserId === "0") senderUserId = getIntStr(nf, 8);
673
+ if (senderUserId === "0") senderUserId = getStr(nf, 13) || getStr(nf, 9) || "";
674
+ const iconBuf = getBytes(nf, 2);
675
+ if (iconBuf) {
676
+ const icf = decodeProto(iconBuf);
677
+ const urlBufs = getAllBytes(icf, 1);
678
+ for (const ub of urlBufs) {
679
+ const candidate = ub.toString("utf-8");
680
+ if (candidate.includes("://")) {
681
+ iconUrl = candidate;
682
+ break;
683
+ }
684
+ }
685
+ iconKey = getStr(icf, 2) || "";
686
+ accentColor = getStr(icf, 5) || "";
687
+ }
688
+ const senderOuter = getBytes(nf, 4);
689
+ if (senderOuter) {
690
+ const sf = decodeProto(senderOuter);
691
+ const sub = getBytes(sf, 1);
692
+ if (sub) {
693
+ const su = decodeProto(sub);
694
+ const nickBuf = getBytes(su, 2);
695
+ if (nickBuf) {
696
+ try {
697
+ senderNickname = nickBuf.toString("utf-8");
698
+ } catch {
699
+ }
700
+ }
701
+ senderUniqueId = getStr(su, 4) || "";
702
+ if (!senderUserId || senderUserId === "0") senderUserId = getIntStr(su, 1);
703
+ const avBuf = getBytes(su, 3);
704
+ if (avBuf) {
705
+ const af = decodeProto(avBuf);
706
+ const urlBufs = getAllBytes(af, 1);
707
+ for (const ub of urlBufs) {
708
+ const candidate = ub.toString("utf-8");
709
+ if (candidate.includes("://")) {
710
+ senderAvatarUrl = candidate;
711
+ break;
712
+ }
713
+ }
714
+ }
715
+ }
716
+ }
717
+ }
718
+ const commentBuf = getBytes(inner, 3);
719
+ if (commentBuf) {
720
+ const cf = decodeProto(commentBuf);
721
+ commentTemplate = getStr(cf, 2) || "";
722
+ }
723
+ }
724
+ return {
725
+ ...base,
726
+ type: "battleItemCard",
727
+ battleId,
728
+ cardType,
729
+ effect,
730
+ effectKey,
731
+ multiplier,
732
+ senderUserId,
733
+ senderNickname,
734
+ senderUniqueId,
735
+ senderAvatarUrl,
736
+ activatedAtSec,
737
+ durationSec,
738
+ endsAtSec,
739
+ commentTemplate,
740
+ iconUrl,
741
+ iconKey,
742
+ accentColor
513
743
  };
514
744
  }
515
745
  // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
@@ -712,13 +942,13 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
712
942
  preSignedCookies = credData.data.cookies || "";
713
943
  preSignedUA = credData.data.user_agent || DEFAULT_UA;
714
944
  ttwid = "__presigned__";
715
- console.log(`[TikTokLive] \u2705 Got pre-signed URL from sign server`);
945
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got pre-signed URL from sign server`);
716
946
  } else {
717
- console.log(`[TikTokLive] \u26A0\uFE0F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
947
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
718
948
  }
719
949
  }
720
950
  } catch (e) {
721
- console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
951
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
722
952
  }
723
953
  if (!ttwid) {
724
954
  console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
@@ -733,9 +963,9 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
733
963
  break;
734
964
  }
735
965
  }
736
- console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\u2705" : "\u274C none"}`);
966
+ console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\xE2\u0153\u2026" : "\xE2\x9D\u0152 none"}`);
737
967
  } catch (e) {
738
- console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
968
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
739
969
  }
740
970
  }
741
971
  } else {
@@ -751,13 +981,13 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
751
981
  const roomData = await roomResp.json();
752
982
  if (roomData.status_code === 0 && roomData.data?.room_id) {
753
983
  roomId = roomData.data.room_id;
754
- console.log(`[TikTokLive] \u2705 Got roomId from sign server: ${roomId}`);
984
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got roomId from sign server: ${roomId}`);
755
985
  } else {
756
- console.log(`[TikTokLive] \u26A0\uFE0F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
986
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
757
987
  }
758
988
  }
759
989
  } catch (e) {
760
- console.log(`[TikTokLive] \u274C room_id request failed: ${e.message}`);
990
+ console.log(`[TikTokLive] \xE2\x9D\u0152 room_id request failed: ${e.message}`);
761
991
  }
762
992
  if (roomId) {
763
993
  try {
@@ -773,14 +1003,14 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
773
1003
  const m = credData.data.cookies.match(/ttwid=([^;]+)/);
774
1004
  if (m) {
775
1005
  ttwid = m[1];
776
- console.log(`[TikTokLive] \u2705 Got ttwid from sign server`);
777
- } else console.log(`[TikTokLive] \u26A0\uFE0F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
1006
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got ttwid from sign server`);
1007
+ } else console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
778
1008
  } else {
779
- console.log(`[TikTokLive] \u26A0\uFE0F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
1009
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
780
1010
  }
781
1011
  }
782
1012
  } catch (e) {
783
- console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
1013
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
784
1014
  }
785
1015
  }
786
1016
  if (!roomId || !ttwid) {
@@ -817,7 +1047,7 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
817
1047
  }
818
1048
  console.log(`[TikTokLive] Direct fetch result: roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"}`);
819
1049
  } catch (e) {
820
- console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
1050
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
821
1051
  }
822
1052
  }
823
1053
  }
@@ -903,18 +1133,21 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
903
1133
  this.handleFrame(Buffer.from(rawData));
904
1134
  });
905
1135
  this.ws.on("close", (code, reason) => {
1136
+ const wasConnected = this._connected;
906
1137
  this._connected = false;
907
1138
  this.stopHeartbeat();
908
1139
  const reasonStr = reason?.toString() || "";
909
1140
  this.emit("disconnected", code, reasonStr);
910
- if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1141
+ if (wasConnected && !this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
911
1142
  this.reconnectAttempts++;
912
1143
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
913
1144
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
914
1145
  }
915
1146
  });
916
1147
  this.ws.on("error", (err) => {
917
- this.emit("error", err);
1148
+ if (this._connected) {
1149
+ this.emit("error", err);
1150
+ }
918
1151
  if (!this._connected) reject(err);
919
1152
  });
920
1153
  });
@@ -931,7 +1164,7 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
931
1164
  }
932
1165
  /**
933
1166
  * Fully destroy the client, releasing all resources and listeners.
934
- * After calling destroy(), the instance cannot be reused — create a new one.
1167
+ * After calling destroy(), the instance cannot be reused — create a new one.
935
1168
  */
936
1169
  destroy() {
937
1170
  this.disconnect();
@@ -956,7 +1189,7 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
956
1189
  /**
957
1190
  * Get live stream video URLs for a TikTok user.
958
1191
  * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
959
- * This is a standalone method — no WebSocket connection required.
1192
+ * This is a standalone method — no WebSocket connection required.
960
1193
  *
961
1194
  * @example
962
1195
  * ```ts