@tiktool/live 1.6.5 → 2.0.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/dist/index.mjs CHANGED
@@ -1,11 +1,32 @@
1
- // src/client.ts
2
- import { EventEmitter } from "events";
3
- import * as http from "http";
4
- import * as https from "https";
5
- import * as zlib from "zlib";
6
- import WebSocket from "ws";
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
7
 
8
8
  // src/proto.ts
9
+ var encoder = new TextEncoder();
10
+ var decoder = new TextDecoder();
11
+ function concatBytes(...arrays) {
12
+ let totalLength = 0;
13
+ for (const arr of arrays) totalLength += arr.length;
14
+ const result = new Uint8Array(totalLength);
15
+ let offset = 0;
16
+ for (const arr of arrays) {
17
+ result.set(arr, offset);
18
+ offset += arr.length;
19
+ }
20
+ return result;
21
+ }
22
+ function readInt32LE(buf, offset) {
23
+ return buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24;
24
+ }
25
+ function readBigInt64LE(buf, offset) {
26
+ const lo = BigInt(buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24 >>> 0);
27
+ const hi = BigInt(buf[offset + 4] | buf[offset + 5] << 8 | buf[offset + 6] << 16 | buf[offset + 7] << 24 >>> 0);
28
+ return hi << 32n | lo & 0xFFFFFFFFn;
29
+ }
9
30
  function decodeVarint(buf, offset) {
10
31
  let result = 0, shift = 0;
11
32
  while (offset < buf.length) {
@@ -35,15 +56,15 @@ function encodeVarint(v) {
35
56
  if (n > 0n) b |= 128;
36
57
  bytes.push(b);
37
58
  } while (n > 0n);
38
- return Buffer.from(bytes);
59
+ return new Uint8Array(bytes);
39
60
  }
40
61
  function encodeField(fn, wt, value) {
41
62
  const tag = encodeVarint(fn << 3 | wt);
42
63
  if (wt === 0) {
43
- return Buffer.concat([tag, encodeVarint(typeof value === "number" ? BigInt(value) : value)]);
64
+ return concatBytes(tag, encodeVarint(typeof value === "number" ? BigInt(value) : value));
44
65
  }
45
- const data = typeof value === "string" ? Buffer.from(value) : value;
46
- return Buffer.concat([tag, encodeVarint(data.length), data]);
66
+ const data = typeof value === "string" ? encoder.encode(value) : value;
67
+ return concatBytes(tag, encodeVarint(data.length), data);
47
68
  }
48
69
  function decodeProto(buf) {
49
70
  const fields = [];
@@ -64,10 +85,10 @@ function decodeProto(buf) {
64
85
  offset += lenR.value;
65
86
  fields.push({ fn, wt, value: data });
66
87
  } else if (wt === 1) {
67
- fields.push({ fn, wt, value: buf.readBigInt64LE(offset) });
88
+ fields.push({ fn, wt, value: readBigInt64LE(buf, offset) });
68
89
  offset += 8;
69
90
  } else if (wt === 5) {
70
- fields.push({ fn, wt, value: BigInt(buf.readInt32LE(offset)) });
91
+ fields.push({ fn, wt, value: BigInt(readInt32LE(buf, offset)) });
71
92
  offset += 4;
72
93
  } else {
73
94
  break;
@@ -77,7 +98,7 @@ function decodeProto(buf) {
77
98
  }
78
99
  function getStr(fields, fn) {
79
100
  const f = fields.find((x) => x.fn === fn && x.wt === 2);
80
- return f ? f.value.toString("utf-8") : "";
101
+ return f ? decoder.decode(f.value) : "";
81
102
  }
82
103
  function getBytes(fields, fn) {
83
104
  const f = fields.find((x) => x.fn === fn && x.wt === 2);
@@ -92,33 +113,33 @@ function getAllBytes(fields, fn) {
92
113
  }
93
114
  function buildHeartbeat(roomId) {
94
115
  const payload = encodeField(1, 0, BigInt(roomId));
95
- return Buffer.concat([
116
+ return concatBytes(
96
117
  encodeField(6, 2, "pb"),
97
118
  encodeField(7, 2, "hb"),
98
119
  encodeField(8, 2, payload)
99
- ]);
120
+ );
100
121
  }
101
122
  function buildImEnterRoom(roomId) {
102
- const inner = Buffer.concat([
123
+ const inner = concatBytes(
103
124
  encodeField(1, 0, BigInt(roomId)),
104
125
  encodeField(4, 0, 12n),
105
126
  encodeField(5, 2, "audience"),
106
127
  encodeField(6, 2, ""),
107
128
  encodeField(9, 2, ""),
108
129
  encodeField(10, 2, "")
109
- ]);
110
- return Buffer.concat([
130
+ );
131
+ return concatBytes(
111
132
  encodeField(6, 2, "pb"),
112
133
  encodeField(7, 2, "im_enter_room"),
113
134
  encodeField(8, 2, inner)
114
- ]);
135
+ );
115
136
  }
116
137
  function buildAck(id) {
117
- return Buffer.concat([
138
+ return concatBytes(
118
139
  encodeField(2, 0, id),
119
140
  encodeField(6, 2, "pb"),
120
141
  encodeField(7, 2, "ack")
121
- ]);
142
+ );
122
143
  }
123
144
  function parseUser(data) {
124
145
  const f = decodeProto(data);
@@ -131,7 +152,7 @@ function parseUser(data) {
131
152
  try {
132
153
  const avatarFields = decodeProto(avatarBuf);
133
154
  const urlBufs = getAllBytes(avatarFields, 1);
134
- if (urlBufs.length > 0) profilePicture = urlBufs[0].toString("utf-8");
155
+ if (urlBufs.length > 0) profilePicture = decoder.decode(urlBufs[0]);
135
156
  } catch {
136
157
  }
137
158
  }
@@ -448,27 +469,47 @@ function parseWebcastResponse(payload) {
448
469
  // src/client.ts
449
470
  var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
450
471
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
451
- function httpGet(url, headers) {
452
- return new Promise((resolve, reject) => {
453
- const mod = url.startsWith("https") ? https : http;
454
- const req = mod.get(url, { headers }, (res) => {
455
- const chunks = [];
456
- const enc = res.headers["content-encoding"];
457
- const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
458
- stream.on("data", (c) => chunks.push(c));
459
- stream.on("end", () => resolve({
460
- status: res.statusCode || 0,
461
- headers: res.headers,
462
- body: Buffer.concat(chunks)
463
- }));
464
- stream.on("error", reject);
465
- });
466
- req.on("error", reject);
467
- req.setTimeout(15e3, () => {
468
- req.destroy();
469
- reject(new Error("Request timeout"));
470
- });
471
- });
472
+ var TypedEmitter = class {
473
+ _listeners = /* @__PURE__ */ new Map();
474
+ on(event, fn) {
475
+ const arr = this._listeners.get(event) || [];
476
+ arr.push(fn);
477
+ this._listeners.set(event, arr);
478
+ return this;
479
+ }
480
+ once(event, fn) {
481
+ const wrapper = (...args) => {
482
+ this.off(event, wrapper);
483
+ fn(...args);
484
+ };
485
+ return this.on(event, wrapper);
486
+ }
487
+ off(event, fn) {
488
+ const arr = this._listeners.get(event);
489
+ if (arr) {
490
+ this._listeners.set(event, arr.filter((l) => l !== fn));
491
+ }
492
+ return this;
493
+ }
494
+ emit(event, ...args) {
495
+ const arr = this._listeners.get(event);
496
+ if (!arr || arr.length === 0) return false;
497
+ for (const fn of [...arr]) fn(...args);
498
+ return true;
499
+ }
500
+ removeAllListeners(event) {
501
+ if (event) this._listeners.delete(event);
502
+ else this._listeners.clear();
503
+ return this;
504
+ }
505
+ };
506
+ function gunzipSync(data) {
507
+ try {
508
+ const zlib = __require("zlib");
509
+ return new Uint8Array(zlib.gunzipSync(data));
510
+ } catch {
511
+ return data;
512
+ }
472
513
  }
473
514
  function getWsHost(clusterRegion) {
474
515
  if (!clusterRegion) return "webcast-ws.tiktok.com";
@@ -477,7 +518,21 @@ function getWsHost(clusterRegion) {
477
518
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
478
519
  return "webcast-ws.tiktok.com";
479
520
  }
480
- var TikTokLive = class extends EventEmitter {
521
+ async function resolveWebSocket(userImpl) {
522
+ if (userImpl) return userImpl;
523
+ if (typeof globalThis.WebSocket !== "undefined") {
524
+ return globalThis.WebSocket;
525
+ }
526
+ try {
527
+ const ws = await import("ws");
528
+ return ws.default || ws;
529
+ } catch {
530
+ throw new Error(
531
+ 'No WebSocket implementation found. Either use Node.js 22+ (native WebSocket), Cloudflare Workers, or install the "ws" package: npm i ws'
532
+ );
533
+ }
534
+ }
535
+ var TikTokLive = class extends TypedEmitter {
481
536
  ws = null;
482
537
  heartbeatTimer = null;
483
538
  reconnectAttempts = 0;
@@ -485,7 +540,6 @@ var TikTokLive = class extends EventEmitter {
485
540
  _connected = false;
486
541
  _eventCount = 0;
487
542
  _roomId = "";
488
- // Cache host identities from battle events for enriching battleArmies
489
543
  _battleHosts = /* @__PURE__ */ new Map();
490
544
  uniqueId;
491
545
  signServerUrl;
@@ -494,6 +548,8 @@ var TikTokLive = class extends EventEmitter {
494
548
  maxReconnectAttempts;
495
549
  heartbeatInterval;
496
550
  debug;
551
+ webSocketImpl;
552
+ WS;
497
553
  constructor(options) {
498
554
  super();
499
555
  this.uniqueId = options.uniqueId.replace(/^@/, "");
@@ -504,24 +560,40 @@ var TikTokLive = class extends EventEmitter {
504
560
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
505
561
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
506
562
  this.debug = options.debug ?? false;
563
+ this.webSocketImpl = options.webSocketImpl;
507
564
  }
508
565
  async connect() {
509
566
  this.intentionalClose = false;
510
- const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
511
- "User-Agent": DEFAULT_UA,
512
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
513
- "Accept-Encoding": "gzip, deflate, br",
514
- "Accept-Language": "en-US,en;q=0.9"
567
+ if (!this.WS) {
568
+ this.WS = await resolveWebSocket(this.webSocketImpl);
569
+ }
570
+ const resp = await fetch(`https://www.tiktok.com/@${this.uniqueId}/live`, {
571
+ headers: {
572
+ "User-Agent": DEFAULT_UA,
573
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
574
+ "Accept-Language": "en-US,en;q=0.9"
575
+ },
576
+ redirect: "follow"
515
577
  });
516
578
  let ttwid = "";
517
- for (const sc of [resp.headers["set-cookie"] || []].flat()) {
518
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
519
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
579
+ const setCookies = resp.headers.get("set-cookie") || "";
580
+ for (const part of setCookies.split(",")) {
581
+ const trimmed = part.trim();
582
+ if (trimmed.startsWith("ttwid=")) {
583
+ ttwid = trimmed.split(";")[0].split("=").slice(1).join("=");
520
584
  break;
521
585
  }
522
586
  }
587
+ if (!ttwid && typeof resp.headers.getSetCookie === "function") {
588
+ for (const sc of resp.headers.getSetCookie()) {
589
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
590
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
591
+ break;
592
+ }
593
+ }
594
+ }
523
595
  if (!ttwid) throw new Error("Failed to obtain session cookie");
524
- const html = resp.body.toString();
596
+ const html = await resp.text();
525
597
  let roomId = "";
526
598
  const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
527
599
  if (sigiMatch) {
@@ -549,7 +621,7 @@ var TikTokLive = class extends EventEmitter {
549
621
  browser_name: "Mozilla",
550
622
  browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
551
623
  browser_online: "true",
552
- tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
624
+ tz_name: "Etc/UTC",
553
625
  app_name: "tiktok_web",
554
626
  sup_ws_ds_opt: "1",
555
627
  update_version_code: "2.0.0",
@@ -590,18 +662,27 @@ var TikTokLive = class extends EventEmitter {
590
662
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
591
663
  }
592
664
  return new Promise((resolve, reject) => {
593
- this.ws = new WebSocket(wsUrl, {
594
- headers: {
595
- "User-Agent": DEFAULT_UA,
596
- "Cookie": `ttwid=${ttwid}`,
597
- "Origin": "https://www.tiktok.com"
598
- }
599
- });
600
- this.ws.on("open", () => {
665
+ const connUrl = wsUrl + (wsUrl.includes("?") ? "&" : "?") + `ttwid=${ttwid}`;
666
+ try {
667
+ this.ws = new this.WS(connUrl, {
668
+ headers: {
669
+ "User-Agent": DEFAULT_UA,
670
+ "Cookie": `ttwid=${ttwid}`,
671
+ "Origin": "https://www.tiktok.com"
672
+ }
673
+ });
674
+ } catch {
675
+ this.ws = new this.WS(connUrl);
676
+ }
677
+ const ws = this.ws;
678
+ let settled = false;
679
+ ws.onopen = () => {
601
680
  this._connected = true;
602
681
  this.reconnectAttempts = 0;
603
- this.ws.send(buildHeartbeat(roomId));
604
- this.ws.send(buildImEnterRoom(roomId));
682
+ const hb = buildHeartbeat(roomId);
683
+ const enter = buildImEnterRoom(roomId);
684
+ ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
685
+ ws.send(enter.buffer.byteLength === enter.length ? enter.buffer : enter.buffer.slice(enter.byteOffset, enter.byteOffset + enter.byteLength));
605
686
  this.startHeartbeat(roomId);
606
687
  const roomInfo = {
607
688
  roomId,
@@ -611,26 +692,41 @@ var TikTokLive = class extends EventEmitter {
611
692
  };
612
693
  this.emit("connected");
613
694
  this.emit("roomInfo", roomInfo);
614
- resolve();
615
- });
616
- this.ws.on("message", (rawData) => {
617
- this.handleFrame(Buffer.from(rawData));
618
- });
619
- this.ws.on("close", (code, reason) => {
695
+ if (!settled) {
696
+ settled = true;
697
+ resolve();
698
+ }
699
+ };
700
+ ws.onmessage = (event) => {
701
+ const raw = event.data !== void 0 ? event.data : event;
702
+ this.handleMessage(raw);
703
+ };
704
+ ws.onclose = (event) => {
620
705
  this._connected = false;
621
706
  this.stopHeartbeat();
622
- const reasonStr = reason?.toString() || "";
623
- this.emit("disconnected", code, reasonStr);
707
+ const code = event?.code ?? 1006;
708
+ const reason = event?.reason ?? "";
709
+ this.emit("disconnected", code, reason?.toString?.() || "");
624
710
  if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
625
711
  this.reconnectAttempts++;
626
712
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
627
713
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
628
714
  }
629
- });
630
- this.ws.on("error", (err) => {
631
- this.emit("error", err);
632
- if (!this._connected) reject(err);
633
- });
715
+ };
716
+ ws.onerror = (err) => {
717
+ this.emit("error", err instanceof Error ? err : new Error(String(err?.message || err)));
718
+ if (!settled) {
719
+ settled = true;
720
+ reject(err);
721
+ }
722
+ };
723
+ setTimeout(() => {
724
+ if (!settled) {
725
+ settled = true;
726
+ ws.close();
727
+ reject(new Error("Connection timeout"));
728
+ }
729
+ }, 15e3);
634
730
  });
635
731
  }
636
732
  disconnect() {
@@ -651,6 +747,7 @@ var TikTokLive = class extends EventEmitter {
651
747
  get roomId() {
652
748
  return this._roomId;
653
749
  }
750
+ // Typed event emitter overrides
654
751
  on(event, listener) {
655
752
  return super.on(event, listener);
656
753
  }
@@ -663,6 +760,25 @@ var TikTokLive = class extends EventEmitter {
663
760
  emit(event, ...args) {
664
761
  return super.emit(event, ...args);
665
762
  }
763
+ // ── Message handling ──────────────────────────────────────────────
764
+ async handleMessage(raw) {
765
+ try {
766
+ let bytes;
767
+ if (raw instanceof ArrayBuffer) {
768
+ bytes = new Uint8Array(raw);
769
+ } else if (raw instanceof Uint8Array) {
770
+ bytes = raw;
771
+ } else if (typeof Blob !== "undefined" && raw instanceof Blob) {
772
+ bytes = new Uint8Array(await raw.arrayBuffer());
773
+ } else if (raw?.buffer instanceof ArrayBuffer) {
774
+ bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
775
+ } else {
776
+ return;
777
+ }
778
+ this.handleFrame(bytes);
779
+ } catch {
780
+ }
781
+ }
666
782
  handleFrame(buf) {
667
783
  try {
668
784
  const fields = decodeProto(buf);
@@ -670,14 +786,15 @@ var TikTokLive = class extends EventEmitter {
670
786
  const id = idField ? idField.value : 0n;
671
787
  const type = getStr(fields, 7);
672
788
  const binary = getBytes(fields, 8);
673
- if (id > 0n && this.ws?.readyState === WebSocket.OPEN) {
674
- this.ws.send(buildAck(id));
789
+ if (id > 0n && this.ws && this.ws.readyState === 1) {
790
+ const ack = buildAck(id);
791
+ this.ws.send(ack.buffer.byteLength === ack.length ? ack.buffer : ack.buffer.slice(ack.byteOffset, ack.byteOffset + ack.byteLength));
675
792
  }
676
793
  if (type === "msg" && binary && binary.length > 0) {
677
794
  let inner = binary;
678
795
  if (inner.length > 2 && inner[0] === 31 && inner[1] === 139) {
679
796
  try {
680
- inner = zlib.gunzipSync(inner);
797
+ inner = gunzipSync(inner);
681
798
  } catch {
682
799
  }
683
800
  }
@@ -715,8 +832,9 @@ var TikTokLive = class extends EventEmitter {
715
832
  startHeartbeat(roomId) {
716
833
  this.stopHeartbeat();
717
834
  this.heartbeatTimer = setInterval(() => {
718
- if (this.ws?.readyState === WebSocket.OPEN) {
719
- this.ws.send(buildHeartbeat(roomId));
835
+ if (this.ws && this.ws.readyState === 1) {
836
+ const hb = buildHeartbeat(roomId);
837
+ this.ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
720
838
  }
721
839
  }, this.heartbeatInterval);
722
840
  }