@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/README.md +426 -235
- package/dist/index.d.mts +78 -5
- package/dist/index.d.ts +78 -5
- package/dist/index.js +365 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +365 -58
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -3
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:
|
|
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
|
-
|
|
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
|
|
439
|
-
for (const
|
|
443
|
+
const hostBufs = getAllBytes(f, 14);
|
|
444
|
+
for (const hb of hostBufs) {
|
|
440
445
|
try {
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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,
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
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
|
|
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?.
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
736
|
-
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
737
|
-
if (sigiMatch) {
|
|
992
|
+
if (roomId) {
|
|
738
993
|
try {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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.
|
|
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
|
|
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
|
|
1192
|
+
* This is a standalone method — no WebSocket connection required.
|
|
886
1193
|
*
|
|
887
1194
|
* @example
|
|
888
1195
|
* ```ts
|