@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 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 your free API key at [tik.tools](https://tik.tools)
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 — Direct (default, this SDK)
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 — only the initial `ws_credentials` call is billed against your API key.
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 — Relayed (via TikTools managed edge)
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 — your application connects only to `api.tik.tools`.
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 — same SDK, one option:**
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 — raw WebSocket (no SDK, your own client):**
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` — score updates during a PK
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} — ${e.secsRemaining}s remaining`);
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} — total ${host.teamTotalScore}`);
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` — boosters / power-ups during a PK
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 — drop it into an OBS overlay as-is.
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 — TikTok CDN
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` — PK lifecycle events
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` | — | TikTok username (without @) |
331
- | `apiKey` | `string` | — | **Required.** API key from [tik.tools](https://tik.tools) |
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` | — | 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`. |
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 — repeated lookups are free and instant.
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
- | **Community** | Free forever | 2,500 | 15 (2h per WS) | Masked leaderboards. No datacenter proxies — calls run from your own IP. |
407
- | **Pro** | from $59/mo | 75,000 | 50 (8h) | Unmasked leaderboards · CAPTCHA Solver · Feed Discovery · 5 AI caption streams · priority routing |
408
- | **Ultra** | from $219/mo | 300,000 | 250 (8h) | Unmasked leagues · 20 AI caption streams · 99.5% uptime SLA · priority chat support |
409
- | **Global Agency** | $549/mo | 300,000 | 500 + Firehose | Everything in Ultra + **Live Gifter Firehose WS** (region/league/global + min-diamond filters) + VIP Telegram alerts + VIP Web Vault |
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. The free Community tier caps each WebSocket at 2 hours and
413
- is sufficient for most use cases.
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 — Global Agency
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 || '—'} ${top?.score ?? 0})`);
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 — x2/x3 boosters, gloves, mist, thunder,
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[]` — multi-guest PK breakdown with per-host
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 ef = decodeProto(extraBuf);
1632
- toUserId = String(getInt(ef, 8));
1635
+ if (!toUserId && extraBuf) {
1636
+ const id = getIntStr(decodeProto(extraBuf), 8);
1637
+ if (id !== "0") toUserId = id;
1633
1638
  }
1634
- const groupId = toUserId || getStr(f, 11);
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, 1) || viewerCount;
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 {