@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/README.md +12 -327
- package/dist/index.d.mts +81 -707
- package/dist/index.d.ts +81 -707
- package/dist/index.js +335 -978
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +334 -972
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -16
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import * as http from 'http';
|
|
3
2
|
|
|
4
3
|
interface TikTokUser {
|
|
5
4
|
id: string;
|
|
6
5
|
nickname: string;
|
|
7
6
|
uniqueId: string;
|
|
8
7
|
profilePicture?: string;
|
|
9
|
-
|
|
8
|
+
/** High-resolution avatar URL from protobuf field 11 (avatarLarge ~720x720) */
|
|
9
|
+
avatarLargeUrl?: string;
|
|
10
10
|
badges?: string[];
|
|
11
|
-
/** User's gifter/spender level (1-50). Higher = more coins spent platform-wide. */
|
|
12
|
-
payGrade?: number;
|
|
13
11
|
}
|
|
14
12
|
interface BaseEvent {
|
|
15
13
|
type: string;
|
|
@@ -20,6 +18,11 @@ interface ChatEvent extends BaseEvent {
|
|
|
20
18
|
type: 'chat';
|
|
21
19
|
user: TikTokUser;
|
|
22
20
|
comment: string;
|
|
21
|
+
emotes?: Array<{
|
|
22
|
+
emoteId: string;
|
|
23
|
+
imageUrl: string;
|
|
24
|
+
placeInComment: number;
|
|
25
|
+
}>;
|
|
23
26
|
}
|
|
24
27
|
interface MemberEvent extends BaseEvent {
|
|
25
28
|
type: 'member';
|
|
@@ -43,7 +46,6 @@ interface GiftEvent extends BaseEvent {
|
|
|
43
46
|
combo: boolean;
|
|
44
47
|
giftType: number;
|
|
45
48
|
groupId: string;
|
|
46
|
-
giftPictureUrl?: string;
|
|
47
49
|
}
|
|
48
50
|
interface SocialEvent extends BaseEvent {
|
|
49
51
|
type: 'social';
|
|
@@ -61,9 +63,9 @@ interface BattleTeamUser {
|
|
|
61
63
|
}
|
|
62
64
|
interface BattleTeam {
|
|
63
65
|
hostUserId: string;
|
|
66
|
+
hostUser?: TikTokUser;
|
|
64
67
|
score: number;
|
|
65
68
|
users: BattleTeamUser[];
|
|
66
|
-
hostUser?: TikTokUser;
|
|
67
69
|
}
|
|
68
70
|
interface BattleEvent extends BaseEvent {
|
|
69
71
|
type: 'battle';
|
|
@@ -71,45 +73,18 @@ interface BattleEvent extends BaseEvent {
|
|
|
71
73
|
status: number;
|
|
72
74
|
battleDuration: number;
|
|
73
75
|
teams: BattleTeam[];
|
|
74
|
-
battleSettings?: {
|
|
75
|
-
startTimeMs?: number;
|
|
76
|
-
duration?: number;
|
|
77
|
-
endTimeMs?: number;
|
|
78
|
-
};
|
|
79
76
|
}
|
|
80
77
|
interface BattleArmiesEvent extends BaseEvent {
|
|
81
78
|
type: 'battleArmies';
|
|
82
79
|
battleId: string;
|
|
83
80
|
status: number;
|
|
84
81
|
teams: BattleTeam[];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
giftSentTime?: number;
|
|
92
|
-
}
|
|
93
|
-
interface BattleTaskEvent extends BaseEvent {
|
|
94
|
-
type: 'battleTask';
|
|
95
|
-
taskAction: number;
|
|
96
|
-
battleRefId: string;
|
|
97
|
-
missionType: string;
|
|
98
|
-
multiplier: number;
|
|
99
|
-
missionDuration: number;
|
|
100
|
-
missionTarget: number;
|
|
101
|
-
remainingSeconds: number;
|
|
102
|
-
endTimestampS: number;
|
|
103
|
-
timerType: number;
|
|
104
|
-
}
|
|
105
|
-
interface BarrageEvent extends BaseEvent {
|
|
106
|
-
type: 'barrage';
|
|
107
|
-
msgType: number;
|
|
108
|
-
subType: number;
|
|
109
|
-
displayType: number;
|
|
110
|
-
duration: number;
|
|
111
|
-
defaultPattern: string;
|
|
112
|
-
content: string;
|
|
82
|
+
/** Battle start timestamp in milliseconds (from f18.f2) */
|
|
83
|
+
battleStartMs: number;
|
|
84
|
+
/** Total battle duration in seconds (from f18.f3, typically 300) */
|
|
85
|
+
battleDurationSeconds: number;
|
|
86
|
+
/** Seconds remaining in the battle, computed as max(0, duration - elapsed) */
|
|
87
|
+
timeLeftSeconds: number;
|
|
113
88
|
}
|
|
114
89
|
interface SubscribeEvent extends BaseEvent {
|
|
115
90
|
type: 'subscribe';
|
|
@@ -121,7 +96,6 @@ interface EmoteChatEvent extends BaseEvent {
|
|
|
121
96
|
user: TikTokUser;
|
|
122
97
|
emoteId: string;
|
|
123
98
|
emoteUrl: string;
|
|
124
|
-
emoteName?: string;
|
|
125
99
|
}
|
|
126
100
|
interface EnvelopeEvent extends BaseEvent {
|
|
127
101
|
type: 'envelope';
|
|
@@ -164,7 +138,7 @@ interface UnknownEvent extends BaseEvent {
|
|
|
164
138
|
type: 'unknown';
|
|
165
139
|
method: string;
|
|
166
140
|
}
|
|
167
|
-
type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent |
|
|
141
|
+
type LiveEvent = ChatEvent | MemberEvent | LikeEvent | GiftEvent | SocialEvent | RoomUserSeqEvent | BattleEvent | BattleArmiesEvent | SubscribeEvent | EmoteChatEvent | EnvelopeEvent | QuestionEvent | ControlEvent | RoomEvent | LiveIntroEvent | RankUpdateEvent | LinkMicEvent | UnknownEvent;
|
|
168
142
|
interface TikTokLiveEvents {
|
|
169
143
|
connected: () => void;
|
|
170
144
|
disconnected: (code: number, reason: string) => void;
|
|
@@ -178,8 +152,6 @@ interface TikTokLiveEvents {
|
|
|
178
152
|
roomUserSeq: (event: RoomUserSeqEvent) => void;
|
|
179
153
|
battle: (event: BattleEvent) => void;
|
|
180
154
|
battleArmies: (event: BattleArmiesEvent) => void;
|
|
181
|
-
battleTask: (event: BattleTaskEvent) => void;
|
|
182
|
-
barrage: (event: BarrageEvent) => void;
|
|
183
155
|
subscribe: (event: SubscribeEvent) => void;
|
|
184
156
|
emoteChat: (event: EmoteChatEvent) => void;
|
|
185
157
|
envelope: (event: EnvelopeEvent) => void;
|
|
@@ -197,7 +169,30 @@ interface RoomInfo {
|
|
|
197
169
|
wsHost: string;
|
|
198
170
|
clusterRegion: string;
|
|
199
171
|
connectedAt: string;
|
|
200
|
-
|
|
172
|
+
}
|
|
173
|
+
/** Quality tiers available for live stream video */
|
|
174
|
+
type StreamQuality = 'FULL_HD1' | 'HD1' | 'SD1' | 'SD2' | 'origin' | string;
|
|
175
|
+
/** Stream URLs for a specific quality level */
|
|
176
|
+
interface StreamUrls {
|
|
177
|
+
/** FLV pull URL for this quality */
|
|
178
|
+
flv?: string;
|
|
179
|
+
/** HLS (m3u8) pull URL for this quality */
|
|
180
|
+
hls?: string;
|
|
181
|
+
}
|
|
182
|
+
/** Full stream info returned by getStreamUrl() */
|
|
183
|
+
interface StreamInfo {
|
|
184
|
+
/** Room ID of the live stream */
|
|
185
|
+
roomId: string;
|
|
186
|
+
/** Whether the user is currently live */
|
|
187
|
+
alive: boolean;
|
|
188
|
+
/** Stream URLs keyed by quality (FULL_HD1, HD1, SD1, SD2) */
|
|
189
|
+
streamUrls: Record<StreamQuality, StreamUrls>;
|
|
190
|
+
/** Recommended default quality */
|
|
191
|
+
defaultQuality: StreamQuality;
|
|
192
|
+
/** Best available FLV URL (convenience shortcut) */
|
|
193
|
+
flvPullUrl?: string;
|
|
194
|
+
/** Best available HLS URL (convenience shortcut) */
|
|
195
|
+
hlsPullUrl?: string;
|
|
201
196
|
}
|
|
202
197
|
interface TikTokLiveOptions {
|
|
203
198
|
uniqueId: string;
|
|
@@ -207,236 +202,10 @@ interface TikTokLiveOptions {
|
|
|
207
202
|
maxReconnectAttempts?: number;
|
|
208
203
|
heartbeatInterval?: number;
|
|
209
204
|
debug?: boolean;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* TikTok browser session ID cookie for authenticated connections.
|
|
213
|
-
* Enables ranklist API access and authenticated WebSocket features.
|
|
214
|
-
* Obtain from the user's tiktok.com browser cookies.
|
|
215
|
-
*/
|
|
216
|
-
sessionId?: string;
|
|
217
|
-
/**
|
|
218
|
-
* TikTok target IDC region (e.g. 'useast5').
|
|
219
|
-
* Required when sessionId is provided — must match the account's region.
|
|
220
|
-
*/
|
|
221
|
-
ttTargetIdc?: string;
|
|
222
|
-
/**
|
|
223
|
-
* HTTP agent for routing connections through a proxy.
|
|
224
|
-
* Pass an HttpsProxyAgent (or any http.Agent) to route both the
|
|
225
|
-
* initial HTTP request and WebSocket connection through a proxy.
|
|
226
|
-
*
|
|
227
|
-
* Example:
|
|
228
|
-
* import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
229
|
-
* agent: new HttpsProxyAgent('http://user:pass@host:port')
|
|
230
|
-
*/
|
|
231
|
-
agent?: http.Agent;
|
|
232
|
-
/**
|
|
233
|
-
* If you already know the room ID (e.g. from a leaderboard API),
|
|
234
|
-
* pass it here to skip the HTML page scrape entirely.
|
|
235
|
-
* This avoids geo-restriction issues and speeds up connection.
|
|
236
|
-
*/
|
|
205
|
+
/** Pre-resolved room ID — skips direct TikTok page fetch when provided with sessionId */
|
|
237
206
|
roomId?: string;
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
* the library skips the HTTP fetch to tiktok.com entirely.
|
|
241
|
-
* Fetch once and share across multiple TikTokLive instances.
|
|
242
|
-
*/
|
|
243
|
-
ttwid?: string;
|
|
244
|
-
}
|
|
245
|
-
interface RanklistUser {
|
|
246
|
-
/** TikTok user ID */
|
|
247
|
-
id_str: string;
|
|
248
|
-
/** Display name */
|
|
249
|
-
nickname: string;
|
|
250
|
-
/** Username (@handle) */
|
|
251
|
-
display_id?: string;
|
|
252
|
-
unique_id?: string;
|
|
253
|
-
/** Avatar thumbnail URLs */
|
|
254
|
-
avatar_thumb?: {
|
|
255
|
-
url_list: string[];
|
|
256
|
-
};
|
|
257
|
-
/** Full avatar URLs */
|
|
258
|
-
avatar_medium?: {
|
|
259
|
-
url_list: string[];
|
|
260
|
-
};
|
|
261
|
-
/** Follower info */
|
|
262
|
-
follow_info?: {
|
|
263
|
-
follow_status: number;
|
|
264
|
-
follower_count: number;
|
|
265
|
-
following_count: number;
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Entry from online_audience endpoint (in-room top gifters).
|
|
270
|
-
* Has explicit `rank` (1-based) and `score` fields.
|
|
271
|
-
*/
|
|
272
|
-
interface OnlineAudienceEntry {
|
|
273
|
-
rank: number;
|
|
274
|
-
score: number;
|
|
275
|
-
user: RanklistUser;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Entry from anchor/rank_list endpoint (leaderboard/gifter rankings).
|
|
279
|
-
* Position is the array index. Uses `value` (not `score`).
|
|
280
|
-
*/
|
|
281
|
-
interface AnchorRankListEntry {
|
|
282
|
-
/** Gift value (diamonds/coins) */
|
|
283
|
-
value: number;
|
|
284
|
-
/** Rank category type (e.g. 3 = gifter) */
|
|
285
|
-
rank_type: number;
|
|
286
|
-
/** Time period type (e.g. 1 = hourly) */
|
|
287
|
-
rank_time_type: number;
|
|
288
|
-
/** Auto-thanks message configured by anchor */
|
|
289
|
-
auto_thanks_message?: string;
|
|
290
|
-
/** Schema URL for navigation */
|
|
291
|
-
schema_url?: string;
|
|
292
|
-
/** Highlight DM share status */
|
|
293
|
-
highlight_dm_share_status?: number;
|
|
294
|
-
/** Ranked user data */
|
|
295
|
-
user: RanklistUser;
|
|
296
|
-
}
|
|
297
|
-
interface RanklistSelfInfo {
|
|
298
|
-
rank: number;
|
|
299
|
-
score: number;
|
|
300
|
-
gap_description?: string;
|
|
301
|
-
}
|
|
302
|
-
/** Response from "online_audience" sub-endpoint — in-room top gifters */
|
|
303
|
-
interface OnlineAudienceResponse {
|
|
304
|
-
status_code: number;
|
|
305
|
-
data: {
|
|
306
|
-
ranks: OnlineAudienceEntry[];
|
|
307
|
-
self_info?: RanklistSelfInfo;
|
|
308
|
-
currency?: string;
|
|
309
|
-
total?: number;
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
/** Response from "anchor_rank_list" sub-endpoint — gifter leaderboard */
|
|
313
|
-
interface AnchorRankListResponse {
|
|
314
|
-
status_code: number;
|
|
315
|
-
data: {
|
|
316
|
-
rank_list: AnchorRankListEntry[];
|
|
317
|
-
/** Room where ranks were accumulated */
|
|
318
|
-
latest_room_id_str?: string;
|
|
319
|
-
/** Rank period start (unix timestamp) */
|
|
320
|
-
rank_time_begin?: number;
|
|
321
|
-
/** Rank period end (unix timestamp) */
|
|
322
|
-
rank_time_end?: number;
|
|
323
|
-
/** Whether to show rank summary */
|
|
324
|
-
show_rank_summary?: boolean;
|
|
325
|
-
/** Default rank type for this anchor */
|
|
326
|
-
default_rank_type?: number;
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
/** Entrance tab info from "entrance" sub-endpoint */
|
|
330
|
-
interface EntranceTab {
|
|
331
|
-
title: string;
|
|
332
|
-
rank_type: number;
|
|
333
|
-
list_lynx_type?: number;
|
|
334
|
-
}
|
|
335
|
-
/** Entrance config for a single rank_type */
|
|
336
|
-
interface EntranceInfo {
|
|
337
|
-
rank_type: number;
|
|
338
|
-
/** Whether the anchor appears in this ranking */
|
|
339
|
-
owner_on_rank: boolean;
|
|
340
|
-
/** Anchor's position in this ranking (0-based) */
|
|
341
|
-
owner_rank_idx?: number;
|
|
342
|
-
/** Current score in this ranking */
|
|
343
|
-
current_score?: number;
|
|
344
|
-
/** Countdown in seconds until ranking resets */
|
|
345
|
-
countdown?: number;
|
|
346
|
-
/** Window size in seconds (86400 = daily) */
|
|
347
|
-
window_size?: number;
|
|
348
|
-
/** Unix timestamp when ranking resets */
|
|
349
|
-
reset_time?: number;
|
|
350
|
-
/** Related tab to show when clicking this entrance */
|
|
351
|
-
related_tab_rank_type?: number;
|
|
352
|
-
/** Gap description with points needed to reach a rank */
|
|
353
|
-
affiliated_content?: {
|
|
354
|
-
gap_desc?: {
|
|
355
|
-
default_pattern: string;
|
|
356
|
-
pieces?: Array<{
|
|
357
|
-
string_value: string;
|
|
358
|
-
type: number;
|
|
359
|
-
}>;
|
|
360
|
-
};
|
|
361
|
-
};
|
|
362
|
-
/** Class/League info */
|
|
363
|
-
class_info?: {
|
|
364
|
-
class_type: number;
|
|
365
|
-
star_count: number;
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
interface EntranceResponse {
|
|
369
|
-
status_code: number;
|
|
370
|
-
data: Array<{
|
|
371
|
-
group_type?: number;
|
|
372
|
-
Priority?: number;
|
|
373
|
-
data?: {
|
|
374
|
-
tabs?: EntranceTab[];
|
|
375
|
-
entrances?: EntranceInfo[];
|
|
376
|
-
};
|
|
377
|
-
}>;
|
|
378
|
-
}
|
|
379
|
-
type RanklistResponse = OnlineAudienceResponse | AnchorRankListResponse | EntranceResponse;
|
|
380
|
-
/** Streamer info in a feed room entry */
|
|
381
|
-
interface FeedRoomOwner {
|
|
382
|
-
/** TikTok user ID */
|
|
383
|
-
id_str: string;
|
|
384
|
-
/** Username (@handle) */
|
|
385
|
-
display_id: string;
|
|
386
|
-
/** Display name */
|
|
387
|
-
nickname: string;
|
|
388
|
-
/** Avatar thumbnail URLs */
|
|
389
|
-
avatar_thumb?: {
|
|
390
|
-
url_list: string[];
|
|
391
|
-
};
|
|
392
|
-
/** Avatar large URLs */
|
|
393
|
-
avatar_large?: {
|
|
394
|
-
url_list: string[];
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
/** A single live room entry from the feed */
|
|
398
|
-
interface FeedRoom {
|
|
399
|
-
/** Room ID */
|
|
400
|
-
id_str: string;
|
|
401
|
-
/** Stream title */
|
|
402
|
-
title: string;
|
|
403
|
-
/** Current viewer count */
|
|
404
|
-
user_count: number;
|
|
405
|
-
/** Stream cover image URL */
|
|
406
|
-
cover?: {
|
|
407
|
-
url_list: string[];
|
|
408
|
-
};
|
|
409
|
-
/** Streamer info */
|
|
410
|
-
owner: FeedRoomOwner;
|
|
411
|
-
/** Stream status (2 = live) */
|
|
412
|
-
status: number;
|
|
413
|
-
/** Stream start time (unix seconds) */
|
|
414
|
-
create_time?: number;
|
|
415
|
-
/** Like count */
|
|
416
|
-
like_count?: number;
|
|
417
|
-
/** Hashtag IDs */
|
|
418
|
-
hashtag_ids?: string[];
|
|
419
|
-
}
|
|
420
|
-
/** Signed-URL response from GET/POST /webcast/feed */
|
|
421
|
-
interface FeedSignedResponse {
|
|
422
|
-
/** Always 0 on success */
|
|
423
|
-
status_code: number;
|
|
424
|
-
/** The signed TikTok URL to fetch */
|
|
425
|
-
signed_url: string;
|
|
426
|
-
/** Required headers */
|
|
427
|
-
headers: Record<string, string>;
|
|
428
|
-
/** Cookies to include (ttwid, sessionid, etc.) */
|
|
429
|
-
cookies?: string;
|
|
430
|
-
/** Region used */
|
|
431
|
-
region: string;
|
|
432
|
-
/** Channel ID used */
|
|
433
|
-
channel_id: string;
|
|
434
|
-
/** Remaining daily feed calls */
|
|
435
|
-
feed_remaining: number;
|
|
436
|
-
/** Daily feed call limit */
|
|
437
|
-
feed_limit: number;
|
|
438
|
-
/** Human-readable instructions */
|
|
439
|
-
note: string;
|
|
207
|
+
/** Pre-resolved ttwid session cookie — skips direct TikTok page fetch when provided with roomId */
|
|
208
|
+
sessionId?: string;
|
|
440
209
|
}
|
|
441
210
|
|
|
442
211
|
declare class TikTokLive extends EventEmitter {
|
|
@@ -445,9 +214,12 @@ declare class TikTokLive extends EventEmitter {
|
|
|
445
214
|
private reconnectAttempts;
|
|
446
215
|
private intentionalClose;
|
|
447
216
|
private _connected;
|
|
217
|
+
private _destroyed;
|
|
448
218
|
private _eventCount;
|
|
449
219
|
private _roomId;
|
|
450
|
-
private
|
|
220
|
+
private static readonly MAX_BATTLE_HOSTS;
|
|
221
|
+
private _battleHosts;
|
|
222
|
+
private _pendingHostResolves;
|
|
451
223
|
private readonly uniqueId;
|
|
452
224
|
private readonly signServerUrl;
|
|
453
225
|
private readonly apiKey;
|
|
@@ -455,453 +227,55 @@ declare class TikTokLive extends EventEmitter {
|
|
|
455
227
|
private readonly maxReconnectAttempts;
|
|
456
228
|
private readonly heartbeatInterval;
|
|
457
229
|
private readonly debug;
|
|
458
|
-
private
|
|
459
|
-
private
|
|
460
|
-
private readonly proxyAgent?;
|
|
461
|
-
private readonly _presetRoomId?;
|
|
462
|
-
private readonly _presetTtwid?;
|
|
230
|
+
private readonly _presetRoomId;
|
|
231
|
+
private readonly _presetSessionId;
|
|
463
232
|
constructor(options: TikTokLiveOptions);
|
|
464
233
|
connect(): Promise<void>;
|
|
465
234
|
disconnect(): void;
|
|
235
|
+
/**
|
|
236
|
+
* Fully destroy the client, releasing all resources and listeners.
|
|
237
|
+
* After calling destroy(), the instance cannot be reused — create a new one.
|
|
238
|
+
*/
|
|
239
|
+
destroy(): void;
|
|
466
240
|
get connected(): boolean;
|
|
241
|
+
get destroyed(): boolean;
|
|
467
242
|
get eventCount(): number;
|
|
468
243
|
get roomId(): string;
|
|
469
|
-
/** Get the stored session ID (if any) */
|
|
470
|
-
get sessionId(): string | undefined;
|
|
471
|
-
/** Update the session ID at runtime (e.g. after TikTok login) */
|
|
472
|
-
setSession(sessionId: string, ttTargetIdc?: string): void;
|
|
473
244
|
/**
|
|
474
|
-
*
|
|
475
|
-
* Returns
|
|
245
|
+
* Get live stream video URLs for a TikTok user.
|
|
246
|
+
* Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
|
|
247
|
+
* This is a standalone method — no WebSocket connection required.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* const stream = await TikTokLive.getStreamUrl({
|
|
252
|
+
* uniqueId: 'username',
|
|
253
|
+
* apiKey: 'your-api-key',
|
|
254
|
+
* });
|
|
255
|
+
* if (stream.alive) {
|
|
256
|
+
* console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
|
|
257
|
+
* console.log(stream.flvPullUrl); // Best FLV URL
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
476
260
|
*/
|
|
477
|
-
|
|
261
|
+
static getStreamUrl(options: {
|
|
262
|
+
uniqueId: string;
|
|
263
|
+
apiKey: string;
|
|
264
|
+
signServerUrl?: string;
|
|
265
|
+
quality?: string;
|
|
266
|
+
}): Promise<StreamInfo>;
|
|
478
267
|
on<K extends keyof TikTokLiveEvents>(event: K, listener: TikTokLiveEvents[K]): this;
|
|
479
268
|
once<K extends keyof TikTokLiveEvents>(event: K, listener: TikTokLiveEvents[K]): this;
|
|
480
269
|
off<K extends keyof TikTokLiveEvents>(event: K, listener: TikTokLiveEvents[K]): this;
|
|
481
270
|
emit<K extends keyof TikTokLiveEvents>(event: K, ...args: Parameters<TikTokLiveEvents[K]>): boolean;
|
|
482
271
|
private handleFrame;
|
|
483
|
-
private startHeartbeat;
|
|
484
|
-
private stopHeartbeat;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* TikTokCaptions — Real-time speech-to-text transcription and translation for TikTok LIVE streams.
|
|
489
|
-
*
|
|
490
|
-
* AI-powered audio transcription with speaker diarization, multi-language auto-detection,
|
|
491
|
-
* real-time translation, and sub-second latency. Connects via WebSocket to the TikTool
|
|
492
|
-
* captions relay for continuous streaming transcription.
|
|
493
|
-
*
|
|
494
|
-
* @example
|
|
495
|
-
* ```ts
|
|
496
|
-
* import { TikTokCaptions } from '@tiktool/live';
|
|
497
|
-
*
|
|
498
|
-
* const captions = new TikTokCaptions({
|
|
499
|
-
* uniqueId: 'username',
|
|
500
|
-
* apiKey: 'your-api-key',
|
|
501
|
-
* language: 'en', // translate to English
|
|
502
|
-
* });
|
|
503
|
-
*
|
|
504
|
-
* captions.on('caption', (data) => {
|
|
505
|
-
* console.log(`[${data.language}] ${data.text}`);
|
|
506
|
-
* });
|
|
507
|
-
*
|
|
508
|
-
* captions.on('translation', (data) => {
|
|
509
|
-
* console.log(`[translated] ${data.text}`);
|
|
510
|
-
* });
|
|
511
|
-
*
|
|
512
|
-
* await captions.start();
|
|
513
|
-
* ```
|
|
514
|
-
*/
|
|
515
|
-
|
|
516
|
-
interface TikTokCaptionsOptions {
|
|
517
|
-
/** TikTok username to transcribe (without @) */
|
|
518
|
-
uniqueId: string;
|
|
519
|
-
/** API key for authentication */
|
|
520
|
-
apiKey: string;
|
|
521
|
-
/** Source language hint (e.g. 'en', 'ja'). Leave empty for auto-detection. */
|
|
522
|
-
language?: string;
|
|
523
|
-
/** Target language for real-time translation (e.g. 'en', 'es', 'fr'). One language per session. */
|
|
524
|
-
translate?: string;
|
|
525
|
-
/** Enable speaker diarization to identify individual speakers (default: true) */
|
|
526
|
-
diarization?: boolean;
|
|
527
|
-
/** Max session duration in minutes before auto-disconnect (default: 60, max: 300) */
|
|
528
|
-
maxDurationMinutes?: number;
|
|
529
|
-
/** Custom server URL (default: wss://api.tik.tools) */
|
|
530
|
-
signServerUrl?: string;
|
|
531
|
-
/** Enable debug logging */
|
|
532
|
-
debug?: boolean;
|
|
533
|
-
/** Auto-reconnect on disconnect */
|
|
534
|
-
autoReconnect?: boolean;
|
|
535
|
-
/** Max reconnect attempts (default: 5) */
|
|
536
|
-
maxReconnectAttempts?: number;
|
|
537
|
-
}
|
|
538
|
-
interface CaptionData {
|
|
539
|
-
/** Transcribed text */
|
|
540
|
-
text: string;
|
|
541
|
-
/** Detected source language */
|
|
542
|
-
language: string;
|
|
543
|
-
/** Whether this is the final version of this segment */
|
|
544
|
-
isFinal: boolean;
|
|
545
|
-
/** Confidence score (0-1) */
|
|
546
|
-
confidence: number;
|
|
547
|
-
/** Speaker identifier (if available) */
|
|
548
|
-
speaker?: string;
|
|
549
|
-
/** Start time in milliseconds */
|
|
550
|
-
startMs?: number;
|
|
551
|
-
/** End time in milliseconds */
|
|
552
|
-
endMs?: number;
|
|
553
|
-
}
|
|
554
|
-
interface TranslationData {
|
|
555
|
-
/** Translated text */
|
|
556
|
-
text: string;
|
|
557
|
-
/** Target language */
|
|
558
|
-
language: string;
|
|
559
|
-
/** Whether this is the final version */
|
|
560
|
-
isFinal: boolean;
|
|
561
|
-
/** Confidence score (0-1) */
|
|
562
|
-
confidence: number;
|
|
563
|
-
/** Speaker identifier (if available) */
|
|
564
|
-
speaker?: string;
|
|
565
|
-
}
|
|
566
|
-
interface CaptionCredits {
|
|
567
|
-
/** Credits remaining */
|
|
568
|
-
remaining: number;
|
|
569
|
-
/** Total credits purchased */
|
|
570
|
-
total: number;
|
|
571
|
-
/** Credits used in this session */
|
|
572
|
-
used: number;
|
|
573
|
-
/** Whether credits are low */
|
|
574
|
-
warning: boolean;
|
|
575
|
-
}
|
|
576
|
-
interface CaptionStatus {
|
|
577
|
-
/** Current status */
|
|
578
|
-
status: 'connecting' | 'waiting' | 'live' | 'transcribing' | 'ended' | 'switching_language' | 'language_switched' | 'stream_ended';
|
|
579
|
-
/** TikTok username */
|
|
580
|
-
uniqueId?: string;
|
|
581
|
-
/** Room ID (once resolved) */
|
|
582
|
-
roomId?: string;
|
|
583
|
-
/** Language (for language switch events) */
|
|
584
|
-
language?: string;
|
|
585
|
-
/** Status message */
|
|
586
|
-
message?: string;
|
|
587
|
-
}
|
|
588
|
-
interface CaptionError {
|
|
589
|
-
/** Error code */
|
|
590
|
-
code: string;
|
|
591
|
-
/** Human-readable error message */
|
|
592
|
-
message: string;
|
|
593
|
-
}
|
|
594
|
-
interface TikTokCaptionsEvents {
|
|
595
|
-
/** Fired for each transcription token */
|
|
596
|
-
caption: (data: CaptionData) => void;
|
|
597
|
-
/** Fired for each translated token */
|
|
598
|
-
translation: (data: TranslationData) => void;
|
|
599
|
-
/** Status changes (connecting, live, transcribing, etc.) */
|
|
600
|
-
status: (data: CaptionStatus) => void;
|
|
601
|
-
/** Credit balance updates */
|
|
602
|
-
credits: (data: CaptionCredits) => void;
|
|
603
|
-
/** Low credit warning */
|
|
604
|
-
credits_low: (data: {
|
|
605
|
-
remaining: number;
|
|
606
|
-
total: number;
|
|
607
|
-
percent: number;
|
|
608
|
-
}) => void;
|
|
609
|
-
/** Error events */
|
|
610
|
-
error: (data: CaptionError) => void;
|
|
611
|
-
/** WebSocket connected */
|
|
612
|
-
connected: () => void;
|
|
613
|
-
/** WebSocket disconnected */
|
|
614
|
-
disconnected: (code: number, reason: string) => void;
|
|
615
|
-
}
|
|
616
|
-
declare class TikTokCaptions extends EventEmitter {
|
|
617
|
-
private ws;
|
|
618
|
-
private _connected;
|
|
619
|
-
private intentionalClose;
|
|
620
|
-
private reconnectAttempts;
|
|
621
|
-
private readonly uniqueId;
|
|
622
|
-
private readonly apiKey;
|
|
623
|
-
private readonly serverUrl;
|
|
624
|
-
private readonly autoReconnect;
|
|
625
|
-
private readonly maxReconnectAttempts;
|
|
626
|
-
private readonly debug;
|
|
627
|
-
private readonly _translate;
|
|
628
|
-
private readonly _diarization;
|
|
629
|
-
private readonly _maxDurationMinutes;
|
|
630
|
-
private _language;
|
|
631
|
-
private streamAbortController;
|
|
632
|
-
private flvExtractor;
|
|
633
|
-
private streamUrl;
|
|
634
|
-
constructor(options: TikTokCaptionsOptions);
|
|
635
272
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* once the user goes live (or immediately if already live).
|
|
273
|
+
* Resolve unknown host user IDs via the sign server API.
|
|
274
|
+
* Caches results and re-emits the battleArmies event with enriched hostUser data.
|
|
639
275
|
*/
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
*/
|
|
644
|
-
stop(): void;
|
|
645
|
-
/**
|
|
646
|
-
* Switch the translation target language on-the-fly.
|
|
647
|
-
* Causes a brief interruption while the transcription engine reconfigures.
|
|
648
|
-
*/
|
|
649
|
-
setLanguage(language: string): void;
|
|
650
|
-
/**
|
|
651
|
-
* Request a credit balance update from the server.
|
|
652
|
-
*/
|
|
653
|
-
getCredits(): void;
|
|
654
|
-
/** Whether the WebSocket is currently connected */
|
|
655
|
-
get connected(): boolean;
|
|
656
|
-
/** The current target language */
|
|
657
|
-
get language(): string;
|
|
658
|
-
on<K extends keyof TikTokCaptionsEvents>(event: K, listener: TikTokCaptionsEvents[K]): this;
|
|
659
|
-
once<K extends keyof TikTokCaptionsEvents>(event: K, listener: TikTokCaptionsEvents[K]): this;
|
|
660
|
-
off<K extends keyof TikTokCaptionsEvents>(event: K, listener: TikTokCaptionsEvents[K]): this;
|
|
661
|
-
emit<K extends keyof TikTokCaptionsEvents>(event: K, ...args: Parameters<TikTokCaptionsEvents[K]>): boolean;
|
|
662
|
-
private buildWsUrl;
|
|
663
|
-
private send;
|
|
664
|
-
private handleMessage;
|
|
665
|
-
/**
|
|
666
|
-
* Connect to the TikTok FLV stream and extract audio.
|
|
667
|
-
* Sends binary audio buffers to the server via WebSocket.
|
|
668
|
-
*/
|
|
669
|
-
private connectToStream;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
interface GetRanklistOptions {
|
|
673
|
-
/** API server URL (default: https://api.tik.tools) */
|
|
674
|
-
serverUrl?: string;
|
|
675
|
-
/** API key for authentication */
|
|
676
|
-
apiKey: string;
|
|
677
|
-
/** TikTok username to look up (auto-resolves room_id and anchor_id) */
|
|
678
|
-
uniqueId?: string;
|
|
679
|
-
/** Direct room ID (skip resolution) */
|
|
680
|
-
roomId?: string;
|
|
681
|
-
/** Direct anchor/owner ID (skip resolution) */
|
|
682
|
-
anchorId?: string;
|
|
683
|
-
/**
|
|
684
|
-
* TikTok session cookie string for authentication.
|
|
685
|
-
* Required — ranklist endpoints return 20003 without login.
|
|
686
|
-
* Example: "sessionid=abc123; sid_guard=def456"
|
|
687
|
-
*/
|
|
688
|
-
sessionCookie?: string;
|
|
689
|
-
/**
|
|
690
|
-
* Which ranklist sub-endpoint to call:
|
|
691
|
-
* - "online_audience" (default) — top gifters with scores
|
|
692
|
-
* - "anchor_rank_list" — gifter ranking by rank_type
|
|
693
|
-
* - "entrance" — entrance UI metadata, tabs, gap-to-rank
|
|
694
|
-
*/
|
|
695
|
-
type?: 'online_audience' | 'anchor_rank_list' | 'entrance';
|
|
696
|
-
/**
|
|
697
|
-
* For "anchor_rank_list" type only:
|
|
698
|
-
* - "1" = hourly ranking (default)
|
|
699
|
-
* - "8" = daily ranking
|
|
700
|
-
*/
|
|
701
|
-
rankType?: string;
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Fetch ranked user lists from TikTok via the sign server.
|
|
705
|
-
*
|
|
706
|
-
* When `sessionCookie` is provided, the server returns a **sign-and-return**
|
|
707
|
-
* response with a signed URL that you must fetch from your own IP
|
|
708
|
-
* (TikTok sessions are IP-bound). Check for `sign_and_return: true` in
|
|
709
|
-
* the response to detect this mode.
|
|
710
|
-
*
|
|
711
|
-
* @example
|
|
712
|
-
* ```ts
|
|
713
|
-
* const data = await getRanklist({
|
|
714
|
-
* apiKey: 'your-key',
|
|
715
|
-
* uniqueId: 'katarina.live',
|
|
716
|
-
* sessionCookie: 'sessionid=abc; sid_guard=def',
|
|
717
|
-
* type: 'online_audience',
|
|
718
|
-
* });
|
|
719
|
-
*
|
|
720
|
-
* if (data.sign_and_return) {
|
|
721
|
-
* // Fetch the signed URL from YOUR IP with your session cookie
|
|
722
|
-
* const resp = await fetch(data.signed_url, {
|
|
723
|
-
* method: data.method,
|
|
724
|
-
* headers: { ...data.headers, Cookie: sessionCookie },
|
|
725
|
-
* ...(data.body ? { body: data.body } : {}),
|
|
726
|
-
* });
|
|
727
|
-
* const tikData = await resp.json();
|
|
728
|
-
* console.log(tikData); // TikTok's raw response
|
|
729
|
-
* } else {
|
|
730
|
-
* console.log(data); // Direct TikTok response (no session)
|
|
731
|
-
* }
|
|
732
|
-
* ```
|
|
733
|
-
*/
|
|
734
|
-
declare function getRanklist(opts: GetRanklistOptions): Promise<RanklistResponse>;
|
|
735
|
-
|
|
736
|
-
interface GetRegionalRanklistOptions {
|
|
737
|
-
/** API server URL (default: https://api.tik.tools) */
|
|
738
|
-
serverUrl?: string;
|
|
739
|
-
/** API key for authentication (Pro or Ultra tier required) */
|
|
740
|
-
apiKey: string;
|
|
741
|
-
/** TikTok username to look up (auto-resolves room_id and anchor_id) */
|
|
742
|
-
uniqueId?: string;
|
|
743
|
-
/** Direct room ID (skip resolution) */
|
|
744
|
-
roomId?: string;
|
|
745
|
-
/** Direct anchor/owner ID (skip resolution) */
|
|
746
|
-
anchorId?: string;
|
|
747
|
-
/**
|
|
748
|
-
* Ranking period:
|
|
749
|
-
* - "1" = Hourly
|
|
750
|
-
* - "8" = Daily (default)
|
|
751
|
-
* - "15" = Popular LIVE
|
|
752
|
-
* - "16" = League
|
|
753
|
-
*/
|
|
754
|
-
rankType?: '1' | '8' | '15' | '16';
|
|
755
|
-
/**
|
|
756
|
-
* Sub-endpoint type:
|
|
757
|
-
* - "list" (default) — ranked users with scores
|
|
758
|
-
* - "entrance" — available ranking tabs/metadata
|
|
759
|
-
*/
|
|
760
|
-
type?: 'list' | 'entrance';
|
|
761
|
-
/** Gap interval filter (default: "0") */
|
|
762
|
-
gapInterval?: string;
|
|
763
|
-
/**
|
|
764
|
-
* TikTok session cookie string for authentication.
|
|
765
|
-
* Passed to the API server for proxied requests.
|
|
766
|
-
* Example: "sessionid=abc123; sessionid_ss=abc123"
|
|
767
|
-
*/
|
|
768
|
-
sessionCookie?: string;
|
|
769
|
-
/**
|
|
770
|
-
* Client's real IP address (for deployed server scenarios).
|
|
771
|
-
* When running on a remote server, pass the end-user's IP so the
|
|
772
|
-
* API server can proxy the TikTok request from the correct IP.
|
|
773
|
-
*/
|
|
774
|
-
clientIp?: string;
|
|
775
|
-
}
|
|
776
|
-
interface RegionalRanklistSignedResponse {
|
|
777
|
-
/** Always 0 on success */
|
|
778
|
-
status_code: number;
|
|
779
|
-
/** Always "fetch_signed_url" */
|
|
780
|
-
action: string;
|
|
781
|
-
/** The signed TikTok URL to POST */
|
|
782
|
-
signed_url: string;
|
|
783
|
-
/** HTTP method (always POST) */
|
|
784
|
-
method: string;
|
|
785
|
-
/** Required headers for the fetch */
|
|
786
|
-
headers: Record<string, string>;
|
|
787
|
-
/** URL-encoded POST body */
|
|
788
|
-
body: string;
|
|
789
|
-
/** Cookies to include (ttwid etc.) — append your sessionid */
|
|
790
|
-
cookies: string;
|
|
791
|
-
/** Human-readable note */
|
|
792
|
-
note: string;
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Get a signed URL for fetching regional LIVE leaderboard data.
|
|
796
|
-
*
|
|
797
|
-
* **Two-step pattern**: TikTok sessions are IP-bound, so instead of
|
|
798
|
-
* server-side fetching, this returns a signed URL with headers/body
|
|
799
|
-
* that you POST from your own IP with your session cookie.
|
|
800
|
-
*
|
|
801
|
-
* Requires **Pro** or **Ultra** API key tier.
|
|
802
|
-
*
|
|
803
|
-
* @example
|
|
804
|
-
* ```ts
|
|
805
|
-
* // Step 1: Get signed URL
|
|
806
|
-
* const signed = await getRegionalRanklist({
|
|
807
|
-
* apiKey: 'your-pro-key',
|
|
808
|
-
* roomId: '7607695933891218198',
|
|
809
|
-
* anchorId: '7444599004337652758',
|
|
810
|
-
* rankType: '8', // Daily
|
|
811
|
-
* });
|
|
812
|
-
*
|
|
813
|
-
* // Step 2: Fetch from YOUR IP with YOUR session
|
|
814
|
-
* const resp = await fetch(signed.signed_url, {
|
|
815
|
-
* method: signed.method,
|
|
816
|
-
* headers: { ...signed.headers, Cookie: `sessionid=YOUR_SID; ${signed.cookies}` },
|
|
817
|
-
* body: signed.body,
|
|
818
|
-
* });
|
|
819
|
-
* const { data } = await resp.json();
|
|
820
|
-
* data.rank_view.ranks.forEach((r, i) =>
|
|
821
|
-
* console.log(`${i+1}. ${r.user.nickname} — ${r.score} pts`)
|
|
822
|
-
* );
|
|
823
|
-
* ```
|
|
824
|
-
*/
|
|
825
|
-
declare function getRegionalRanklist(opts: GetRegionalRanklistOptions): Promise<RegionalRanklistSignedResponse>;
|
|
826
|
-
|
|
827
|
-
interface GetLiveFeedOptions {
|
|
828
|
-
/** API server URL (default: https://api.tik.tools) */
|
|
829
|
-
serverUrl?: string;
|
|
830
|
-
/** API key for authentication (Pro or Ultra tier required) */
|
|
831
|
-
apiKey: string;
|
|
832
|
-
/** Region code (default: 'US') */
|
|
833
|
-
region?: string;
|
|
834
|
-
/**
|
|
835
|
-
* Feed channel:
|
|
836
|
-
* - '87' = Recommended (default)
|
|
837
|
-
* - '86' = Suggested
|
|
838
|
-
* - '42' = Following
|
|
839
|
-
* - '1111006' = Gaming
|
|
840
|
-
*/
|
|
841
|
-
channelId?: string;
|
|
842
|
-
/** Number of rooms to return (max 50, default 20) */
|
|
843
|
-
count?: number;
|
|
844
|
-
/** Pagination cursor from previous response (default: '0') */
|
|
845
|
-
maxTime?: string;
|
|
846
|
-
/** TikTok sessionid cookie — required for populated results */
|
|
847
|
-
sessionId?: string;
|
|
848
|
-
/** TikTok ttwid cookie */
|
|
849
|
-
ttwid?: string;
|
|
850
|
-
/** TikTok msToken cookie */
|
|
851
|
-
msToken?: string;
|
|
276
|
+
private resolveHostUsers;
|
|
277
|
+
private startHeartbeat;
|
|
278
|
+
private stopHeartbeat;
|
|
852
279
|
}
|
|
853
|
-
/**
|
|
854
|
-
* Get a signed URL for fetching the TikTok LIVE feed.
|
|
855
|
-
*
|
|
856
|
-
* **Two-step pattern**: Returns a signed URL with headers and cookies.
|
|
857
|
-
* Fetch the signed URL from your own IP to get the feed data.
|
|
858
|
-
*
|
|
859
|
-
* Requires **Pro** or **Ultra** API key tier.
|
|
860
|
-
*
|
|
861
|
-
* @example
|
|
862
|
-
* ```ts
|
|
863
|
-
* // Step 1: Get signed URL
|
|
864
|
-
* const signed = await getLiveFeed({
|
|
865
|
-
* apiKey: 'your-pro-key',
|
|
866
|
-
* sessionId: 'your-tiktok-sessionid',
|
|
867
|
-
* region: 'US',
|
|
868
|
-
* count: 10,
|
|
869
|
-
* });
|
|
870
|
-
*
|
|
871
|
-
* // Step 2: Fetch from YOUR IP
|
|
872
|
-
* const resp = await fetch(signed.signed_url, {
|
|
873
|
-
* headers: { ...signed.headers, Cookie: signed.cookies || '' },
|
|
874
|
-
* });
|
|
875
|
-
* const data = await resp.json();
|
|
876
|
-
* console.log(`Found ${data.data?.length || 0} live rooms`);
|
|
877
|
-
*
|
|
878
|
-
* // Step 3: Load more (pagination)
|
|
879
|
-
* const nextSigned = await getLiveFeed({
|
|
880
|
-
* apiKey: 'your-pro-key',
|
|
881
|
-
* sessionId: 'your-tiktok-sessionid',
|
|
882
|
-
* maxTime: data.extra?.max_time || '0',
|
|
883
|
-
* });
|
|
884
|
-
* ```
|
|
885
|
-
*/
|
|
886
|
-
declare function getLiveFeed(opts: GetLiveFeedOptions): Promise<FeedSignedResponse>;
|
|
887
|
-
/**
|
|
888
|
-
* Convenience: Get the feed AND fetch the signed URL in one step.
|
|
889
|
-
* Returns the parsed JSON feed data from TikTok.
|
|
890
|
-
*
|
|
891
|
-
* @example
|
|
892
|
-
* ```ts
|
|
893
|
-
* const feed = await fetchFeed({
|
|
894
|
-
* apiKey: 'your-pro-key',
|
|
895
|
-
* sessionId: 'your-tiktok-sessionid',
|
|
896
|
-
* region: 'GR',
|
|
897
|
-
* count: 10,
|
|
898
|
-
* });
|
|
899
|
-
* for (const entry of feed.data || []) {
|
|
900
|
-
* const room = entry.data;
|
|
901
|
-
* console.log(`🔴 @${room.owner.display_id}: "${room.title}" — ${room.user_count} viewers`);
|
|
902
|
-
* }
|
|
903
|
-
* ```
|
|
904
|
-
*/
|
|
905
|
-
declare function fetchFeed(opts: GetLiveFeedOptions): Promise<any>;
|
|
906
280
|
|
|
907
|
-
export { type
|
|
281
|
+
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 };
|