@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.mjs CHANGED
@@ -228,10 +228,11 @@ function parseBattleTeamFromArmies(itemBuf) {
228
228
  const points = getInt(gf, 2);
229
229
  teamScore += points;
230
230
  const userBufs = getAllBytes(gf, 1);
231
+ const individualScore = userBufs.length === 1 ? points : -1;
231
232
  for (const ub of userBufs) {
232
233
  try {
233
234
  const user = parseUser(ub);
234
- users.push({ user, score: points });
235
+ users.push({ user, score: individualScore });
235
236
  } catch {
236
237
  }
237
238
  }
@@ -395,27 +396,41 @@ function parseWebcastMessage(method, payload) {
395
396
  // Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
396
397
  // battleUsers → { battleGroup=2 → { LinkUser user=1 } }
397
398
  case "WebcastLinkMicBattle": {
398
- const battleId = String(getInt(f, 1) || "");
399
- const status = getInt(f, 2) || 1;
399
+ const battleId = String(getInt(f, 4) || getInt(f, 1) || "");
400
+ let status = getInt(f, 7);
401
+ if (!status) {
402
+ const fallback = getInt(f, 2);
403
+ status = fallback > 0 && fallback < 100 ? fallback : 1;
404
+ }
400
405
  const battleDuration = getInt(f, 3);
401
406
  const teams = [];
402
- const battleUserBufs = getAllBytes(f, 10);
403
- for (const bub of battleUserBufs) {
407
+ const hostBufs = getAllBytes(f, 14);
408
+ for (const hb of hostBufs) {
404
409
  try {
405
- const bf = decodeProto(bub);
406
- const groupBuf = getBytes(bf, 2);
407
- if (groupBuf) {
408
- const gf = decodeProto(groupBuf);
409
- const linkUserBuf = getBytes(gf, 1);
410
- if (linkUserBuf) {
411
- const user = parseUser(linkUserBuf);
412
- teams.push({
413
- hostUserId: user.id,
414
- score: 0,
415
- users: [{ user, score: 0 }]
416
- });
410
+ const hf = decodeProto(hb);
411
+ const hostUserId = String(getInt(hf, 1) || "");
412
+ const teamTotalScore = getInt(hf, 3);
413
+ const users = [];
414
+ const containerBuf = getBytes(hf, 4);
415
+ if (containerBuf) {
416
+ const cf = decodeProto(containerBuf);
417
+ const userBufs = getAllBytes(cf, 1);
418
+ for (const ub of userBufs) {
419
+ try {
420
+ const uf = decodeProto(ub);
421
+ users.push({
422
+ user: {
423
+ id: String(getInt(uf, 1) || ""),
424
+ nickname: getStr(uf, 3) || "",
425
+ uniqueId: getStr(uf, 3) || ""
426
+ },
427
+ score: getInt(uf, 2)
428
+ });
429
+ } catch {
430
+ }
417
431
  }
418
432
  }
433
+ teams.push({ hostUserId, score: teamTotalScore, users });
419
434
  } catch {
420
435
  }
421
436
  }
@@ -428,15 +443,42 @@ function parseWebcastMessage(method, payload) {
428
443
  }
429
444
  }
430
445
  }
446
+ if (teams.length === 0) {
447
+ const battleUserBufs = getAllBytes(f, 10);
448
+ for (const bub of battleUserBufs) {
449
+ try {
450
+ const bf = decodeProto(bub);
451
+ const groupBuf = getBytes(bf, 2);
452
+ if (groupBuf) {
453
+ const gf = decodeProto(groupBuf);
454
+ const linkUserBuf = getBytes(gf, 1);
455
+ if (linkUserBuf) {
456
+ const user = parseUser(linkUserBuf);
457
+ teams.push({
458
+ hostUserId: user.id,
459
+ score: 0,
460
+ users: [{ user, score: 0 }]
461
+ });
462
+ }
463
+ }
464
+ } catch {
465
+ }
466
+ }
467
+ }
431
468
  return { ...base, type: "battle", battleId, status, battleDuration, teams };
432
469
  }
433
- // Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
434
- // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
435
- // battleItems → { hostUserId=1, repeated battleGroups=2 }
436
- // battleGroups → { repeated users=1, int32 points=2 }
470
+ // Proto: WebcastLinkMicArmies { msg_header=1, matchId=2, session_id=4, startedAtMs=5,
471
+ // serverTsMs=6, repeated battleItems=3, status=7, sessionTag=17,
472
+ // battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 },
473
+ // repeated hosts=14 { hostUserId=1, teamTotalScore=3, teamIdx=5,
474
+ // userContainer=4 { repeated users=1 { userId=1, score=2, nickname=3 } } }
437
475
  case "WebcastLinkMicArmies": {
438
- const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
476
+ const sessionTag = getStr(f, 17);
477
+ const matchId = getIntStr(f, 2);
478
+ const sessionId = getIntStr(f, 4);
479
+ const battleId = sessionTag || matchId;
439
480
  const battleStatus = getInt(f, 7);
481
+ const serverTsMs = getInt(f, 6);
440
482
  const teams = [];
441
483
  const itemBufs = getAllBytes(f, 3);
442
484
  for (const ib of itemBufs) {
@@ -456,11 +498,47 @@ function parseWebcastMessage(method, payload) {
456
498
  } catch {
457
499
  }
458
500
  }
501
+ let secsRemaining = 0;
502
+ if (battleStartMs > 0 && battleDurationSeconds > 0 && serverTsMs > 0) {
503
+ secsRemaining = Math.max(0, battleDurationSeconds - Math.floor((serverTsMs - battleStartMs) / 1e3));
504
+ }
505
+ let endTimeMs = 0;
459
506
  let timeLeftSeconds = 0;
460
507
  if (battleStartMs > 0 && battleDurationSeconds > 0) {
508
+ endTimeMs = battleStartMs + battleDurationSeconds * 1e3;
461
509
  const elapsedMs = Date.now() - battleStartMs;
462
510
  timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
463
511
  }
512
+ const hosts = [];
513
+ const hostBufs = getAllBytes(f, 14);
514
+ for (const hb of hostBufs) {
515
+ try {
516
+ const hf = decodeProto(hb);
517
+ const hostUserId = getIntStr(hf, 1);
518
+ const teamTotalScore = getInt(hf, 3);
519
+ const teamIdx = getInt(hf, 5);
520
+ const contributors = [];
521
+ const containerBuf = getBytes(hf, 4);
522
+ if (containerBuf) {
523
+ const cf = decodeProto(containerBuf);
524
+ const userBufs = getAllBytes(cf, 1);
525
+ for (const ub of userBufs) {
526
+ try {
527
+ const uf = decodeProto(ub);
528
+ contributors.push({
529
+ userId: getIntStr(uf, 1),
530
+ score: getInt(uf, 2),
531
+ nickname: getStr(uf, 3) || ""
532
+ });
533
+ } catch {
534
+ }
535
+ }
536
+ contributors.sort((a, b) => b.score - a.score);
537
+ }
538
+ hosts.push({ hostUserId, teamTotalScore, teamIdx, contributors });
539
+ } catch {
540
+ }
541
+ }
464
542
  return {
465
543
  ...base,
466
544
  type: "battleArmies",
@@ -469,7 +547,163 @@ function parseWebcastMessage(method, payload) {
469
547
  status: battleStatus,
470
548
  battleStartMs,
471
549
  battleDurationSeconds,
472
- timeLeftSeconds
550
+ timeLeftSeconds,
551
+ endTimeMs,
552
+ matchId,
553
+ sessionId,
554
+ serverTsMs,
555
+ sessionTag,
556
+ durationSec: battleDurationSeconds,
557
+ secsRemaining,
558
+ hosts
559
+ };
560
+ }
561
+ // Proto: WebcastLinkMicBattleItemCard
562
+ // #2 battleId
563
+ // #3 cardType (2=gloves/crit, 3=mist, 4=match_guide, 10=x2?, 11=x3, ...)
564
+ // #5/#6/#7/#14 inner (location varies with cardType — scan numerically):
565
+ // #1.{
566
+ // #1 effectKey
567
+ // #2 icon block { #1 repeated CDN urls, #2 iconKey, #5 accentColor }
568
+ // #3 activatedAtSec (unix seconds)
569
+ // #4 sender block: #1 sub { #1 userId, #2 nickname, #3 avatar block, #4 uniqueId }
570
+ // #5 durationSec
571
+ // #8 senderUserId (new boosters)
572
+ // #11 endsAtSec
573
+ // #12 senderUserId (old gloves/mist)
574
+ // }
575
+ // #3 comment template msg → #2 commentText
576
+ case "WebcastLinkMicBattleItemCard": {
577
+ const battleId = getIntStr(f, 2);
578
+ const cardType = getInt(f, 3);
579
+ let inner = null;
580
+ for (const fn of [5, 6, 7, 14, 15, 16, 8, 9, 10, 11, 12, 13]) {
581
+ const buf = getBytes(f, fn);
582
+ if (buf) {
583
+ inner = decodeProto(buf);
584
+ break;
585
+ }
586
+ }
587
+ let effectKey = "";
588
+ let effect = "unknown";
589
+ let multiplier = 0;
590
+ let activatedAtSec = 0;
591
+ let durationSec = 0;
592
+ let endsAtSec = 0;
593
+ let senderUserId = "";
594
+ let senderNickname = "";
595
+ let senderUniqueId = "";
596
+ let senderAvatarUrl = "";
597
+ let commentTemplate = "";
598
+ let iconUrl = "";
599
+ let iconKey = "";
600
+ let accentColor = "";
601
+ if (inner) {
602
+ let nameBuf = getBytes(inner, 1);
603
+ let nf = nameBuf ? decodeProto(nameBuf) : [];
604
+ let probedKey = nf.length ? getStr(nf, 1) || "" : "";
605
+ if (!probedKey) {
606
+ const altBuf = getBytes(inner, 3);
607
+ if (altBuf) {
608
+ const altFields = decodeProto(altBuf);
609
+ const altKey = getStr(altFields, 1) || "";
610
+ if (altKey) {
611
+ nameBuf = altBuf;
612
+ nf = altFields;
613
+ probedKey = altKey;
614
+ }
615
+ }
616
+ }
617
+ if (nameBuf && nf.length) {
618
+ effectKey = probedKey;
619
+ if (effectKey === "pm_mt_boost_crit_name") effect = "hammer";
620
+ else if (effectKey === "pm_mt_boost_glove_name" || effectKey === "pm_mt_boost_gloves_name") effect = "gloves";
621
+ else if (effectKey === "pm_mt_boost_mist_name") effect = "mist";
622
+ else if (effectKey === "pm_mt_match_guide_gotPowerUps") effect = "match_guide";
623
+ else if (effectKey.includes("userSentBooster_no2") || effectKey.includes("gotPowerUp_panel_no2")) {
624
+ effect = "booster_x2";
625
+ multiplier = 2;
626
+ } else if (effectKey.includes("userSentBooster_no3") || effectKey.includes("gotPowerUp_panel_no3")) {
627
+ effect = "booster_x3";
628
+ multiplier = 3;
629
+ } else if (effectKey.includes("thunder")) effect = "thunder";
630
+ else if (effectKey.includes("extra_time") || effectKey.includes("extra_round")) effect = "extra_time";
631
+ else if (effectKey) effect = effectKey;
632
+ activatedAtSec = getInt(nf, 3);
633
+ durationSec = getInt(nf, 5);
634
+ endsAtSec = getInt(nf, 11) || getInt(nf, 7);
635
+ senderUserId = getIntStr(nf, 12);
636
+ if (senderUserId === "0") senderUserId = getIntStr(nf, 8);
637
+ if (senderUserId === "0") senderUserId = getStr(nf, 13) || getStr(nf, 9) || "";
638
+ const iconBuf = getBytes(nf, 2);
639
+ if (iconBuf) {
640
+ const icf = decodeProto(iconBuf);
641
+ const urlBufs = getAllBytes(icf, 1);
642
+ for (const ub of urlBufs) {
643
+ const candidate = ub.toString("utf-8");
644
+ if (candidate.includes("://")) {
645
+ iconUrl = candidate;
646
+ break;
647
+ }
648
+ }
649
+ iconKey = getStr(icf, 2) || "";
650
+ accentColor = getStr(icf, 5) || "";
651
+ }
652
+ const senderOuter = getBytes(nf, 4);
653
+ if (senderOuter) {
654
+ const sf = decodeProto(senderOuter);
655
+ const sub = getBytes(sf, 1);
656
+ if (sub) {
657
+ const su = decodeProto(sub);
658
+ const nickBuf = getBytes(su, 2);
659
+ if (nickBuf) {
660
+ try {
661
+ senderNickname = nickBuf.toString("utf-8");
662
+ } catch {
663
+ }
664
+ }
665
+ senderUniqueId = getStr(su, 4) || "";
666
+ if (!senderUserId || senderUserId === "0") senderUserId = getIntStr(su, 1);
667
+ const avBuf = getBytes(su, 3);
668
+ if (avBuf) {
669
+ const af = decodeProto(avBuf);
670
+ const urlBufs = getAllBytes(af, 1);
671
+ for (const ub of urlBufs) {
672
+ const candidate = ub.toString("utf-8");
673
+ if (candidate.includes("://")) {
674
+ senderAvatarUrl = candidate;
675
+ break;
676
+ }
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+ const commentBuf = getBytes(inner, 3);
683
+ if (commentBuf) {
684
+ const cf = decodeProto(commentBuf);
685
+ commentTemplate = getStr(cf, 2) || "";
686
+ }
687
+ }
688
+ return {
689
+ ...base,
690
+ type: "battleItemCard",
691
+ battleId,
692
+ cardType,
693
+ effect,
694
+ effectKey,
695
+ multiplier,
696
+ senderUserId,
697
+ senderNickname,
698
+ senderUniqueId,
699
+ senderAvatarUrl,
700
+ activatedAtSec,
701
+ durationSec,
702
+ endsAtSec,
703
+ commentTemplate,
704
+ iconUrl,
705
+ iconKey,
706
+ accentColor
473
707
  };
474
708
  }
475
709
  // Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
@@ -651,24 +885,37 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
651
885
  let ttwid = this._presetSessionId;
652
886
  let roomId = this._presetRoomId;
653
887
  let clusterRegion = "";
888
+ let preSignedUrl = "";
889
+ let preSignedCookies = "";
890
+ let preSignedUA = "";
654
891
  if (roomId && ttwid) {
892
+ console.log(`[TikTokLive] Using preset roomId=${roomId} + sessionId (skip TikTok fetch)`);
655
893
  } else if (roomId && !ttwid) {
894
+ console.log(`[TikTokLive] Have roomId=${roomId}, fetching ttwid from sign server...`);
656
895
  try {
657
896
  const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
658
897
  method: "POST",
659
898
  headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
660
899
  body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
661
900
  });
901
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
662
902
  if (credResp.ok) {
663
903
  const credData = await credResp.json();
664
- if (credData.data?.cookies) {
665
- const m = credData.data.cookies.match(/ttwid=([^;]+)/);
666
- if (m) ttwid = m[1];
904
+ if (credData.status_code === 0 && credData.data?.signed_url) {
905
+ preSignedUrl = credData.data.signed_url;
906
+ preSignedCookies = credData.data.cookies || "";
907
+ preSignedUA = credData.data.user_agent || DEFAULT_UA;
908
+ ttwid = "__presigned__";
909
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got pre-signed URL from sign server`);
910
+ } else {
911
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F ws_credentials bad response: ${JSON.stringify(credData).substring(0, 120)}`);
667
912
  }
668
913
  }
669
- } catch {
914
+ } catch (e) {
915
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
670
916
  }
671
917
  if (!ttwid) {
918
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
672
919
  try {
673
920
  const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
674
921
  "User-Agent": DEFAULT_UA,
@@ -680,35 +927,93 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
680
927
  break;
681
928
  }
682
929
  }
683
- } catch {
930
+ console.log(`[TikTokLive] Direct fetch ttwid: ${ttwid ? "\xE2\u0153\u2026" : "\xE2\x9D\u0152 none"}`);
931
+ } catch (e) {
932
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
684
933
  }
685
934
  }
686
935
  } else {
687
- const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
688
- "User-Agent": DEFAULT_UA,
689
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
690
- "Accept-Encoding": "gzip, deflate, br",
691
- "Accept-Language": "en-US,en;q=0.9"
692
- });
693
- for (const sc of [resp.headers["set-cookie"] || []].flat()) {
694
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
695
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
696
- break;
936
+ console.log(`[TikTokLive] No presets for @${this.uniqueId}. Resolving via sign server (${this.signServerUrl})...`);
937
+ try {
938
+ const roomResp = await fetch(`${this.signServerUrl}/webcast/room_id`, {
939
+ method: "POST",
940
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
941
+ body: JSON.stringify({ unique_id: this.uniqueId })
942
+ });
943
+ console.log(`[TikTokLive] room_id response: ${roomResp.status}`);
944
+ if (roomResp.ok) {
945
+ const roomData = await roomResp.json();
946
+ if (roomData.status_code === 0 && roomData.data?.room_id) {
947
+ roomId = roomData.data.room_id;
948
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got roomId from sign server: ${roomId}`);
949
+ } else {
950
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F room_id response: ${JSON.stringify(roomData).substring(0, 120)}`);
951
+ }
697
952
  }
953
+ } catch (e) {
954
+ console.log(`[TikTokLive] \xE2\x9D\u0152 room_id request failed: ${e.message}`);
698
955
  }
699
- const html = resp.body.toString();
700
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
701
- if (sigiMatch) {
956
+ if (roomId) {
702
957
  try {
703
- const json = JSON.parse(sigiMatch[1]);
704
- const jsonStr = JSON.stringify(json);
705
- const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
706
- if (m) roomId = m[1];
707
- } catch {
958
+ const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
959
+ method: "POST",
960
+ headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
961
+ body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
962
+ });
963
+ console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
964
+ if (credResp.ok) {
965
+ const credData = await credResp.json();
966
+ if (credData.data?.cookies) {
967
+ const m = credData.data.cookies.match(/ttwid=([^;]+)/);
968
+ if (m) {
969
+ ttwid = m[1];
970
+ console.log(`[TikTokLive] \xE2\u0153\u2026 Got ttwid from sign server`);
971
+ } else console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No ttwid in cookies: ${credData.data.cookies.substring(0, 60)}`);
972
+ } else {
973
+ console.log(`[TikTokLive] \xE2\u0161\xA0\xEF\xB8\x8F No cookies in ws_credentials: ${JSON.stringify(credData).substring(0, 120)}`);
974
+ }
975
+ }
976
+ } catch (e) {
977
+ console.log(`[TikTokLive] \xE2\x9D\u0152 ws_credentials failed: ${e.message}`);
978
+ }
979
+ }
980
+ if (!roomId || !ttwid) {
981
+ console.log(`[TikTokLive] Fallback: direct TikTok fetch (roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"})...`);
982
+ try {
983
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
984
+ "User-Agent": DEFAULT_UA,
985
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
986
+ "Accept-Encoding": "gzip, deflate, br",
987
+ "Accept-Language": "en-US,en;q=0.9"
988
+ });
989
+ if (!ttwid) {
990
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
991
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
992
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
993
+ break;
994
+ }
995
+ }
996
+ }
997
+ if (!roomId) {
998
+ const html = resp.body.toString();
999
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
1000
+ if (sigiMatch) {
1001
+ try {
1002
+ const json = JSON.parse(sigiMatch[1]);
1003
+ const jsonStr = JSON.stringify(json);
1004
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
1005
+ if (m) roomId = m[1];
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
1010
+ clusterRegion = crMatch ? crMatch[1] : "";
1011
+ }
1012
+ console.log(`[TikTokLive] Direct fetch result: roomId=${roomId || "none"}, ttwid=${ttwid ? "yes" : "none"}`);
1013
+ } catch (e) {
1014
+ console.log(`[TikTokLive] \xE2\x9D\u0152 Direct TikTok fetch failed: ${e.message}`);
708
1015
  }
709
1016
  }
710
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
711
- clusterRegion = crMatch ? crMatch[1] : "";
712
1017
  }
713
1018
  if (!ttwid) throw new Error("Failed to obtain session cookie");
714
1019
  if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
@@ -766,12 +1071,11 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
766
1071
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
767
1072
  }
768
1073
  return new Promise((resolve, reject) => {
769
- this.ws = new WebSocket(wsUrl, {
770
- headers: {
771
- "User-Agent": DEFAULT_UA,
772
- "Cookie": `ttwid=${ttwid}`,
773
- "Origin": "https://www.tiktok.com"
774
- }
1074
+ const finalWsUrl = preSignedUrl ? preSignedUrl.replace(/^https:\/\//, "wss://") : wsUrl;
1075
+ 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" };
1076
+ console.log(`[TikTokLive] Connecting WS: ${preSignedUrl ? "pre-signed URL" : "self-built URL"}`);
1077
+ this.ws = new WebSocket(finalWsUrl, {
1078
+ headers: finalHeaders
775
1079
  });
776
1080
  this.ws.on("open", () => {
777
1081
  this._connected = true;
@@ -793,18 +1097,21 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
793
1097
  this.handleFrame(Buffer.from(rawData));
794
1098
  });
795
1099
  this.ws.on("close", (code, reason) => {
1100
+ const wasConnected = this._connected;
796
1101
  this._connected = false;
797
1102
  this.stopHeartbeat();
798
1103
  const reasonStr = reason?.toString() || "";
799
1104
  this.emit("disconnected", code, reasonStr);
800
- if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1105
+ if (wasConnected && !this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
801
1106
  this.reconnectAttempts++;
802
1107
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
803
1108
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
804
1109
  }
805
1110
  });
806
1111
  this.ws.on("error", (err) => {
807
- this.emit("error", err);
1112
+ if (this._connected) {
1113
+ this.emit("error", err);
1114
+ }
808
1115
  if (!this._connected) reject(err);
809
1116
  });
810
1117
  });
@@ -821,7 +1128,7 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
821
1128
  }
822
1129
  /**
823
1130
  * Fully destroy the client, releasing all resources and listeners.
824
- * After calling destroy(), the instance cannot be reused — create a new one.
1131
+ * After calling destroy(), the instance cannot be reused — create a new one.
825
1132
  */
826
1133
  destroy() {
827
1134
  this.disconnect();
@@ -846,7 +1153,7 @@ var TikTokLive = class _TikTokLive extends EventEmitter {
846
1153
  /**
847
1154
  * Get live stream video URLs for a TikTok user.
848
1155
  * Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
849
- * This is a standalone method — no WebSocket connection required.
1156
+ * This is a standalone method — no WebSocket connection required.
850
1157
  *
851
1158
  * @example
852
1159
  * ```ts