@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.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:
|
|
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
|
-
|
|
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
|
|
403
|
-
for (const
|
|
407
|
+
const hostBufs = getAllBytes(f, 14);
|
|
408
|
+
for (const hb of hostBufs) {
|
|
404
409
|
try {
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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,
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
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
|
|
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?.
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
701
|
-
if (sigiMatch) {
|
|
956
|
+
if (roomId) {
|
|
702
957
|
try {
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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.
|
|
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
|
|
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
|
|
1156
|
+
* This is a standalone method — no WebSocket connection required.
|
|
850
1157
|
*
|
|
851
1158
|
* @example
|
|
852
1159
|
* ```ts
|