@tiktool/live 2.7.0 → 2.8.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.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
  }
@@ -431,27 +432,41 @@ function parseWebcastMessage(method, payload) {
431
432
  // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
432
433
  // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
433
434
  case "WebcastLinkMicBattle": {
434
- const battleId = String(getInt(f, 1) || "");
435
- const status = getInt(f, 2) || 1;
435
+ const battleId = String(getInt(f, 4) || getInt(f, 1) || "");
436
+ let status = getInt(f, 7);
437
+ if (!status) {
438
+ const fallback = getInt(f, 2);
439
+ status = fallback > 0 && fallback < 100 ? fallback : 1;
440
+ }
436
441
  const battleDuration = getInt(f, 3);
437
442
  const teams = [];
438
- const battleUserBufs = getAllBytes(f, 10);
439
- for (const bub of battleUserBufs) {
443
+ const hostBufs = getAllBytes(f, 14);
444
+ for (const hb of hostBufs) {
440
445
  try {
441
- const bf = decodeProto(bub);
442
- const groupBuf = getBytes(bf, 2);
443
- if (groupBuf) {
444
- const gf = decodeProto(groupBuf);
445
- const linkUserBuf = getBytes(gf, 1);
446
- if (linkUserBuf) {
447
- const user = parseUser(linkUserBuf);
448
- teams.push({
449
- hostUserId: user.id,
450
- score: 0,
451
- users: [{ user, score: 0 }]
452
- });
446
+ const hf = decodeProto(hb);
447
+ const hostUserId = String(getInt(hf, 1) || "");
448
+ const teamTotalScore = getInt(hf, 3);
449
+ const users = [];
450
+ const containerBuf = getBytes(hf, 4);
451
+ if (containerBuf) {
452
+ const cf = decodeProto(containerBuf);
453
+ const userBufs = getAllBytes(cf, 1);
454
+ for (const ub of userBufs) {
455
+ try {
456
+ const uf = decodeProto(ub);
457
+ users.push({
458
+ user: {
459
+ id: String(getInt(uf, 1) || ""),
460
+ nickname: getStr(uf, 3) || "",
461
+ uniqueId: getStr(uf, 3) || ""
462
+ },
463
+ score: getInt(uf, 2)
464
+ });
465
+ } catch {
466
+ }
453
467
  }
454
468
  }
469
+ teams.push({ hostUserId, score: teamTotalScore, users });
455
470
  } catch {
456
471
  }
457
472
  }
@@ -464,15 +479,42 @@ function parseWebcastMessage(method, payload) {
464
479
  }
465
480
  }
466
481
  }
482
+ if (teams.length === 0) {
483
+ const battleUserBufs = getAllBytes(f, 10);
484
+ for (const bub of battleUserBufs) {
485
+ try {
486
+ const bf = decodeProto(bub);
487
+ const groupBuf = getBytes(bf, 2);
488
+ if (groupBuf) {
489
+ const gf = decodeProto(groupBuf);
490
+ const linkUserBuf = getBytes(gf, 1);
491
+ if (linkUserBuf) {
492
+ const user = parseUser(linkUserBuf);
493
+ teams.push({
494
+ hostUserId: user.id,
495
+ score: 0,
496
+ users: [{ user, score: 0 }]
497
+ });
498
+ }
499
+ }
500
+ } catch {
501
+ }
502
+ }
503
+ }
467
504
  return { ...base, type: "battle", battleId, status, battleDuration, teams };
468
505
  }
469
- // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
470
- // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
471
- // battleItems → { hostUserId=1, repeated battleGroups=2 }
472
- // battleGroups → { repeated users=1, int32 points=2 }
506
+ // Proto: WebcastLinkMicArmies { msg_header=1, matchId=2, session_id=4, startedAtMs=5,
507
+ // serverTsMs=6, repeated battleItems=3, status=7, sessionTag=17,
508
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 },
509
+ // repeated hosts=14 { hostUserId=1, teamTotalScore=3, teamIdx=5,
510
+ // userContainer=4 { repeated users=1 { userId=1, score=2, nickname=3 } } }
473
511
  case "WebcastLinkMicArmies": {
474
- const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
512
+ const sessionTag = getStr(f, 17);
513
+ const matchId = getIntStr(f, 2);
514
+ const sessionId = getIntStr(f, 4);
515
+ const battleId = sessionTag || matchId;
475
516
  const battleStatus = getInt(f, 7);
517
+ const serverTsMs = getInt(f, 6);
476
518
  const teams = [];
477
519
  const itemBufs = getAllBytes(f, 3);
478
520
  for (const ib of itemBufs) {
@@ -492,11 +534,47 @@ function parseWebcastMessage(method, payload) {
492
534
  } catch {
493
535
  }
494
536
  }
537
+ let secsRemaining = 0;
538
+ if (battleStartMs > 0 && battleDurationSeconds > 0 && serverTsMs > 0) {
539
+ secsRemaining = Math.max(0, battleDurationSeconds - Math.floor((serverTsMs - battleStartMs) / 1e3));
540
+ }
541
+ let endTimeMs = 0;
495
542
  let timeLeftSeconds = 0;
496
543
  if (battleStartMs > 0 && battleDurationSeconds > 0) {
544
+ endTimeMs = battleStartMs + battleDurationSeconds * 1e3;
497
545
  const elapsedMs = Date.now() - battleStartMs;
498
546
  timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
499
547
  }
548
+ const hosts = [];
549
+ const hostBufs = getAllBytes(f, 14);
550
+ for (const hb of hostBufs) {
551
+ try {
552
+ const hf = decodeProto(hb);
553
+ const hostUserId = getIntStr(hf, 1);
554
+ const teamTotalScore = getInt(hf, 3);
555
+ const teamIdx = getInt(hf, 5);
556
+ const contributors = [];
557
+ const containerBuf = getBytes(hf, 4);
558
+ if (containerBuf) {
559
+ const cf = decodeProto(containerBuf);
560
+ const userBufs = getAllBytes(cf, 1);
561
+ for (const ub of userBufs) {
562
+ try {
563
+ const uf = decodeProto(ub);
564
+ contributors.push({
565
+ userId: getIntStr(uf, 1),
566
+ score: getInt(uf, 2),
567
+ nickname: getStr(uf, 3) || ""
568
+ });
569
+ } catch {
570
+ }
571
+ }
572
+ contributors.sort((a, b) => b.score - a.score);
573
+ }
574
+ hosts.push({ hostUserId, teamTotalScore, teamIdx, contributors });
575
+ } catch {
576
+ }
577
+ }
500
578
  return {
501
579
  ...base,
502
580
  type: "battleArmies",
@@ -505,7 +583,163 @@ function parseWebcastMessage(method, payload) {
505
583
  status: battleStatus,
506
584
  battleStartMs,
507
585
  battleDurationSeconds,
508
- timeLeftSeconds
586
+ timeLeftSeconds,
587
+ endTimeMs,
588
+ matchId,
589
+ sessionId,
590
+ serverTsMs,
591
+ sessionTag,
592
+ durationSec: battleDurationSeconds,
593
+ secsRemaining,
594
+ hosts
595
+ };
596
+ }
597
+ // Proto: WebcastLinkMicBattleItemCard
598
+ // #2 battleId
599
+ // #3 cardType (2=gloves/crit, 3=mist, 4=match_guide, 10=x2?, 11=x3, ...)
600
+ // #5/#6/#7/#14 inner (location varies with cardType — scan numerically):
601
+ // #1.{
602
+ // #1 effectKey
603
+ // #2 icon block { #1 repeated CDN urls, #2 iconKey, #5 accentColor }
604
+ // #3 activatedAtSec (unix seconds)
605
+ // #4 sender block: #1 sub { #1 userId, #2 nickname, #3 avatar block, #4 uniqueId }
606
+ // #5 durationSec
607
+ // #8 senderUserId (new boosters)
608
+ // #11 endsAtSec
609
+ // #12 senderUserId (old gloves/mist)
610
+ // }
611
+ // #3 comment template msg → #2 commentText
612
+ case "WebcastLinkMicBattleItemCard": {
613
+ const battleId = getIntStr(f, 2);
614
+ const cardType = getInt(f, 3);
615
+ let inner = null;
616
+ for (const fn of [5, 6, 7, 14, 15, 16, 8, 9, 10, 11, 12, 13]) {
617
+ const buf = getBytes(f, fn);
618
+ if (buf) {
619
+ inner = decodeProto(buf);
620
+ break;
621
+ }
622
+ }
623
+ let effectKey = "";
624
+ let effect = "unknown";
625
+ let multiplier = 0;
626
+ let activatedAtSec = 0;
627
+ let durationSec = 0;
628
+ let endsAtSec = 0;
629
+ let senderUserId = "";
630
+ let senderNickname = "";
631
+ let senderUniqueId = "";
632
+ let senderAvatarUrl = "";
633
+ let commentTemplate = "";
634
+ let iconUrl = "";
635
+ let iconKey = "";
636
+ let accentColor = "";
637
+ if (inner) {
638
+ let nameBuf = getBytes(inner, 1);
639
+ let nf = nameBuf ? decodeProto(nameBuf) : [];
640
+ let probedKey = nf.length ? getStr(nf, 1) || "" : "";
641
+ if (!probedKey) {
642
+ const altBuf = getBytes(inner, 3);
643
+ if (altBuf) {
644
+ const altFields = decodeProto(altBuf);
645
+ const altKey = getStr(altFields, 1) || "";
646
+ if (altKey) {
647
+ nameBuf = altBuf;
648
+ nf = altFields;
649
+ probedKey = altKey;
650
+ }
651
+ }
652
+ }
653
+ if (nameBuf && nf.length) {
654
+ effectKey = probedKey;
655
+ if (effectKey === "pm_mt_boost_crit_name") effect = "hammer";
656
+ else if (effectKey === "pm_mt_boost_glove_name" || effectKey === "pm_mt_boost_gloves_name") effect = "gloves";
657
+ else if (effectKey === "pm_mt_boost_mist_name") effect = "mist";
658
+ else if (effectKey === "pm_mt_match_guide_gotPowerUps") effect = "match_guide";
659
+ else if (effectKey.includes("userSentBooster_no2") || effectKey.includes("gotPowerUp_panel_no2")) {
660
+ effect = "booster_x2";
661
+ multiplier = 2;
662
+ } else if (effectKey.includes("userSentBooster_no3") || effectKey.includes("gotPowerUp_panel_no3")) {
663
+ effect = "booster_x3";
664
+ multiplier = 3;
665
+ } else if (effectKey.includes("thunder")) effect = "thunder";
666
+ else if (effectKey.includes("extra_time") || effectKey.includes("extra_round")) effect = "extra_time";
667
+ else if (effectKey) effect = effectKey;
668
+ activatedAtSec = getInt(nf, 3);
669
+ durationSec = getInt(nf, 5);
670
+ endsAtSec = getInt(nf, 11) || getInt(nf, 7);
671
+ senderUserId = getIntStr(nf, 12);
672
+ if (senderUserId === "0") senderUserId = getIntStr(nf, 8);
673
+ if (senderUserId === "0") senderUserId = getStr(nf, 13) || getStr(nf, 9) || "";
674
+ const iconBuf = getBytes(nf, 2);
675
+ if (iconBuf) {
676
+ const icf = decodeProto(iconBuf);
677
+ const urlBufs = getAllBytes(icf, 1);
678
+ for (const ub of urlBufs) {
679
+ const candidate = ub.toString("utf-8");
680
+ if (candidate.includes("://")) {
681
+ iconUrl = candidate;
682
+ break;
683
+ }
684
+ }
685
+ iconKey = getStr(icf, 2) || "";
686
+ accentColor = getStr(icf, 5) || "";
687
+ }
688
+ const senderOuter = getBytes(nf, 4);
689
+ if (senderOuter) {
690
+ const sf = decodeProto(senderOuter);
691
+ const sub = getBytes(sf, 1);
692
+ if (sub) {
693
+ const su = decodeProto(sub);
694
+ const nickBuf = getBytes(su, 2);
695
+ if (nickBuf) {
696
+ try {
697
+ senderNickname = nickBuf.toString("utf-8");
698
+ } catch {
699
+ }
700
+ }
701
+ senderUniqueId = getStr(su, 4) || "";
702
+ if (!senderUserId || senderUserId === "0") senderUserId = getIntStr(su, 1);
703
+ const avBuf = getBytes(su, 3);
704
+ if (avBuf) {
705
+ const af = decodeProto(avBuf);
706
+ const urlBufs = getAllBytes(af, 1);
707
+ for (const ub of urlBufs) {
708
+ const candidate = ub.toString("utf-8");
709
+ if (candidate.includes("://")) {
710
+ senderAvatarUrl = candidate;
711
+ break;
712
+ }
713
+ }
714
+ }
715
+ }
716
+ }
717
+ }
718
+ const commentBuf = getBytes(inner, 3);
719
+ if (commentBuf) {
720
+ const cf = decodeProto(commentBuf);
721
+ commentTemplate = getStr(cf, 2) || "";
722
+ }
723
+ }
724
+ return {
725
+ ...base,
726
+ type: "battleItemCard",
727
+ battleId,
728
+ cardType,
729
+ effect,
730
+ effectKey,
731
+ multiplier,
732
+ senderUserId,
733
+ senderNickname,
734
+ senderUniqueId,
735
+ senderAvatarUrl,
736
+ activatedAtSec,
737
+ durationSec,
738
+ endsAtSec,
739
+ commentTemplate,
740
+ iconUrl,
741
+ iconKey,
742
+ accentColor
509
743
  };
510
744
  }
511
745
  // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
@@ -687,24 +921,37 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
687
921
  let ttwid = this._presetSessionId;
688
922
  let roomId = this._presetRoomId;
689
923
  let clusterRegion = "";
924
+ let preSignedUrl = "";
925
+ let preSignedCookies = "";
926
+ let preSignedUA = "";
690
927
  if (roomId && ttwid) {
928
+ console.log(`[TikTokLive] Using preset roomId=${roomId} + sessionId (skip TikTok fetch)`);
691
929
  } else if (roomId && !ttwid) {
930
+ console.log(`[TikTokLive] Have roomId=${roomId}, fetching ttwid from sign server...`);
692
931
  try {
693
932
  const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
694
933
  method: "POST",
695
934
  headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
696
935
  body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
697
936
  });
937
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
698
938
  if (credResp.ok) {
699
939
  const credData = await credResp.json();
700
- if (credData.data?.cookies) {
701
- const m = credData.data.cookies.match(/ttwid=([^;]+)/);
702
- if (m) ttwid = m[1];
940
+ if (credData.status_code === 0 && credData.data?.signed_url) {
941
+ preSignedUrl = credData.data.signed_url;
942
+ preSignedCookies = credData.data.cookies || "";
943
+ preSignedUA = credData.data.user_agent || DEFAULT_UA;
944
+ ttwid = "__presigned__";
945
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got pre-signed URL from sign server`);
946
+ } else {
947
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
703
948
  }
704
949
  }
705
- } catch {
950
+ } catch (e) {
951
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
706
952
  }
707
953
  if (!ttwid) {
954
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
708
955
  try {
709
956
  const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
710
957
  "User-Agent": DEFAULT_UA,
@@ -716,35 +963,93 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
716
963
  break;
717
964
  }
718
965
  }
719
- } catch {
966
+ console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\xE2\u0153\u2026" : "\xE2\x9D\u0152 none"}`);
967
+ } catch (e) {
968
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
720
969
  }
721
970
  }
722
971
  } 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;
972
+ console.log(`[TikTokLive] No presets for @${this.uniqueId}. Resolving via sign server (${this.signServerUrl})...`);
973
+ try {
974
+ const roomResp = await fetch(`${this.signServerUrl}/webcast/room_id`, {
975
+ method: "POST",
976
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
977
+ body: JSON.stringify({ unique_id: this.uniqueId })
978
+ });
979
+ console.log(`[TikTokLive] room_id response: ${roomResp.status}`);
980
+ if (roomResp.ok) {
981
+ const roomData = await roomResp.json();
982
+ if (roomData.status_code === 0 && roomData.data?.room_id) {
983
+ roomId = roomData.data.room_id;
984
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got roomId from sign server: ${roomId}`);
985
+ } else {
986
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
987
+ }
733
988
  }
989
+ } catch (e) {
990
+ console.log(`[TikTokLive] \xE2\x9D\u0152 room_id request failed: ${e.message}`);
734
991
  }
735
- const html = resp.body.toString();
736
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
737
- if (sigiMatch) {
992
+ if (roomId) {
738
993
  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 {
994
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
995
+ method: "POST",
996
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
997
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
998
+ });
999
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
1000
+ if (credResp.ok) {
1001
+ const credData = await credResp.json();
1002
+ if (credData.data?.cookies) {
1003
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
1004
+ if (m) {
1005
+ ttwid = m[1];
1006
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got ttwid from sign server`);
1007
+ } else console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
1008
+ } else {
1009
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
1010
+ }
1011
+ }
1012
+ } catch (e) {
1013
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
1014
+ }
1015
+ }
1016
+ if (!roomId || !ttwid) {
1017
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch (roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"})...`);
1018
+ try {
1019
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
1020
+ "User-Agent": DEFAULT_UA,
1021
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
1022
+ "Accept-Encoding": "gzip, deflate, br",
1023
+ "Accept-Language": "en-US,en;q=0.9"
1024
+ });
1025
+ if (!ttwid) {
1026
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
1027
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
1028
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
1029
+ break;
1030
+ }
1031
+ }
1032
+ }
1033
+ if (!roomId) {
1034
+ const html = resp.body.toString();
1035
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
1036
+ if (sigiMatch) {
1037
+ try {
1038
+ const json = JSON.parse(sigiMatch[1]);
1039
+ const jsonStr = JSON.stringify(json);
1040
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
1041
+ if (m) roomId = m[1];
1042
+ } catch {
1043
+ }
1044
+ }
1045
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
1046
+ clusterRegion = crMatch ? crMatch[1] : "";
1047
+ }
1048
+ console.log(`[TikTokLive] Direct fetch result: roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"}`);
1049
+ } catch (e) {
1050
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
744
1051
  }
