@tiktool/live 2.7.1 → 2.9.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 CHANGED
@@ -1,235 +1,505 @@
1
- <div align="center">
2
-
3
- # @tiktool/live
4
-
5
- ### Connect to any TikTok LIVE stream in 4 lines of code.
6
-
7
- [![npm version](https://img.shields.io/npm/v/@tiktool/live?color=%23ff0050&label=npm&logo=npm)](https://www.npmjs.com/package/@tiktool/live)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green?logo=node.js)](https://nodejs.org)
10
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org)
11
-
12
- Real-time chat, gifts, viewers, battles, follows & 18+ event types from any TikTok livestream.
13
-
14
- [Quick Start](#-quick-start) · [Events](#-events) · [API](#-api-reference) · [Rate Limits](#-rate-limits) · [Get API Key](https://tik.tools)
15
-
16
- </div>
17
-
18
- ---
19
-
20
- ## ⚡ Quick Start
21
-
22
- ```bash
23
- npm install @tiktool/live
24
- ```
25
-
26
- Get your free API key at [tik.tools](https://tik.tools)
27
-
28
- ```typescript
29
- import { TikTokLive } from '@tiktool/live';
30
-
31
- const live = new TikTokLive({
32
- uniqueId: 'tv_asahi_news',
33
- apiKey: 'YOUR_API_KEY',
34
- });
35
-
36
- live.on('chat', e => console.log(`${e.user.uniqueId}: ${e.comment}`));
37
- live.on('gift', e => console.log(`${e.user.uniqueId} sent ${e.giftName} (${e.diamondCount} diamonds)`));
38
- live.on('member', e => console.log(`${e.user.uniqueId} joined`));
39
- live.on('roomUserSeq', e => console.log(`Viewers: ${e.viewerCount}`));
40
-
41
- await live.connect();
42
- ```
43
-
44
- ---
45
-
46
- ## How It Works
47
-
48
- ```
49
- Your App tik.tools TikTok
50
- +-----------+ +--------------+ +--------------+
51
- | -+-- sign_url --> Signs URL | | |
52
- | Your <-+-- X-Bogus --| with params | | TikTok |
53
- | Code | | | | WebSocket |
54
- | -+------------ Connect directly ---------->| Server |
55
- | <-+------------ Live events (protobuf) <---| |
56
- +-----------+ +--------------+ +--------------+
57
- ^ Only interaction ^ Direct from
58
- with our server YOUR IP
59
- ```
60
-
61
- - Your app connects directly to TikTok from your IP address
62
- - The sign server only generates cryptographic signatures (requires API key)
63
- - TikTok never sees the sign server
64
- - Built-in protobuf parser, no external dependencies
65
-
66
- ---
67
-
68
- ## Events
69
-
70
- ### Listening
71
-
72
- ```typescript
73
- live.on('chat', (event) => {
74
- event.user.uniqueId // string
75
- event.comment // string
76
- });
77
-
78
- live.on('event', (event) => {
79
- console.log(event.type, event);
80
- });
81
- ```
82
-
83
- ### Reference
84
-
85
- | Event | Type | Description | Fields |
86
- |-------|------|-------------|--------|
87
- | `chat` | `ChatEvent` | Chat message | `user`, `comment` |
88
- | `member` | `MemberEvent` | User joined | `user`, `action` |
89
- | `like` | `LikeEvent` | User liked | `user`, `likeCount`, `totalLikes` |
90
- | `gift` | `GiftEvent` | Gift sent | `user`, `giftName`, `diamondCount`, `repeatCount`, `combo` |
91
- | `social` | `SocialEvent` | Follow / Share | `user`, `action` |
92
- | `roomUserSeq` | `RoomUserSeqEvent` | Viewer count | `viewerCount`, `totalViewers` |
93
- | `battle` | `BattleEvent` | Link Mic battle | `status` |
94
- | `battleArmies` | `BattleArmiesEvent` | Battle teams | — |
95
- | `subscribe` | `SubscribeEvent` | New subscriber | `user`, `subMonth` |
96
- | `emoteChat` | `EmoteChatEvent` | Emote in chat | `user`, `emoteId` |
97
- | `envelope` | `EnvelopeEvent` | Treasure chest | `diamondCount` |
98
- | `question` | `QuestionEvent` | Q&A question | `user`, `questionText` |
99
- | `control` | `ControlEvent` | Stream control | `action` (3 = ended) |
100
- | `room` | `RoomEvent` | Room status | `status` |
101
- | `liveIntro` | `LiveIntroEvent` | Stream intro | `title` |
102
- | `rankUpdate` | `RankUpdateEvent` | Rank update | `rankType` |
103
- | `linkMic` | `LinkMicEvent` | Link Mic | `action` |
104
- | `unknown` | `UnknownEvent` | Unrecognized | `method` |
105
-
106
- ### Connection Events
107
-
108
- | Event | Callback | Description |
109
- |-------|----------|-------------|
110
- | `connected` | `() => void` | Connected to stream |
111
- | `disconnected` | `(code, reason) => void` | Disconnected |
112
- | `roomInfo` | `(info: RoomInfo) => void` | Room info |
113
- | `error` | `(error: Error) => void` | Error |
114
-
115
- ---
116
-
117
- ## API Reference
118
-
119
- ### `new TikTokLive(options)`
120
-
121
- | Option | Type | Default | Description |
122
- |--------|------|---------|-------------|
123
- | `uniqueId` | `string` | — | TikTok username (without @) |
124
- | `apiKey` | `string` | — | **Required.** API key from [tik.tools](https://tik.tools) |
125
- | `signServerUrl` | `string` | `https://api.tik.tools` | Sign server URL |
126
- | `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
127
- | `maxReconnectAttempts` | `number` | `5` | Max reconnect attempts |
128
- | `heartbeatInterval` | `number` | `10000` | Heartbeat interval (ms) |
129
- | `debug` | `boolean` | `false` | Debug logging |
130
-
131
- ### Methods
132
-
133
- | Method | Returns | Description |
134
- |--------|---------|-------------|
135
- | `connect()` | `Promise<void>` | Connect to livestream |
136
- | `disconnect()` | `void` | Disconnect |
137
- | `connected` | `boolean` | Connection status |
138
- | `eventCount` | `number` | Total events received |
139
- | `roomId` | `string` | Current room ID |
140
-
141
- ---
142
-
143
- ## Rate Limits
144
-
145
- All API requests require an API key. Get yours at [tik.tools](https://tik.tools).
146
-
147
- | Tier | Rate Limit | Endpoints | Price |
148
- |------|-----------|-----------|-------|
149
- | **Free** | 5/min | `sign_url`, `check_alive` | Free |
150
- | **Basic** | 30/min | + `fetch`, `room_info`, `bulk_live_check`, WS relay | Free (with key) |
151
- | **Pro** | 120/min | + `room_video`, all endpoints | Coming soon |
152
-
153
- The SDK calls the sign server **once per connection**, then stays connected via WebSocket. A free key is sufficient for most use cases.
154
-
155
- ---
156
-
157
- ## Examples
158
-
159
- ### Chat Bot
160
-
161
- ```typescript
162
- import { TikTokLive } from '@tiktool/live';
163
-
164
- const live = new TikTokLive({
165
- uniqueId: 'streamer_name',
166
- apiKey: 'YOUR_API_KEY',
167
- });
168
-
169
- live.on('chat', (e) => {
170
- if (e.comment.toLowerCase() === '!hello') {
171
- console.log(`Hello, ${e.user.nickname}!`);
172
- }
173
- });
174
-
175
- live.on('gift', (e) => {
176
- if (e.repeatEnd) {
177
- console.log(`${e.user.uniqueId} sent ${e.repeatCount}x ${e.giftName} (${e.diamondCount * e.repeatCount} diamonds)`);
178
- }
179
- });
180
-
181
- await live.connect();
182
- ```
183
-
184
- ### OBS Overlay
185
-
186
- ```typescript
187
- import { TikTokLive } from '@tiktool/live';
188
- import { WebSocketServer } from 'ws';
189
-
190
- const wss = new WebSocketServer({ port: 8080 });
191
- const live = new TikTokLive({
192
- uniqueId: 'streamer_name',
193
- apiKey: 'YOUR_API_KEY',
194
- });
195
-
196
- live.on('event', (event) => {
197
- for (const client of wss.clients) {
198
- client.send(JSON.stringify(event));
199
- }
200
- });
201
-
202
- await live.connect();
203
- console.log('Forwarding events to ws://localhost:8080');
204
- ```
205
-
206
- ---
207
-
208
- ## TypeScript
209
-
210
- Full TypeScript support with type inference:
211
-
212
- ```typescript
213
- import { TikTokLive, ChatEvent, GiftEvent } from '@tiktool/live';
214
-
215
- const live = new TikTokLive({
216
- uniqueId: 'username',
217
- apiKey: 'YOUR_API_KEY',
218
- });
219
-
220
- live.on('chat', (event: ChatEvent) => {
221
- const username: string = event.user.uniqueId;
222
- const message: string = event.comment;
223
- });
224
-
225
- live.on('gift', (event: GiftEvent) => {
226
- const diamonds: number = event.diamondCount;
227
- const isCombo: boolean = event.combo;
228
- });
229
- ```
230
-
231
- ---
232
-
233
- ## License
234
-
235
- MIT © [tiktool](https://tik.tools)
1
+ <div align="center">
2
+
3
+ # @tiktool/live
4
+
5
+ ### Connect to any TikTok LIVE stream in 4 lines of code.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@tiktool/live?color=%23ff0050&label=npm&logo=npm)](https://www.npmjs.com/package/@tiktool/live)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green?logo=node.js)](https://nodejs.org)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org)
11
+
12
+ Real-time chat, gifts, viewers, **PK battles with MVP breakdown**, **x2/x3 boosters**, gloves, mist, match-guide & 20+ event types from any TikTok livestream.
13
+
14
+ [Quick Start](#-quick-start) · [Events](#-events) · [PK Battles](#-pk-battles) · [API](#-api-reference) · [Rate Limits](#-rate-limits) · [Get API Key](https://tik.tools)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## ⚡ Quick Start
21
+
22
+ ```bash
23
+ npm install @tiktool/live
24
+ ```
25
+
26
+ Get your free API key at [tik.tools](https://tik.tools)
27
+
28
+ ```typescript
29
+ import { TikTokLive } from '@tiktool/live';
30
+
31
+ const live = new TikTokLive({
32
+ uniqueId: 'tv_asahi_news',
33
+ apiKey: 'YOUR_API_KEY',
34
+ });
35
+
36
+ live.on('chat', e => console.log(`${e.user.uniqueId}: ${e.comment}`));
37
+ live.on('gift', e => console.log(`${e.user.uniqueId} sent ${e.giftName} (${e.diamondCount} diamonds)`));
38
+ live.on('member', e => console.log(`${e.user.uniqueId} joined`));
39
+ live.on('roomUserSeq', e => console.log(`Viewers: ${e.viewerCount}`));
40
+
41
+ await live.connect();
42
+ ```
43
+
44
+ ---
45
+
46
+ ## How It Works
47
+
48
+ ```
49
+ Your App tik.tools TikTok
50
+ +-----------+ +--------------+ +--------------+
51
+ | -+-- sign_url --> Signs URL | | |
52
+ | Your <-+-- X-Bogus --| with params | | TikTok |
53
+ | Code | | | | WebSocket |
54
+ | -+------------ Connect directly ---------->| Server |
55
+ | <-+------------ Live events (protobuf) <---| |
56
+ +-----------+ +--------------+ +--------------+
57
+ ^ Only interaction ^ Direct from
58
+ with our server YOUR IP
59
+ ```
60
+
61
+ - Your app connects directly to TikTok from your IP address
62
+ - The sign server only generates cryptographic signatures (requires API key)
63
+ - TikTok never sees the sign server
64
+ - Built-in protobuf parser, **zero external dependencies** (only `ws` for WebSocket)
65
+
66
+ ---
67
+
68
+ ## Connection Modes
69
+
70
+ There are **two ways** to receive TikTok LIVE events from `tik.tools`. Pick based on scale and IP-exposure tolerance.
71
+
72
+ ### Mode 1 — Direct (default, this SDK)
73
+
74
+ ```
75
+ Your App ──signs URL via──▶ tik.tools ──returns signed URL──▶ Your App
76
+ Your App ───────────────── WS direct to TikTok ───────────────▶ TikTok
77
+ Your App ◀────────────────── live events ────────────────────── TikTok
78
+ ```
79
+
80
+ - **TikTok sees your IP.** Cheapest on our API quota (only the `ws_credentials` call is charged per connection).
81
+ - Good for **up to ~30 concurrent streams** from one container.
82
+ - Above that, TikTok's per-IP rate limit kicks in → frequent `code=1006` closes + reconnect loops.
83
+ - **Solution at scale**: enable the `proxy` option (below) to dial through your own residential pool.
84
+
85
+ ```typescript
86
+ const live = new TikTokLive({
87
+ uniqueId: 'creator_username',
88
+ apiKey: 'YOUR_TIKTOOLS_KEY',
89
+ // Optional: route WS + HTTP through your own proxy (recommended for 30+ streams)
90
+ proxy: 'http://USER:PASS@p.webshare.io:80',
91
+ });
92
+ ```
93
+
94
+ Install `https-proxy-agent` if using `proxy`:
95
+
96
+ ```bash
97
+ npm install https-proxy-agent
98
+ ```
99
+
100
+ ### Mode 2 — Relayed (via our proxy pool, no SDK install)
101
+
102
+ ```
103
+ Your App ◀──── wss://api.tik.tools/?uniqueId=X&apiKey=Y ──── tik.tools
104
+ │
105
+ │ WS via
106
+ │ rotated proxies
107
+ ▼
108
+ TikTok
109
+ ```
110
+
111
+ - **TikTok sees our IPs** (rotated across hundreds of Webshare + residential proxies).
112
+ - We absorb every per-IP rate-limit hit. Survives any block automatically.
113
+ - Best for **30+ concurrent streams**, low-budget setups, or anyone who doesn't want their own IP fingerprinted by TikTok.
114
+ - Costs slightly more per stream-hour (events stream through us, not signed-once direct).
115
+ - No SDK install — plain `WebSocket`. Events arrive pre-parsed as JSON.
116
+
117
+ ```typescript
118
+ import WebSocket from 'ws';
119
+
120
+ const ws = new WebSocket(
121
+ `wss://api.tik.tools/?uniqueId=creator_username&apiKey=YOUR_KEY`
122
+ );
123
+
124
+ ws.on('message', raw => {
125
+ const msg = JSON.parse(raw.toString());
126
+ if (msg.event === 'chat') console.log(msg.data.user.uniqueId, msg.data.comment);
127
+ if (msg.event === 'gift') console.log(msg.data.user.uniqueId, msg.data.giftName, msg.data.diamondCount);
128
+ if (msg.event === 'battleArmies') console.log(msg.data);
129
+ });
130
+ ```
131
+
132
+ Event payloads are identical to Direct-mode `client.on('chat', …)` etc. — only the wrapping changes (`{event, data}` envelope vs the SDK's separate emit).
133
+
134
+ ### Side-by-side
135
+
136
+ | Aspect | Direct (Mode 1) | Relayed (Mode 2) |
137
+ |---|---|---|
138
+ | TikTok sees | Your IP | Our rotated proxy pool |
139
+ | Setup | `new TikTokLive(...)` | `new WebSocket(url)` |
140
+ | Scale limit | ~30 streams / container IP | Thousands |
141
+ | Per-event cost | 1× `ws_credentials` per (re)connect | 1× per event |
142
+ | TikTok rate-limit risk | Yes (mitigated with `proxy`) | None — we handle it |
143
+ | Best for | Hobby / single-creator | Production at scale |
144
+
145
+ ---
146
+
147
+ ## Events
148
+
149
+ ### Listening
150
+
151
+ ```typescript
152
+ live.on('chat', (event) => {
153
+ event.user.uniqueId // string
154
+ event.comment // string
155
+ });
156
+
157
+ live.on('event', (event) => {
158
+ console.log(event.type, event);
159
+ });
160
+ ```
161
+
162
+ ### Reference
163
+
164
+ | Event | Type | Description | Fields |
165
+ |-------|------|-------------|--------|
166
+ | `chat` | `ChatEvent` | Chat message (with inline emotes) | `user`, `comment`, `emotes` |
167
+ | `member` | `MemberEvent` | User joined | `user`, `action` |
168
+ | `like` | `LikeEvent` | User liked | `user`, `likeCount`, `totalLikes` |
169
+ | `gift` | `GiftEvent` | Gift sent | `user`, `giftId`, `giftName`, `diamondCount`, `repeatCount`, `combo`, `groupId` |
170
+ | `social` | `SocialEvent` | Follow / Share | `user`, `action` |
171
+ | `roomUserSeq` | `RoomUserSeqEvent` | Viewer count | `viewerCount`, `totalViewers` |
172
+ | `battle` | `BattleEvent` | PK Battle start / status | `battleId`, `status`, `battleDuration`, `teams` |
173
+ | `battleArmies` | `BattleArmiesEvent` | **PK score update + per-host MVP breakdown** | `battleId`, `teams`, `hosts[]`, `secsRemaining`, `serverTsMs`, `matchId`, `sessionId` |
174
+ | `battleItemCard` | `BattleItemCardEvent` | **x2/x3 boosters, gloves, mist, thunder, extra-time, match-guide** | `effect`, `multiplier`, `senderUserId`, `iconUrl`, `iconKey`, `accentColor`, … |
175
+ | `subscribe` | `SubscribeEvent` | New subscriber | `user`, `subMonth` |
176
+ | `emoteChat` | `EmoteChatEvent` | Emote in chat | `user`, `emoteId`, `emoteUrl` |
177
+ | `envelope` | `EnvelopeEvent` | Treasure chest | `diamondCount` |
178
+ | `question` | `QuestionEvent` | Q&A question | `user`, `questionText` |
179
+ | `control` | `ControlEvent` | Stream control | `action` (3 = ended) |
180
+ | `room` | `RoomEvent` | Room status | `status` |
181
+ | `liveIntro` | `LiveIntroEvent` | Stream intro | `roomId`, `title` |
182
+ | `rankUpdate` | `RankUpdateEvent` | Hourly / weekly rank | `rankType`, `rankList` |
183
+ | `linkMic` | `LinkMicEvent` | Link Mic guest action | `action`, `users` |
184
+ | `unknown` | `UnknownEvent` | Unrecognized | `method` |
185
+
186
+ ### Connection Events
187
+
188
+ | Event | Callback | Description |
189
+ |-------|----------|-------------|
190
+ | `connected` | `() => void` | Connected to stream |
191
+ | `disconnected` | `(code, reason) => void` | Disconnected |
192
+ | `roomInfo` | `(info: RoomInfo) => void` | Room info |
193
+ | `error` | `(error: Error) => void` | Error |
194
+
195
+ ---
196
+
197
+ ## 🥊 PK Battles
198
+
199
+ This SDK fully parses TikTok PK (Player-vs-Killer) battle protobufs, including
200
+ **multi-guest** matches with up to 4 hosts per side and per-gifter MVP scores.
201
+
202
+ ### `battleArmies` — score updates during a PK
203
+
204
+ Emitted every few seconds while a PK is active. The new `hosts[]` array gives
205
+ you the **per-host breakdown** with each gifter's contribution sorted **MVP
206
+ first** (highest score → lowest):
207
+
208
+ ```typescript
209
+ live.on('battleArmies', (e) => {
210
+ console.log(`PK ${e.battleId} — ${e.secsRemaining}s remaining`);
211
+
212
+ for (const host of e.hosts ?? []) {
213
+ console.log(` Host ${host.hostUserId} — total ${host.teamTotalScore}`);
214
+
215
+ // contributors[0] = MVP (highest gifter)
216
+ for (const [i, c] of host.contributors.slice(0, 3).entries()) {
217
+ console.log(` #${i + 1} ${c.nickname || c.userId}: ${c.score}`);
218
+ }
219
+ }
220
+ });
221
+ ```
222
+
223
+ **Per-host fields:**
224
+
225
+ | Field | Type | Description |
226
+ |-------|------|-------------|
227
+ | `hostUserId` | `string` | TikTok userId of this side's host |
228
+ | `teamTotalScore` | `number` | Total diamonds for this side |
229
+ | `teamIdx` | `number` | Side index (0 = left, 1 = right, …) |
230
+ | `contributors[]` | `BattleContributor[]` | Per-gifter breakdown, **sorted MVP first** |
231
+
232
+ **Per-contributor fields:**
233
+
234
+ | Field | Type | Description |
235
+ |-------|------|-------------|
236
+ | `userId` | `string` | Gifter's TikTok userId |
237
+ | `score` | `number` | Diamond score contributed |
238
+ | `nickname` | `string` | Display nickname (may be empty) |
239
+
240
+ **Timer & match identity:**
241
+
242
+ | Field | Type | Description |
243
+ |-------|------|-------------|
244
+ | `battleId` | `string` | Battle session tag |
245
+ | `matchId` | `string` | Stable match ID across multi-round PK |
246
+ | `sessionId` | `string` | Per-round session ID |
247
+ | `secsRemaining` | `number` | Seconds left, computed from **TikTok server clock** (no VPS drift) |
248
+ | `serverTsMs` | `number` | TikTok server-side ms epoch at frame emit |
249
+ | `durationSec` | `number` | Total battle duration |
250
+ | `endTimeMs` | `number` | Battle end ms epoch (alias for `battleStartMs + duration*1000`) |
251
+
252
+ ### `battleItemCard` — boosters / power-ups during a PK
253
+
254
+ Fired when a gifter activates a special card: x2/x3 multipliers, gloves
255
+ (crit), mist, thunder, extra-time, or match-guide. Includes the **card art**
256
+ URL straight from TikTok's CDN — drop it into an OBS overlay as-is.
257
+
258
+ ```typescript
259
+ live.on('battleItemCard', (e) => {
260
+ console.log(`${e.senderNickname} activated ${e.effect}`);
261
+
262
+ if (e.multiplier > 0) {
263
+ console.log(` x${e.multiplier} multiplier for ${e.durationSec}s`);
264
+ }
265
+
266
+ // Use straight in an <img> tag — TikTok CDN
267
+ console.log(` icon: ${e.iconUrl}`);
268
+ console.log(` accent: ${e.accentColor}`);
269
+ });
270
+ ```
271
+
272
+ **Fields:**
273
+
274
+ | Field | Type | Description |
275
+ |-------|------|-------------|
276
+ | `battleId` | `string` | The PK this card belongs to |
277
+ | `cardType` | `number` | 2=gloves, 3=mist, 4=match_guide, 11=x3, … |
278
+ | `effect` | `string` | `'gloves'` \| `'mist'` \| `'booster_x2'` \| `'booster_x3'` \| `'match_guide'` \| `'thunder'` \| `'extra_time'` \| raw key |
279
+ | `effectKey` | `string` | Raw TikTok resource key |
280
+ | `multiplier` | `number` | 2 or 3 for boosters, else 0 |
281
+ | `senderUserId` | `string` | Gifter who activated it |
282
+ | `senderNickname` | `string` | Display name of the sender |
283
+ | `senderUniqueId` | `string` | Username (`@handle`) of the sender |
284
+ | `senderAvatarUrl` | `string` | Sender's avatar (CDN URL) |
285
+ | `activatedAtSec` | `number` | Unix seconds when activated |
286
+ | `durationSec` | `number` | Active duration |
287
+ | `endsAtSec` | `number` | Unix seconds when it ends |
288
+ | `commentTemplate` | `string` | e.g. `"{0:user} sent 1 magic mist"` |
289
+ | `iconUrl` | `string` | Full TikTok CDN URL for the card art (webp/jpeg) |
290
+ | `iconKey` | `string` | Short id, e.g. `card_mist_v3`, `card_crit_v3`, `top3_buffer` |
291
+ | `accentColor` | `string` | Hex color, e.g. `#BCD9E0` (mist blue), `#E0D4BC` (gloves tan) |
292
+
293
+ ### `battle` — PK lifecycle events
294
+
295
+ Fired on start / status change / end. Use for PK ON/OFF banners.
296
+
297
+ ```typescript
298
+ live.on('battle', (e) => {
299
+ // status: 1=ACTIVE, 2=STARTING, 3=ENDED, 4=PREPARING
300
+ if (e.status === 1) console.log('PK started');
301
+ if (e.status === 3) console.log('PK ended');
302
+ });
303
+ ```
304
+
305
+ ---
306
+
307
+ ## API Reference
308
+
309
+ ### `new TikTokLive(options)`
310
+
311
+ | Option | Type | Default | Description |
312
+ |--------|------|---------|-------------|
313
+ | `uniqueId` | `string` | — | TikTok username (without @) |
314
+ | `apiKey` | `string` | — | **Required.** API key from [tik.tools](https://tik.tools) |
315
+ | `signServerUrl` | `string` | `https://api.tik.tools` | Sign server URL |
316
+ | `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
317
+ | `maxReconnectAttempts` | `number` | `5` | Max reconnect attempts |
318
+ | `heartbeatInterval` | `number` | `10000` | Heartbeat interval (ms) |
319
+ | `roomId` | `string` | — | Pre-resolved room ID (skips page fetch when paired with `sessionId`) |
320
+ | `sessionId` | `string` | — | Pre-resolved `ttwid` cookie (skips page fetch when paired with `roomId`) |
321
+ | `debug` | `boolean` | `false` | Debug logging |
322
+
323
+ ### Methods
324
+
325
+ | Method | Returns | Description |
326
+ |--------|---------|-------------|
327
+ | `connect()` | `Promise<void>` | Connect to livestream |
328
+ | `disconnect()` | `void` | Disconnect |
329
+ | `getStreamUrl()` | `Promise<StreamInfo>` | Get FLV/HLS playback URLs for the live video |
330
+ | `connected` | `boolean` | Connection status |
331
+ | `eventCount` | `number` | Total events received |
332
+ | `roomId` | `string` | Current room ID |
333
+
334
+ ---
335
+
336
+ ## Rate Limits
337
+
338
+ All API requests require an API key. Get yours at [tik.tools](https://tik.tools).
339
+
340
+ | Tier | Price (USD / week) | WS Connections | Rate limit |
341
+ |------|--------------------|----------------|------------|
342
+ | **Free** | $0 | 1 | 5 req/min |
343
+ | **Basic** | $9 | 25 | 30 req/min |
344
+ | **Pro** | $19 | 100 | 120 req/min |
345
+ | **Ultra** | $58 | 250 | 600 req/min |
346
+
347
+ The SDK calls the sign server **once per connection**, then stays connected
348
+ via WebSocket. A free key is sufficient for most use cases.
349
+
350
+ ---
351
+
352
+ ## Examples
353
+
354
+ ### Chat Bot
355
+
356
+ ```typescript
357
+ import { TikTokLive } from '@tiktool/live';
358
+
359
+ const live = new TikTokLive({
360
+ uniqueId: 'streamer_name',
361
+ apiKey: 'YOUR_API_KEY',
362
+ });
363
+
364
+ live.on('chat', (e) => {
365
+ if (e.comment.toLowerCase() === '!hello') {
366
+ console.log(`Hello, ${e.user.nickname}!`);
367
+ }
368
+ });
369
+
370
+ live.on('gift', (e) => {
371
+ if (e.repeatEnd) {
372
+ console.log(`${e.user.uniqueId} sent ${e.repeatCount}x ${e.giftName} (${e.diamondCount * e.repeatCount} diamonds)`);
373
+ }
374
+ });
375
+
376
+ await live.connect();
377
+ ```
378
+
379
+ ### Live PK Battle Dashboard
380
+
381
+ ```typescript
382
+ import { TikTokLive } from '@tiktool/live';
383
+
384
+ const live = new TikTokLive({ uniqueId: 'streamer_name', apiKey: 'YOUR_API_KEY' });
385
+
386
+ live.on('battle', e => {
387
+ if (e.status === 1) console.log('🥊 PK STARTED');
388
+ if (e.status === 3) console.log('🏁 PK ENDED');
389
+ });
390
+
391
+ live.on('battleArmies', e => {
392
+ console.log(`\n⏱ ${e.secsRemaining}s remaining`);
393
+ for (const host of e.hosts ?? []) {
394
+ const top = host.contributors[0];
395
+ console.log(` ${host.hostUserId}: ${host.teamTotalScore} ` +
396
+ `(MVP: ${top?.nickname || top?.userId || '—'} ${top?.score ?? 0})`);
397
+ }
398
+ });
399
+
400
+ live.on('battleItemCard', e => {
401
+ const tag = e.multiplier > 0 ? `x${e.multiplier} BOOSTER` : e.effect.toUpperCase();
402
+ console.log(`💥 ${e.senderNickname} → ${tag} (${e.durationSec}s)`);
403
+ });
404
+
405
+ await live.connect();
406
+ ```
407
+
408
+ ### OBS Overlay
409
+
410
+ ```typescript
411
+ import { TikTokLive } from '@tiktool/live';
412
+ import { WebSocketServer } from 'ws';
413
+
414
+ const wss = new WebSocketServer({ port: 8080 });
415
+ const live = new TikTokLive({
416
+ uniqueId: 'streamer_name',
417
+ apiKey: 'YOUR_API_KEY',
418
+ });
419
+
420
+ live.on('event', (event) => {
421
+ for (const client of wss.clients) {
422
+ client.send(JSON.stringify(event));
423
+ }
424
+ });
425
+
426
+ await live.connect();
427
+ console.log('Forwarding events to ws://localhost:8080');
428
+ ```
429
+
430
+ ### HLS / FLV Playback URL
431
+
432
+ ```typescript
433
+ const info = await live.getStreamUrl();
434
+ console.log('HLS:', info.hlsPullUrl);
435
+ console.log('FLV:', info.flvPullUrl);
436
+ // Or pick a specific quality
437
+ console.log('FULL_HD1 HLS:', info.streamUrls.FULL_HD1?.hls);
438
+ ```
439
+
440
+ ---
441
+
442
+ ## TypeScript
443
+
444
+ Full TypeScript support with type inference:
445
+
446
+ ```typescript
447
+ import {
448
+ TikTokLive,
449
+ ChatEvent,
450
+ GiftEvent,
451
+ BattleArmiesEvent,
452
+ BattleItemCardEvent,
453
+ BattleHost,
454
+ BattleContributor,
455
+ } from '@tiktool/live';
456
+
457
+ const live = new TikTokLive({
458
+ uniqueId: 'username',
459
+ apiKey: 'YOUR_API_KEY',
460
+ });
461
+
462
+ live.on('chat', (event: ChatEvent) => {
463
+ const username: string = event.user.uniqueId;
464
+ const message: string = event.comment;
465
+ });
466
+
467
+ live.on('gift', (event: GiftEvent) => {
468
+ const diamonds: number = event.diamondCount;
469
+ const isCombo: boolean = event.combo;
470
+ });
471
+
472
+ live.on('battleArmies', (event: BattleArmiesEvent) => {
473
+ for (const host of event.hosts ?? []) {
474
+ const mvp: BattleContributor | undefined = host.contributors[0];
475
+ if (mvp) console.log(mvp.nickname, mvp.score);
476
+ }
477
+ });
478
+
479
+ live.on('battleItemCard', (event: BattleItemCardEvent) => {
480
+ if (event.multiplier > 0) {
481
+ console.log(`x${event.multiplier} buff active for ${event.durationSec}s`);
482
+ }
483
+ });
484
+ ```
485
+
486
+ ---
487
+
488
+ ## Changelog
489
+
490
+ ### 2.8.0 (2026-05-19)
491
+
492
+ - **NEW** `battleItemCard` event — x2/x3 boosters, gloves, mist, thunder,
493
+ extra-time, match-guide. Includes `iconUrl`, `iconKey`, `accentColor` for
494
+ drop-in overlay use.
495
+ - **NEW** `BattleArmiesEvent.hosts[]` — multi-guest PK breakdown with per-host
496
+ `contributors[]` sorted **MVP first**.
497
+ - **NEW** `matchId`, `sessionId`, `serverTsMs`, `sessionTag`, `secsRemaining`
498
+ fields on `BattleArmiesEvent`.
499
+ - Verified live against multi-guest battles 2026-05-19.
500
+
501
+ ---
502
+
503
+ ## License
504
+
505
+ MIT © [tiktool](https://tik.tools)