@tiktool/live 2.12.3 → 2.13.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 +31 -30
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +22 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Real-time chat, gifts, viewers, **PK battles with MVP breakdown**, **x2/x3 boost
|
|
|
23
23
|
npm install @tiktool/live
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Get
|
|
26
|
+
Get an API key at [tik.tools](https://tik.tools/pricing) (7-day free evaluation).
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
import { TikTokLive } from '@tiktool/live';
|
|
@@ -69,7 +69,7 @@ await live.connect();
|
|
|
69
69
|
|
|
70
70
|
There are **two ways** to receive TikTok LIVE events from `tik.tools`. Pick based on scale and IP-exposure tolerance.
|
|
71
71
|
|
|
72
|
-
### Mode 1
|
|
72
|
+
### Mode 1 - Direct (default, this SDK)
|
|
73
73
|
|
|
74
74
|
```
|
|
75
75
|
Your App ──signs URL via──▶ tik.tools ──returns signed URL──▶ Your App
|
|
@@ -78,7 +78,7 @@ There are **two ways** to receive TikTok LIVE events from `tik.tools`. Pick base
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
- The signed WebSocket is opened from your runtime. TikTok sees the network identity of the host that runs the SDK.
|
|
81
|
-
- Lowest per-stream cost
|
|
81
|
+
- Lowest per-stream cost - only the initial `ws_credentials` call is billed against your API key.
|
|
82
82
|
- Suited to **low- to mid-volume** workloads from a single host.
|
|
83
83
|
- High-volume setups should prefer Mode 2 for predictable, centrally-managed egress.
|
|
84
84
|
|
|
@@ -97,7 +97,7 @@ If you set the optional `proxy` field, install the standard agent:
|
|
|
97
97
|
npm install https-proxy-agent
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
### Mode 2
|
|
100
|
+
### Mode 2 - Relayed (via TikTools managed edge)
|
|
101
101
|
|
|
102
102
|
```
|
|
103
103
|
Your App ◀──── wss://api.tik.tools/?... ──── tik.tools
|
|
@@ -108,10 +108,10 @@ npm install https-proxy-agent
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
- The TikTools service handles the upstream TikTok session and forwards decoded events over a single WebSocket to your client.
|
|
111
|
-
- Single, stable egress
|
|
111
|
+
- Single, stable egress - your application connects only to `api.tik.tools`.
|
|
112
112
|
- Recommended for **production scale**, multi-tenant deployments, or any environment that already centralizes outbound traffic.
|
|
113
113
|
|
|
114
|
-
**Easiest
|
|
114
|
+
**Easiest - same SDK, one option:**
|
|
115
115
|
|
|
116
116
|
```typescript
|
|
117
117
|
import { TikTokLive } from '@tiktool/live';
|
|
@@ -131,7 +131,7 @@ await live.connect();
|
|
|
131
131
|
|
|
132
132
|
Event names + payload shapes are **identical to Direct mode**. Switch back and forth by toggling `mode`.
|
|
133
133
|
|
|
134
|
-
**Advanced
|
|
134
|
+
**Advanced - raw WebSocket (no SDK, your own client):**
|
|
135
135
|
|
|
136
136
|
```typescript
|
|
137
137
|
import WebSocket from 'ws';
|
|
@@ -216,7 +216,7 @@ live.on('event', (event) => {
|
|
|
216
216
|
This SDK fully parses TikTok PK (Player-vs-Killer) battle protobufs, including
|
|
217
217
|
**multi-guest** matches with up to 4 hosts per side and per-gifter MVP scores.
|
|
218
218
|
|
|
219
|
-
### `battleArmies`
|
|
219
|
+
### `battleArmies` - score updates during a PK
|
|
220
220
|
|
|
221
221
|
Emitted every few seconds while a PK is active. The new `hosts[]` array gives
|
|
222
222
|
you the **per-host breakdown** with each gifter's contribution sorted **MVP
|
|
@@ -224,10 +224,10 @@ first** (highest score → lowest):
|
|
|
224
224
|
|
|
225
225
|
```typescript
|
|
226
226
|
live.on('battleArmies', (e) => {
|
|
227
|
-
console.log(`PK ${e.battleId}
|
|
227
|
+
console.log(`PK ${e.battleId} - ${e.secsRemaining}s remaining`);
|
|
228
228
|
|
|
229
229
|
for (const host of e.hosts ?? []) {
|
|
230
|
-
console.log(` Host ${host.hostUserId}
|
|
230
|
+
console.log(` Host ${host.hostUserId} - total ${host.teamTotalScore}`);
|
|
231
231
|
|
|
232
232
|
// contributors[0] = MVP (highest gifter)
|
|
233
233
|
for (const [i, c] of host.contributors.slice(0, 3).entries()) {
|
|
@@ -266,11 +266,11 @@ live.on('battleArmies', (e) => {
|
|
|
266
266
|
| `durationSec` | `number` | Total battle duration |
|
|
267
267
|
| `endTimeMs` | `number` | Battle end ms epoch (alias for `battleStartMs + duration*1000`) |
|
|
268
268
|
|
|
269
|
-
### `battleItemCard`
|
|
269
|
+
### `battleItemCard` - boosters / power-ups during a PK
|
|
270
270
|
|
|
271
271
|
Fired when a gifter activates a special card: x2/x3 multipliers, gloves
|
|
272
272
|
(crit), mist, thunder, extra-time, or match-guide. Includes the **card art**
|
|
273
|
-
URL straight from TikTok's CDN
|
|
273
|
+
URL straight from TikTok's CDN - drop it into an OBS overlay as-is.
|
|
274
274
|
|
|
275
275
|
```typescript
|
|
276
276
|
live.on('battleItemCard', (e) => {
|
|
@@ -280,7 +280,7 @@ live.on('battleItemCard', (e) => {
|
|
|
280
280
|
console.log(` x${e.multiplier} multiplier for ${e.durationSec}s`);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
// Use straight in an <img> tag
|
|
283
|
+
// Use straight in an <img> tag - TikTok CDN
|
|
284
284
|
console.log(` icon: ${e.iconUrl}`);
|
|
285
285
|
console.log(` accent: ${e.accentColor}`);
|
|
286
286
|
});
|
|
@@ -307,7 +307,7 @@ live.on('battleItemCard', (e) => {
|
|
|
307
307
|
| `iconKey` | `string` | Short id, e.g. `card_mist_v3`, `card_crit_v3`, `top3_buffer` |
|
|
308
308
|
| `accentColor` | `string` | Hex color, e.g. `#BCD9E0` (mist blue), `#E0D4BC` (gloves tan) |
|
|
309
309
|
|
|
310
|
-
### `battle`
|
|
310
|
+
### `battle` - PK lifecycle events
|
|
311
311
|
|
|
312
312
|
Fired on start / status change / end. Use for PK ON/OFF banners.
|
|
313
313
|
|
|
@@ -327,15 +327,15 @@ live.on('battle', (e) => {
|
|
|
327
327
|
|
|
328
328
|
| Option | Type | Default | Description |
|
|
329
329
|
|--------|------|---------|-------------|
|
|
330
|
-
| `uniqueId` | `string` |
|
|
331
|
-
| `apiKey` | `string` |
|
|
330
|
+
| `uniqueId` | `string` | - | TikTok username (without @) |
|
|
331
|
+
| `apiKey` | `string` | - | **Required.** API key from [tik.tools](https://tik.tools) |
|
|
332
332
|
| `signServerUrl` | `string` | `https://api.tik.tools` | Sign server URL |
|
|
333
333
|
| `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
|
|
334
334
|
| `maxReconnectAttempts` | `number` | `5` | Max reconnect attempts |
|
|
335
335
|
| `heartbeatInterval` | `number` | `10000` | Heartbeat interval (ms) |
|
|
336
|
-
| `roomId` | `string` |
|
|
337
|
-
| `sessionId` | `string` |
|
|
338
|
-
| `proxy` | `string` |
|
|
336
|
+
| `roomId` | `string` | - | Pre-resolved room ID (skips page fetch when paired with `sessionId`) |
|
|
337
|
+
| `sessionId` | `string` | - | Pre-resolved `ttwid` cookie (skips page fetch when paired with `roomId`) |
|
|
338
|
+
| `proxy` | `string` | - | HTTP(S) proxy URL for Direct mode (e.g. `http://USER:PASS@host:port`). Requires `https-proxy-agent`. |
|
|
339
339
|
| `mode` | `'direct' \| 'relayed'` | `'direct'` | Connection mode. See [Connection Modes](#connection-modes). |
|
|
340
340
|
| `debug` | `boolean` | `false` | Debug logging |
|
|
341
341
|
|
|
@@ -393,7 +393,7 @@ curl -H "X-Api-Key: YOUR_KEY" \
|
|
|
393
393
|
"https://api.tik.tools/webcast/user_profile?unique_id=anyuser"
|
|
394
394
|
```
|
|
395
395
|
|
|
396
|
-
Returns full profile JSON. Pro tier and above. Cached server-side for 24h
|
|
396
|
+
Returns full profile JSON. Pro tier and above. Cached server-side for 24h - repeated lookups are free and instant.
|
|
397
397
|
|
|
398
398
|
---
|
|
399
399
|
|
|
@@ -403,16 +403,17 @@ All API requests require an API key. Get yours at [tik.tools](https://tik.tools)
|
|
|
403
403
|
|
|
404
404
|
| Tier | Price | Requests/day | WS Connections | Notes |
|
|
405
405
|
|------|-------|--------------|----------------|-------|
|
|
406
|
-
| **
|
|
407
|
-
| **
|
|
408
|
-
| **
|
|
409
|
-
| **
|
|
406
|
+
| **Sandbox** | Free, 7-day evaluation | 5,000 | 3 (2h per WS) | Masked leaderboards. No datacenter proxies - calls run from your own IP. Refused after 7 days from account creation (WS close 4401, REST 403). |
|
|
407
|
+
| **Basic** | $7/wk or $19/mo | 10,000 | 20 (8h per WS) | Datacenter proxies, bulk live check |
|
|
408
|
+
| **Pro** | $15/wk or $49/mo | 75,000 | 50 (12h per WS) | Unmasked leaderboards · CAPTCHA Solver · Feed Discovery · priority routing |
|
|
409
|
+
| **Ultra** | $45/wk or $149/mo | 300,000 | 250 (24h per WS) | Unmasked leagues · priority chat support |
|
|
410
|
+
| **Agency** | $119/wk or $399/mo | 1,000,000 | 500 (24h per WS) + Firehose | Everything in Ultra + **Live Gifter Firehose WS** (region/league/global + min-diamond filters) + VIP Telegram alerts + VIP Web Vault |
|
|
410
411
|
|
|
411
412
|
The SDK calls the sign server **once per connection**, then stays connected
|
|
412
|
-
via WebSocket.
|
|
413
|
-
|
|
413
|
+
via WebSocket. Sandbox, the free 7-day evaluation, caps each WebSocket at 2 hours;
|
|
414
|
+
paid plans allow 8 to 24 hours per connection.
|
|
414
415
|
|
|
415
|
-
### Live Gifter Firehose
|
|
416
|
+
### Live Gifter Firehose - Agency
|
|
416
417
|
|
|
417
418
|
Real-time gift event stream from Dragonfly fan-out. Filter by region, league,
|
|
418
419
|
or globally; cap by minimum diamond threshold. Mid-stream filter updates
|
|
@@ -474,7 +475,7 @@ live.on('battleArmies', e => {
|
|
|
474
475
|
for (const host of e.hosts ?? []) {
|
|
475
476
|
const top = host.contributors[0];
|
|
476
477
|
console.log(` ${host.hostUserId}: ${host.teamTotalScore} ` +
|
|
477
|
-
`(MVP: ${top?.nickname || top?.userId || '
|
|
478
|
+
`(MVP: ${top?.nickname || top?.userId || '-'} ${top?.score ?? 0})`);
|
|
478
479
|
}
|
|
479
480
|
});
|
|
480
481
|
|
|
@@ -570,10 +571,10 @@ live.on('battleItemCard', (event: BattleItemCardEvent) => {
|
|
|
570
571
|
|
|
571
572
|
### 2.8.0 (2026-05-19)
|
|
572
573
|
|
|
573
|
-
- **NEW** `battleItemCard` event
|
|
574
|
+
- **NEW** `battleItemCard` event - x2/x3 boosters, gloves, mist, thunder,
|
|
574
575
|
extra-time, match-guide. Includes `iconUrl`, `iconKey`, `accentColor` for
|
|
575
576
|
drop-in overlay use.
|
|
576
|
-
- **NEW** `BattleArmiesEvent.hosts[]`
|
|
577
|
+
- **NEW** `BattleArmiesEvent.hosts[]` - multi-guest PK breakdown with per-host
|
|
577
578
|
`contributors[]` sorted **MVP first**.
|
|
578
579
|
- **NEW** `matchId`, `sessionId`, `serverTsMs`, `sessionTag`, `secsRemaining`
|
|
579
580
|
fields on `BattleArmiesEvent`.
|
package/dist/index.d.mts
CHANGED
|
@@ -45,7 +45,18 @@ interface GiftEvent extends BaseEvent {
|
|
|
45
45
|
repeatEnd: boolean;
|
|
46
46
|
combo: boolean;
|
|
47
47
|
giftType: number;
|
|
48
|
+
/**
|
|
49
|
+
* TikTok's combo group id: the same on every frame of one combo. Follow a
|
|
50
|
+
* combo with (user.id, giftId, groupId) and take its highest repeatCount.
|
|
51
|
+
*/
|
|
48
52
|
groupId: string;
|
|
53
|
+
/**
|
|
54
|
+
* Who received the gift, as a full-precision string: the host, or the
|
|
55
|
+
* guest in a multi-guest LIVE. '' when the frame did not state it.
|
|
56
|
+
*/
|
|
57
|
+
toUserId: string;
|
|
58
|
+
/** Same as `toUserId`; matches the field name used by the relay events. */
|
|
59
|
+
receiverUserId: string;
|
|
49
60
|
}
|
|
50
61
|
interface SocialEvent extends BaseEvent {
|
|
51
62
|
type: 'social';
|
|
@@ -55,6 +66,7 @@ interface SocialEvent extends BaseEvent {
|
|
|
55
66
|
interface RoomUserSeqEvent extends BaseEvent {
|
|
56
67
|
type: 'roomUserSeq';
|
|
57
68
|
viewerCount: number;
|
|
69
|
+
/** TikTok's running viewer total for this LIVE; restarts with a new LIVE. */
|
|
58
70
|
totalViewers: number;
|
|
59
71
|
}
|
|
60
72
|
interface BattleTeamUser {
|
|
@@ -230,6 +242,12 @@ interface TikTokLiveEvents {
|
|
|
230
242
|
envelope: (event: EnvelopeEvent) => void;
|
|
231
243
|
question: (event: QuestionEvent) => void;
|
|
232
244
|
control: (event: ControlEvent) => void;
|
|
245
|
+
/** The host ended the LIVE (TikTok control action 3, or the relay's confirmed end). */
|
|
246
|
+
streamEnd: (event: {
|
|
247
|
+
type: 'streamEnd';
|
|
248
|
+
uniqueId: string;
|
|
249
|
+
reason: string;
|
|
250
|
+
}) => void;
|
|
233
251
|
room: (event: RoomEvent) => void;
|
|
234
252
|
liveIntro: (event: LiveIntroEvent) => void;
|
|
235
253
|
rankUpdate: (event: RankUpdateEvent) => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -45,7 +45,18 @@ interface GiftEvent extends BaseEvent {
|
|
|
45
45
|
repeatEnd: boolean;
|
|
46
46
|
combo: boolean;
|
|
47
47
|
giftType: number;
|
|
48
|
+
/**
|
|
49
|
+
* TikTok's combo group id: the same on every frame of one combo. Follow a
|
|
50
|
+
* combo with (user.id, giftId, groupId) and take its highest repeatCount.
|
|
51
|
+
*/
|
|
48
52
|
groupId: string;
|
|
53
|
+
/**
|
|
54
|
+
* Who received the gift, as a full-precision string: the host, or the
|
|
55
|
+
* guest in a multi-guest LIVE. '' when the frame did not state it.
|
|
56
|
+
*/
|
|
57
|
+
toUserId: string;
|
|
58
|
+
/** Same as `toUserId`; matches the field name used by the relay events. */
|
|
59
|
+
receiverUserId: string;
|
|
49
60
|
}
|
|
50
61
|
interface SocialEvent extends BaseEvent {
|
|
51
62
|
type: 'social';
|
|
@@ -55,6 +66,7 @@ interface SocialEvent extends BaseEvent {
|
|
|
55
66
|
interface RoomUserSeqEvent extends BaseEvent {
|
|
56
67
|
type: 'roomUserSeq';
|
|
57
68
|
viewerCount: number;
|
|
69
|
+
/** TikTok's running viewer total for this LIVE; restarts with a new LIVE. */
|
|
58
70
|
totalViewers: number;
|
|
59
71
|
}
|
|
60
72
|
interface BattleTeamUser {
|
|
@@ -230,6 +242,12 @@ interface TikTokLiveEvents {
|
|
|
230
242
|
envelope: (event: EnvelopeEvent) => void;
|
|
231
243
|
question: (event: QuestionEvent) => void;
|
|
232
244
|
control: (event: ControlEvent) => void;
|
|
245
|
+
/** The host ended the LIVE (TikTok control action 3, or the relay's confirmed end). */
|
|
246
|
+
streamEnd: (event: {
|
|
247
|
+
type: 'streamEnd';
|
|
248
|
+
uniqueId: string;
|
|
249
|
+
reason: string;
|
|
250
|
+
}) => void;
|
|
233
251
|
room: (event: RoomEvent) => void;
|
|
234
252
|
liveIntro: (event: LiveIntroEvent) => void;
|
|
235
253
|
rankUpdate: (event: RankUpdateEvent) => void;
|
package/dist/index.js
CHANGED
|
@@ -1626,12 +1626,18 @@ function parseWebcastMessage(method, payload) {
|
|
|
1626
1626
|
}
|
|
1627
1627
|
}
|
|
1628
1628
|
let toUserId = "";
|
|
1629
|
+
const receiverBuf = getBytes(f, 8);
|
|
1630
|
+
if (receiverBuf) {
|
|
1631
|
+
const r = parseUser(receiverBuf);
|
|
1632
|
+
if (r.id && r.id !== "0") toUserId = r.id;
|
|
1633
|
+
}
|
|
1629
1634
|
const extraBuf = getBytes(f, 23);
|
|
1630
|
-
if (extraBuf) {
|
|
1631
|
-
const
|
|
1632
|
-
toUserId =
|
|
1635
|
+
if (!toUserId && extraBuf) {
|
|
1636
|
+
const id = getIntStr(decodeProto(extraBuf), 8);
|
|
1637
|
+
if (id !== "0") toUserId = id;
|
|
1633
1638
|
}
|
|
1634
|
-
const
|
|
1639
|
+
const rawGroup = getIntStr(f, 11);
|
|
1640
|
+
const groupId = getStr(f, 11) || (rawGroup !== "0" ? rawGroup : "");
|
|
1635
1641
|
return {
|
|
1636
1642
|
...base,
|
|
1637
1643
|
type: "gift",
|
|
@@ -1643,7 +1649,9 @@ function parseWebcastMessage(method, payload) {
|
|
|
1643
1649
|
repeatEnd,
|
|
1644
1650
|
combo: repeatCount > 1 && !repeatEnd,
|
|
1645
1651
|
giftType,
|
|
1646
|
-
groupId
|
|
1652
|
+
groupId,
|
|
1653
|
+
toUserId,
|
|
1654
|
+
receiverUserId: toUserId
|
|
1647
1655
|
};
|
|
1648
1656
|
}
|
|
1649
1657
|
// Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
|
|
@@ -1669,7 +1677,7 @@ function parseWebcastMessage(method, payload) {
|
|
|
1669
1677
|
// Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
|
|
1670
1678
|
case "WebcastRoomUserSeqMessage": {
|
|
1671
1679
|
const viewerCount = getInt(f, 3) || getInt(f, 2);
|
|
1672
|
-
const totalViewers = getInt(f,
|
|
1680
|
+
const totalViewers = getInt(f, 7) || viewerCount;
|
|
1673
1681
|
return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
|
|
1674
1682
|
}
|
|
1675
1683
|
// Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
|
|
@@ -2183,6 +2191,11 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
|
|
|
2183
2191
|
if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
|
|
2184
2192
|
this.intentionalClose = false;
|
|
2185
2193
|
if (this.mode === "relayed") {
|
|
2194
|
+
if (this._presetRoomId) {
|
|
2195
|
+
console.warn(
|
|
2196
|
+
`[TikTokLive] roomId=${this._presetRoomId} is ignored in mode:'relayed' - the managed relay resolves the room itself. Use mode:'direct' with roomId to pin an exact room (resolve it first via /webcast/bulk_live_check).`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2186
2199
|
return this._connectRelayed();
|
|
2187
2200
|
}
|
|
2188
2201
|
let ttwid = this._presetSessionId;
|
|
@@ -2618,6 +2631,9 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
|
|
|
2618
2631
|
}
|
|
2619
2632
|
this.emit("event", evt);
|
|
2620
2633
|
this.emit(evt.type, evt);
|
|
2634
|
+
if (evt.type === "control" && evt.action === 3) {
|
|
2635
|
+
this.emit("streamEnd", { type: "streamEnd", uniqueId: this.uniqueId, reason: "creator_offline" });
|
|
2636
|
+
}
|
|
2621
2637
|
}
|
|
2622
2638
|
}
|
|
2623
2639
|
} catch {
|