@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/README.md +426 -235
- package/dist/index.d.mts +75 -4
- package/dist/index.d.ts +75 -4
- package/dist/index.js +272 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +272 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -3
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
440
|
-
for (const
|
|
443
|
+
const hostBufs = getAllBytes(f, 14);
|
|
444
|
+
for (const hb of hostBufs) {
|
|
441
445
|
try {
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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,
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
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
|
|
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] \
|
|
945
|
+
console.log(`[TikTokLive] \xE2\u0153\u2026 Got pre-signed URL from sign server`);
|
|
716
946
|
} else {
|
|
717
|
-
console.log(`[TikTokLive] \
|
|
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] \
|
|
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 ? "\
|
|
966
|
+
console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\xE2\u0153\u2026" : "\xE2\x9D\u0152 none"}`);
|
|
737
967
|
} catch (e) {
|
|
738
|
-
console.log(`[TikTokLive] \
|
|
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] \
|
|
984
|
+
console.log(`[TikTokLive] \xE2\u0153\u2026 Got roomId from sign server: ${roomId}`);
|
|
755
985
|
} else {
|
|
756
|
-
console.log(`[TikTokLive] \
|
|
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] \
|
|
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] \
|
|
777
|
-
} else console.log(`[TikTokLive] \
|
|
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] \
|
|
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] \
|
|
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] \
|
|
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.
|
|
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
|
|
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
|
|
1192
|
+
* This is a standalone method — no WebSocket connection required.
|
|
960
1193
|
*
|
|
961
1194
|
* @example
|
|
962
1195
|
* ```ts
|