745
1052
  }
746
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
747
- clusterRegion = crMatch ? crMatch[1] : "";
748
1053
  }
749
1054
  if (!ttwid) throw new Error("Failed to obtain session cookie");
750
1055
  if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
@@ -802,12 +1107,11 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
802
1107
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
803
1108
  }
804
1109
  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
- }
1110
+ const finalWsUrl = preSignedUrl ? preSignedUrl.replace(/^https:\/\//, "wss://") : wsUrl;
1111
+ 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" };
1112
+ console.log(`[TikTokLive] Connecting WS: ${preSignedUrl ? "pre-signed URL" : "self-built URL"}`);
1113
+ this.ws = new import_ws.default(finalWsUrl, {
1114
+ headers: finalHeaders
811
1115
  });
812
1116
  this.ws.on("open", () => {
813
1117
  this._connected = true;
@@ -829,18 +1133,21 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
829
1133
  this.handleFrame(Buffer.from(rawData));
830
1134
  });
831
1135
  this.ws.on("close", (code, reason) => {
1136
+ const wasConnected = this._connected;
832
1137
  this._connected = false;
833
1138
  this.stopHeartbeat();
834
1139
  const reasonStr = reason?.toString() || "";
835
1140
  this.emit("disconnected", code, reasonStr);
836
- if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1141
+ if (wasConnected && !this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
837
1142
  this.reconnectAttempts++;
838
1143
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
839
1144
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
840
1145
  }
841
1146
  });
842
1147
  this.ws.on("error", (err) => {
843
- this.emit("error", err);
1148
+ if (this._connected) {
1149
+ this.emit("error", err);
1150
+ }
844
1151
  if (!this._connected) reject(err);
845
1152
  });
846
1153
  });
@@ -857,7 +1164,7 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
857
1164
  }
858
1165
  /**
859
1166
  * Fully destroy the client, releasing all resources and listeners.
860
- * After calling destroy(), the instance cannot be reused — create a new one.
1167
+ * After calling destroy(), the instance cannot be reused — create a new one.
861
1168
  */
862
1169
  destroy() {
863
1170
  this.disconnect();
@@ -882,7 +1189,7 @@ var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
882
1189
  /**
883
1190
  * Get live stream video URLs for a TikTok user.
884
1191
  * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
885
- * This is a standalone method — no WebSocket connection required.
1192
+ * This is a standalone method — no WebSocket connection required.
886
1193
  *
887
1194
  * @example
888
1195
  * ```ts