@tiktool/live 2.7.0 → 2.7.1

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.d.mts CHANGED
@@ -83,8 +83,10 @@ interface BattleArmiesEvent extends BaseEvent {
83
83
  battleStartMs: number;
84
84
  /** Total battle duration in seconds (from f18.f3, typically 300) */
85
85
  battleDurationSeconds: number;
86
- /** Seconds remaining in the battle, computed as max(0, duration - elapsed) */
86
+ /** Seconds remaining in the battle, computed as max(0, duration - elapsed). Note: uses VPS clock, may be off by up to 30s */
87
87
  timeLeftSeconds: number;
88
+ /** Battle end timestamp in milliseconds (battleStartMs + duration*1000). Use for clock-independent timer: Math.max(0, (endTimeMs - Date.now()) / 1000) */
89
+ endTimeMs: number;
88
90
  }
89
91
  interface SubscribeEvent extends BaseEvent {
90
92
  type: 'subscribe';
package/dist/index.d.ts CHANGED
@@ -83,8 +83,10 @@ interface BattleArmiesEvent extends BaseEvent {
83
83
  battleStartMs: number;
84
84
  /** Total battle duration in seconds (from f18.f3, typically 300) */
85
85
  battleDurationSeconds: number;
86
- /** Seconds remaining in the battle, computed as max(0, duration - elapsed) */
86
+ /** Seconds remaining in the battle, computed as max(0, duration - elapsed). Note: uses VPS clock, may be off by up to 30s */
87
87
  timeLeftSeconds: number;
88
+ /** Battle end timestamp in milliseconds (battleStartMs + duration*1000). Use for clock-independent timer: Math.max(0, (endTimeMs - Date.now()) / 1000) */
89
+ endTimeMs: number;
88
90
  }
89
91
  interface SubscribeEvent extends BaseEvent {
90
92
  type: 'subscribe';
package/dist/index.js CHANGED
@@ -264,10 +264,11 @@ function parseBattleTeamFromArmies(itemBuf) {
264
264
  const points = getInt(gf, 2);
265
265
  teamScore += points;
266
266
  const userBufs = getAllBytes(gf, 1);
267
+ const individualScore = userBufs.length === 1 ? points : -1;
267
268
  for (const ub of userBufs) {
268
269
  try {
269
270
  const user = parseUser(ub);
270
- users.push({ user, score: points });
271
+ users.push({ user, score: individualScore });
271
272
  } catch {
272
273
  }
273
274
  }
@@ -492,8 +493,10 @@ function parseWebcastMessage(method, payload) {
492
493
  } catch {
493
494
  }
494
495
  }
496
+ let endTimeMs = 0;
495
497
  let timeLeftSeconds = 0;
496
498
  if (battleStartMs > 0 && battleDurationSeconds > 0) {
499
+ endTimeMs = battleStartMs + battleDurationSeconds * 1e3;
497
500
  const elapsedMs = Date.now() - battleStartMs;
498
501
  timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
499
502
  }
@@ -505,7 +508,8 @@ function parseWebcastMessage(method, payload) {
505
508
  status: battleStatus,
506
509
  battleStartMs,
507
510
  battleDurationSeconds,
508
- timeLeftSeconds
511
+ timeLeftSeconds,
512
+ endTimeMs
509
513
  };
510
514
  }
511
515
  // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
@@ -687,24 +691,37 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
687
691
  let ttwid = this._presetSessionId;
688
692
  let roomId = this._presetRoomId;
689
693
  let clusterRegion = "";
694
+ let preSignedUrl = "";
695
+ let preSignedCookies = "";
696
+ let preSignedUA = "";
690
697
  if (roomId && ttwid) {
698
+ console.log(`[TikTokLive] Using preset roomId=${roomId} + sessionId (skip TikTok fetch)`);
691
699
  } else if (roomId && !ttwid) {
700
+ console.log(`[TikTokLive] Have roomId=${roomId}, fetching ttwid from sign server...`);
692
701
  try {
693
702
  const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
694
703
  method: "POST",
695
704
  headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
696
705
  body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
697
706
  });
707
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
698
708
  if (credResp.ok) {
699
709
  const credData = await credResp.json();
700
- if (credData.data?.cookies) {
701
- const m = credData.data.cookies.match(/ttwid=([^;]+)/);
702
- if (m) ttwid = m[1];
710
+ if (credData.status_code === 0 && credData.data?.signed_url) {
711
+ preSignedUrl = credData.data.signed_url;
712
+ preSignedCookies = credData.data.cookies || "";
713
+ preSignedUA = credData.data.user_agent || DEFAULT_UA;
714
+ ttwid = "__presigned__";
715
+ console.log(`[TikTokLive] \u2705 Got pre-signed URL from sign server`);
716
+ } else {
717
+ console.log(`[TikTokLive] \u26A0\uFE0F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
703
718
  }
704
719
  }
705
- } catch {
720
+ } catch (e) {
721
+ console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
706
722
  }
707
723
  if (!ttwid) {
724
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
708
725
  try {
709
726
  const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
710
727
  "User-Agent": DEFAULT_UA,
@@ -716,35 +733,93 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
716
733
  break;
717
734
  }
718
735
  }
719
- } catch {
736
+ console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\u2705" : "\u274C none"}`);
737
+ } catch (e) {
738
+ console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
720
739
  }
721
740
  }
722
741
  } else {
723
- const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
724
- "User-Agent": DEFAULT_UA,
725
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
726
- "Accept-Encoding": "gzip, deflate, br",
727
- "Accept-Language": "en-US,en;q=0.9"
728
- });
729
- for (const sc of [resp.headers["set-cookie"] || []].flat()) {
730
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
731
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
732
- break;
742
+ console.log(`[TikTokLive] No presets for @${this.uniqueId}. Resolving via sign server (${this.signServerUrl})...`);
743
+ try {
744
+ const roomResp = await fetch(`${this.signServerUrl}/webcast/room_id`, {
745
+ method: "POST",
746
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
747
+ body: JSON.stringify({ unique_id: this.uniqueId })
748
+ });
749
+ console.log(`[TikTokLive] room_id response: ${roomResp.status}`);
750
+ if (roomResp.ok) {
751
+ const roomData = await roomResp.json();
752
+ if (roomData.status_code === 0 && roomData.data?.room_id) {
753
+ roomId = roomData.data.room_id;
754
+ console.log(`[TikTokLive] \u2705 Got roomId from sign server: ${roomId}`);
755
+ } else {
756
+ console.log(`[TikTokLive] \u26A0\uFE0F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
757
+ }
733
758
  }
759
+ } catch (e) {
760
+ console.log(`[TikTokLive] \u274C room_id request failed: ${e.message}`);
734
761
  }
735
- const html = resp.body.toString();
736
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
737
- if (sigiMatch) {
762
+ if (roomId) {
738
763
  try {
739
- const json = JSON.parse(sigiMatch[1]);
740
- const jsonStr = JSON.stringify(json);
741
- const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
742
- if (m) roomId = m[1];
743
- } catch {
764
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
767
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
768
+ });
769
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
770
+ if (credResp.ok) {
771
+ const credData = await credResp.json();
772
+ if (credData.data?.cookies) {
773
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
774
+ if (m) {
775
+ ttwid = m[1];
776
+ console.log(`[TikTokLive] \u2705 Got ttwid from sign server`);
777
+ } else console.log(`[TikTokLive] \u26A0\uFE0F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
778
+ } else {
779
+ console.log(`[TikTokLive] \u26A0\uFE0F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
780
+ }
781
+ }
782
+ } catch (e) {
783
+ console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
784
+ }
785
+ }
786
+ if (!roomId || !ttwid) {
787
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch (roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"})...`);
788
+ try {
789
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
790
+ "User-Agent": DEFAULT_UA,
791
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
792
+ "Accept-Encoding": "gzip, deflate, br",
793
+ "Accept-Language": "en-US,en;q=0.9"
794
+ });
795
+ if (!ttwid) {
796
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
797
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
798
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
799
+ break;
800
+ }
801
+ }
802
+ }
803
+ if (!roomId) {
804
+ const html = resp.body.toString();
805
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
806
+ if (sigiMatch) {
807
+ try {
808
+ const json = JSON.parse(sigiMatch[1]);
809
+ const jsonStr = JSON.stringify(json);
810
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
811
+ if (m) roomId = m[1];
812
+ } catch {
813
+ }
814
+ }
815
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
816
+ clusterRegion = crMatch ? crMatch[1] : "";
817
+ }
818
+ console.log(`[TikTokLive] Direct fetch result: roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"}`);
819
+ } catch (e) {
820
+ console.log(`[TikTokLive] \u274C Direct TikTok fetch failed: ${e.message}`);
744
821
  }
745
822
  }
746
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
747
- clusterRegion = crMatch ? crMatch[1] : "";
748
823
  }
749
824
  if (!ttwid) throw new Error("Failed to obtain session cookie");
750
825
  if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
@@ -802,12 +877,11 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
802
877
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
803
878
  }
804
879
  return new Promise((resolve, reject) => {
805
- this.ws = new import_ws.default(wsUrl, {
806
- headers: {
807
- "User-Agent": DEFAULT_UA,
808
- "Cookie": `ttwid=${ttwid}`,
809
- "Origin": "https://www.tiktok.com"
810
- }
880
+ const finalWsUrl = preSignedUrl ? preSignedUrl.replace(/^https:\/\//, "wss://") : wsUrl;
881
+ const finalHeaders = preSignedUrl ? { "User-Agent": preSignedUA, "Cookie": preSignedCookies, "Origin": "https://www.tiktok.com" } : { "User-Agent": DEFAULT_UA, "Cookie": `ttwid=${ttwid}`, "Origin": "https://www.tiktok.com" };
882
+ console.log(`[TikTokLive] Connecting WS: ${preSignedUrl ? "pre-signed URL" : "self-built URL"}`);
883
+ this.ws = new import_ws.default(finalWsUrl, {
884
+ headers: finalHeaders
811
885
  });
812
886
  this.ws.on("open", () => {
813
887
  this._connected = true;