@tiktool/live 2.6.7 → 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/README.md +25 -323
- package/dist/index.d.mts +83 -707
- package/dist/index.d.ts +83 -707
- package/dist/index.js +429 -998
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +428 -992
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -17
package/dist/index.js
CHANGED
|
@@ -30,12 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
34
|
-
TikTokLive: () => TikTokLive,
|
|
35
|
-
fetchFeed: () => fetchFeed,
|
|
36
|
-
getLiveFeed: () => getLiveFeed,
|
|
37
|
-
getRanklist: () => getRanklist,
|
|
38
|
-
getRegionalRanklist: () => getRegionalRanklist
|
|
33
|
+
TikTokLive: () => TikTokLive
|
|
39
34
|
});
|
|
40
35
|
module.exports = __toCommonJS(index_exports);
|
|
41
36
|
|
|
@@ -44,94 +39,9 @@ var import_events = require("events");
|
|
|
44
39
|
var http = __toESM(require("http"));
|
|
45
40
|
var https = __toESM(require("https"));
|
|
46
41
|
var zlib = __toESM(require("zlib"));
|
|
47
|
-
var import_url = require("url");
|
|
48
42
|
var import_ws = __toESM(require("ws"));
|
|
49
43
|
|
|
50
44
|
// src/proto.ts
|
|
51
|
-
var encoder = new TextEncoder();
|
|
52
|
-
var decoder = new TextDecoder();
|
|
53
|
-
var TIKTOK_EMOTE_MAP = {
|
|
54
|
-
// Standard emojis
|
|
55
|
-
"happy": "\u{1F60A}",
|
|
56
|
-
"angry": "\u{1F621}",
|
|
57
|
-
"cry": "\u{1F622}",
|
|
58
|
-
"embarrassed": "\u{1F633}",
|
|
59
|
-
"surprised": "\u{1F62E}",
|
|
60
|
-
"wronged": "\u{1F61E}",
|
|
61
|
-
"shout": "\u{1F624}",
|
|
62
|
-
"flushed": "\u{1F633}",
|
|
63
|
-
"yummy": "\u{1F60B}",
|
|
64
|
-
"complacent": "\u{1F60C}",
|
|
65
|
-
"drool": "\u{1F924}",
|
|
66
|
-
"scream": "\u{1F631}",
|
|
67
|
-
"weep": "\u{1F62D}",
|
|
68
|
-
"speechless": "\u{1F636}",
|
|
69
|
-
"funnyface": "\u{1F92A}",
|
|
70
|
-
"laughwithtears": "\u{1F602}",
|
|
71
|
-
"wicked": "\u{1F608}",
|
|
72
|
-
"facewithrollingeyes": "\u{1F644}",
|
|
73
|
-
"sulk": "\u{1F612}",
|
|
74
|
-
"thinking": "\u{1F914}",
|
|
75
|
-
"lovely": "\u{1F970}",
|
|
76
|
-
"greedy": "\u{1F911}",
|
|
77
|
-
"wow": "\u{1F62F}",
|
|
78
|
-
"joyful": "\u{1F603}",
|
|
79
|
-
"hehe": "\u{1F601}",
|
|
80
|
-
"slap": "\u{1F44B}",
|
|
81
|
-
"tears": "\u{1F63F}",
|
|
82
|
-
"stun": "\u{1F635}",
|
|
83
|
-
"cute": "\u{1F97A}",
|
|
84
|
-
"blink": "\u{1F609}",
|
|
85
|
-
"disdain": "\u{1F60F}",
|
|
86
|
-
"astonish": "\u{1F632}",
|
|
87
|
-
"cool": "\u{1F60E}",
|
|
88
|
-
"excited": "\u{1F929}",
|
|
89
|
-
"proud": "\u{1F624}",
|
|
90
|
-
"smileface": "\u{1F60A}",
|
|
91
|
-
"evil": "\u{1F47F}",
|
|
92
|
-
"angel": "\u{1F607}",
|
|
93
|
-
"laugh": "\u{1F606}",
|
|
94
|
-
"pride": "\u{1F981}",
|
|
95
|
-
"nap": "\u{1F634}",
|
|
96
|
-
"loveface": "\u{1F60D}",
|
|
97
|
-
"awkward": "\u{1F62C}",
|
|
98
|
-
"shock": "\u{1F628}",
|
|
99
|
-
"funny": "\u{1F604}",
|
|
100
|
-
"rage": "\u{1F92C}",
|
|
101
|
-
// Common aliases used in TikTok LIVE
|
|
102
|
-
"laughcry": "\u{1F602}",
|
|
103
|
-
"heart": "\u2764\uFE0F",
|
|
104
|
-
"like": "\u{1F44D}",
|
|
105
|
-
"love": "\u{1F495}",
|
|
106
|
-
"shy": "\u{1F648}",
|
|
107
|
-
"smile": "\u{1F60A}"
|
|
108
|
-
};
|
|
109
|
-
function replaceEmotes(text, imageMap) {
|
|
110
|
-
return text.replace(/\[([a-zA-Z_]+)\]/g, (match, name) => {
|
|
111
|
-
const lower = name.toLowerCase();
|
|
112
|
-
const emoji = TIKTOK_EMOTE_MAP[lower];
|
|
113
|
-
return emoji || match;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function concatBytes(...arrays) {
|
|
117
|
-
let totalLength = 0;
|
|
118
|
-
for (const arr of arrays) totalLength += arr.length;
|
|
119
|
-
const result = new Uint8Array(totalLength);
|
|
120
|
-
let offset = 0;
|
|
121
|
-
for (const arr of arrays) {
|
|
122
|
-
result.set(arr, offset);
|
|
123
|
-
offset += arr.length;
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
|
-
function readInt32LE(buf, offset) {
|
|
128
|
-
return buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24;
|
|
129
|
-
}
|
|
130
|
-
function readBigInt64LE(buf, offset) {
|
|
131
|
-
const lo = BigInt(buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24 >>> 0);
|
|
132
|
-
const hi = BigInt(buf[offset + 4] | buf[offset + 5] << 8 | buf[offset + 6] << 16 | buf[offset + 7] << 24 >>> 0);
|
|
133
|
-
return hi << 32n | lo & 0xFFFFFFFFn;
|
|
134
|
-
}
|
|
135
45
|
function decodeVarint(buf, offset) {
|
|
136
46
|
let result = 0, shift = 0;
|
|
137
47
|
while (offset < buf.length) {
|
|
@@ -161,15 +71,15 @@ function encodeVarint(v) {
|
|
|
161
71
|
if (n > 0n) b |= 128;
|
|
162
72
|
bytes.push(b);
|
|
163
73
|
} while (n > 0n);
|
|
164
|
-
return
|
|
74
|
+
return Buffer.from(bytes);
|
|
165
75
|
}
|
|
166
76
|
function encodeField(fn, wt, value) {
|
|
167
77
|
const tag = encodeVarint(fn << 3 | wt);
|
|
168
78
|
if (wt === 0) {
|
|
169
|
-
return
|
|
79
|
+
return Buffer.concat([tag, encodeVarint(typeof value === "number" ? BigInt(value) : value)]);
|
|
170
80
|
}
|
|
171
|
-
const data = typeof value === "string" ?
|
|
172
|
-
return
|
|
81
|
+
const data = typeof value === "string" ? Buffer.from(value) : value;
|
|
82
|
+
return Buffer.concat([tag, encodeVarint(data.length), data]);
|
|
173
83
|
}
|
|
174
84
|
function decodeProto(buf) {
|
|
175
85
|
const fields = [];
|
|
@@ -190,10 +100,10 @@ function decodeProto(buf) {
|
|
|
190
100
|
offset += lenR.value;
|
|
191
101
|
fields.push({ fn, wt, value: data });
|
|
192
102
|
} else if (wt === 1) {
|
|
193
|
-
fields.push({ fn, wt, value: readBigInt64LE(
|
|
103
|
+
fields.push({ fn, wt, value: buf.readBigInt64LE(offset) });
|
|
194
104
|
offset += 8;
|
|
195
105
|
} else if (wt === 5) {
|
|
196
|
-
fields.push({ fn, wt, value: BigInt(readInt32LE(
|
|
106
|
+
fields.push({ fn, wt, value: BigInt(buf.readInt32LE(offset)) });
|
|
197
107
|
offset += 4;
|
|
198
108
|
} else {
|
|
199
109
|
break;
|
|
@@ -203,7 +113,7 @@ function decodeProto(buf) {
|
|
|
203
113
|
}
|
|
204
114
|
function getStr(fields, fn) {
|
|
205
115
|
const f = fields.find((x) => x.fn === fn && x.wt === 2);
|
|
206
|
-
return f ?
|
|
116
|
+
return f ? f.value.toString("utf-8") : "";
|
|
207
117
|
}
|
|
208
118
|
function getBytes(fields, fn) {
|
|
209
119
|
const f = fields.find((x) => x.fn === fn && x.wt === 2);
|
|
@@ -215,47 +125,47 @@ function getInt(fields, fn) {
|
|
|
215
125
|
}
|
|
216
126
|
function getIntStr(fields, fn) {
|
|
217
127
|
const f = fields.find((x) => x.fn === fn && x.wt === 0);
|
|
218
|
-
return f ? String(f.value) : "";
|
|
128
|
+
return f ? String(f.value) : "0";
|
|
219
129
|
}
|
|
220
130
|
function getAllBytes(fields, fn) {
|
|
221
131
|
return fields.filter((x) => x.fn === fn && x.wt === 2).map((x) => x.value);
|
|
222
132
|
}
|
|
223
133
|
function buildHeartbeat(roomId) {
|
|
224
134
|
const payload = encodeField(1, 0, BigInt(roomId));
|
|
225
|
-
return
|
|
135
|
+
return Buffer.concat([
|
|
226
136
|
encodeField(6, 2, "pb"),
|
|
227
137
|
encodeField(7, 2, "hb"),
|
|
228
138
|
encodeField(8, 2, payload)
|
|
229
|
-
);
|
|
139
|
+
]);
|
|
230
140
|
}
|
|
231
141
|
function buildImEnterRoom(roomId) {
|
|
232
|
-
const inner =
|
|
142
|
+
const inner = Buffer.concat([
|
|
233
143
|
encodeField(1, 0, BigInt(roomId)),
|
|
234
144
|
encodeField(4, 0, 12n),
|
|
235
145
|
encodeField(5, 2, "audience"),
|
|
236
146
|
encodeField(6, 2, ""),
|
|
237
147
|
encodeField(9, 2, ""),
|
|
238
148
|
encodeField(10, 2, "")
|
|
239
|
-
);
|
|
240
|
-
return
|
|
149
|
+
]);
|
|
150
|
+
return Buffer.concat([
|
|
241
151
|
encodeField(6, 2, "pb"),
|
|
242
152
|
encodeField(7, 2, "im_enter_room"),
|
|
243
153
|
encodeField(8, 2, inner)
|
|
244
|
-
);
|
|
154
|
+
]);
|
|
245
155
|
}
|
|
246
156
|
function buildAck(id) {
|
|
247
|
-
return
|
|
157
|
+
return Buffer.concat([
|
|
248
158
|
encodeField(2, 0, id),
|
|
249
159
|
encodeField(6, 2, "pb"),
|
|
250
160
|
encodeField(7, 2, "ack")
|
|
251
|
-
);
|
|
161
|
+
]);
|
|
252
162
|
}
|
|
253
163
|
function looksLikeUsername(s) {
|
|
254
164
|
return s.length > 0 && s.length <= 50 && !s.includes("://") && !s.includes("\0");
|
|
255
165
|
}
|
|
256
166
|
function parseUser(data) {
|
|
257
167
|
const f = decodeProto(data);
|
|
258
|
-
const id = getIntStr(f, 1)
|
|
168
|
+
const id = getIntStr(f, 1) !== "0" ? getIntStr(f, 1) : getStr(f, 1);
|
|
259
169
|
const nickname = getStr(f, 3) || getStr(f, 2);
|
|
260
170
|
let uniqueId = "";
|
|
261
171
|
const uid4 = getStr(f, 4);
|
|
@@ -277,7 +187,7 @@ function parseUser(data) {
|
|
|
277
187
|
const avatarFields = decodeProto(avatarBuf);
|
|
278
188
|
const urlBufs = getAllBytes(avatarFields, 1);
|
|
279
189
|
for (const urlBuf of urlBufs) {
|
|
280
|
-
const url =
|
|
190
|
+
const url = urlBuf.toString("utf-8");
|
|
281
191
|
if (url.includes("://")) {
|
|
282
192
|
profilePicture = url;
|
|
283
193
|
break;
|
|
@@ -286,25 +196,44 @@ function parseUser(data) {
|
|
|
286
196
|
} catch {
|
|
287
197
|
}
|
|
288
198
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
for (const badgeBuf of badgeBufs) {
|
|
199
|
+
let avatarLargeUrl;
|
|
200
|
+
const largeBuf = getBytes(f, 11);
|
|
201
|
+
if (largeBuf) {
|
|
293
202
|
try {
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
203
|
+
const largeFields = decodeProto(largeBuf);
|
|
204
|
+
const urlBufs = getAllBytes(largeFields, 1);
|
|
205
|
+
for (const urlBuf of urlBufs) {
|
|
206
|
+
const url = urlBuf.toString("utf-8");
|
|
207
|
+
if (url.includes("://")) {
|
|
208
|
+
avatarLargeUrl = url;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!avatarLargeUrl) {
|
|
216
|
+
const medBuf = getBytes(f, 10);
|
|
217
|
+
if (medBuf) {
|
|
218
|
+
try {
|
|
219
|
+
const medFields = decodeProto(medBuf);
|
|
220
|
+
const urlBufs = getAllBytes(medFields, 1);
|
|
221
|
+
for (const urlBuf of urlBufs) {
|
|
222
|
+
const url = urlBuf.toString("utf-8");
|
|
223
|
+
if (url.includes("://")) {
|
|
224
|
+
avatarLargeUrl = url;
|
|
225
|
+
break;
|
|
305
226
|
}
|
|
306
227
|
}
|
|
228
|
+
} catch {
|
|
307
229
|
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const badges = [];
|
|
233
|
+
const badgeBuf = getBytes(f, 64);
|
|
234
|
+
if (badgeBuf) {
|
|
235
|
+
try {
|
|
236
|
+
const badgeFields = decodeProto(badgeBuf);
|
|
308
237
|
const badgeItems = getAllBytes(badgeFields, 21);
|
|
309
238
|
for (const bi of badgeItems) {
|
|
310
239
|
const bf = decodeProto(bi);
|
|
@@ -318,17 +247,16 @@ function parseUser(data) {
|
|
|
318
247
|
id,
|
|
319
248
|
nickname,
|
|
320
249
|
uniqueId: uniqueId || nickname || id,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
250
|
+
profilePicture,
|
|
251
|
+
avatarLargeUrl,
|
|
252
|
+
badges: badges.length > 0 ? badges : void 0
|
|
324
253
|
};
|
|
325
254
|
}
|
|
326
255
|
function parseBattleTeamFromArmies(itemBuf) {
|
|
327
256
|
const f = decodeProto(itemBuf);
|
|
328
|
-
const hostUserId = getIntStr(f, 1)
|
|
257
|
+
const hostUserId = getIntStr(f, 1);
|
|
329
258
|
let teamScore = 0;
|
|
330
259
|
const users = [];
|
|
331
|
-
let hostUser;
|
|
332
260
|
const groups = getAllBytes(f, 2);
|
|
333
261
|
for (const gb of groups) {
|
|
334
262
|
try {
|
|
@@ -336,10 +264,9 @@ function parseBattleTeamFromArmies(itemBuf) {
|
|
|
336
264
|
const points = getInt(gf, 2);
|
|
337
265
|
teamScore += points;
|
|
338
266
|
const userBufs = getAllBytes(gf, 1);
|
|
267
|
+
const individualScore = userBufs.length === 1 ? points : -1;
|
|
339
268
|
for (const ub of userBufs) {
|
|
340
269
|
try {
|
|
341
|
-
const userFields = decodeProto(ub);
|
|
342
|
-
const individualScore = getInt(userFields, 2);
|
|
343
270
|
const user = parseUser(ub);
|
|
344
271
|
users.push({ user, score: individualScore });
|
|
345
272
|
} catch {
|
|
@@ -348,19 +275,7 @@ function parseBattleTeamFromArmies(itemBuf) {
|
|
|
348
275
|
} catch {
|
|
349
276
|
}
|
|
350
277
|
}
|
|
351
|
-
|
|
352
|
-
if (hostUser) break;
|
|
353
|
-
const buf = getBytes(f, fieldNum);
|
|
354
|
-
if (!buf) continue;
|
|
355
|
-
try {
|
|
356
|
-
const parsed = parseUser(buf);
|
|
357
|
-
if (parsed && (parsed.nickname || parsed.uniqueId) && parsed.uniqueId !== parsed.id && looksLikeUsername(parsed.uniqueId || "")) {
|
|
358
|
-
hostUser = parsed;
|
|
359
|
-
}
|
|
360
|
-
} catch {
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return { hostUserId, score: teamScore, users, hostUser };
|
|
278
|
+
return { hostUserId, score: teamScore, users };
|
|
364
279
|
}
|
|
365
280
|
function parseWebcastMessage(method, payload) {
|
|
366
281
|
const f = decodeProto(payload);
|
|
@@ -375,29 +290,42 @@ function parseWebcastMessage(method, payload) {
|
|
|
375
290
|
}
|
|
376
291
|
}
|
|
377
292
|
switch (method) {
|
|
293
|
+
// Proto: WebcastChatMessage { MessageType type=1, User user=2, string comment=3, ..., repeated WebcastSubEmote emotes=12 }
|
|
294
|
+
// WebcastSubEmote { int32 placeInComment=1, EmoteDetails emote=2 }
|
|
295
|
+
// EmoteDetails { string emoteId=1, EmoteImage image=2 }
|
|
296
|
+
// EmoteImage { string imageUrl=1 }
|
|
378
297
|
case "WebcastChatMessage": {
|
|
379
298
|
const userBuf = getBytes(f, 2);
|
|
380
299
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
300
|
+
const comment = getStr(f, 3);
|
|
301
|
+
const emotes = [];
|
|
302
|
+
const emoteBufs12 = getAllBytes(f, 12);
|
|
303
|
+
const emoteBufs13 = getAllBytes(f, 13);
|
|
304
|
+
const emoteBufs = emoteBufs13.length > 0 ? emoteBufs13 : emoteBufs12;
|
|
305
|
+
for (const eb of emoteBufs) {
|
|
306
|
+
try {
|
|
307
|
+
const ef = decodeProto(eb);
|
|
308
|
+
const placeInComment = getInt(ef, 1);
|
|
309
|
+
const detailBuf = getBytes(ef, 2);
|
|
310
|
+
if (detailBuf) {
|
|
311
|
+
const df = decodeProto(detailBuf);
|
|
312
|
+
const emoteId = getStr(df, 1);
|
|
313
|
+
let imageUrl = "";
|
|
314
|
+
const imgBuf = getBytes(df, 2);
|
|
315
|
+
if (imgBuf) {
|
|
316
|
+
const imgf = decodeProto(imgBuf);
|
|
317
|
+
imageUrl = getStr(imgf, 1);
|
|
318
|
+
}
|
|
319
|
+
if (emoteId) {
|
|
320
|
+
emotes.push({ emoteId, imageUrl, placeInComment });
|
|
393
321
|
}
|
|
394
|
-
} catch {
|
|
395
322
|
}
|
|
323
|
+
} catch {
|
|
396
324
|
}
|
|
397
325
|
}
|
|
398
|
-
|
|
399
|
-
return { ...base, type: "chat", user, comment };
|
|
326
|
+
return { ...base, type: "chat", user, comment, emotes };
|
|
400
327
|
}
|
|
328
|
+
// Proto: WebcastMemberMessage { User user=2, WebcastMessageEvent event=1 }
|
|
401
329
|
case "WebcastMemberMessage": {
|
|
402
330
|
const userBuf = getBytes(f, 2);
|
|
403
331
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -415,6 +343,7 @@ function parseWebcastMessage(method, payload) {
|
|
|
415
343
|
}
|
|
416
344
|
return { ...base, type: "member", user, action };
|
|
417
345
|
}
|
|
346
|
+
// Proto: WebcastLikeMessage { User user=5, WebcastMessageEvent event=1, int32 likeCount=2, int32 totalLikeCount=3 }
|
|
418
347
|
case "WebcastLikeMessage": {
|
|
419
348
|
const userBuf = getBytes(f, 5);
|
|
420
349
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -426,6 +355,13 @@ function parseWebcastMessage(method, payload) {
|
|
|
426
355
|
totalLikes: getInt(f, 3)
|
|
427
356
|
};
|
|
428
357
|
}
|
|
358
|
+
// Proto: WebcastGiftMessage {
|
|
359
|
+
// User user=7, int32 giftId=2, int32 repeatCount=5, int32 repeatEnd=9,
|
|
360
|
+
// GiftDetails giftDetails=15, GiftExtra giftExtra=23
|
|
361
|
+
// }
|
|
362
|
+
// GiftDetails { GiftImage giftImage=1, string giftName=16, string describe=2,
|
|
363
|
+
// int32 giftType=11, int32 diamondCount=12 }
|
|
364
|
+
// GiftExtra { uint64 timestamp=6, uint64 toUserId=8 }
|
|
429
365
|
case "WebcastGiftMessage": {
|
|
430
366
|
const userBuf = getBytes(f, 7);
|
|
431
367
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -450,7 +386,7 @@ function parseWebcastMessage(method, payload) {
|
|
|
450
386
|
const extraBuf = getBytes(f, 23);
|
|
451
387
|
if (extraBuf) {
|
|
452
388
|
const ef = decodeProto(extraBuf);
|
|
453
|
-
toUserId =
|
|
389
|
+
toUserId = String(getInt(ef, 8));
|
|
454
390
|
}
|
|
455
391
|
const groupId = toUserId || getStr(f, 11);
|
|
456
392
|
return {
|
|
@@ -464,10 +400,10 @@ function parseWebcastMessage(method, payload) {
|
|
|
464
400
|
repeatEnd,
|
|
465
401
|
combo: repeatCount > 1 && !repeatEnd,
|
|
466
402
|
giftType,
|
|
467
|
-
groupId
|
|
468
|
-
giftPictureUrl: giftImageUrl
|
|
403
|
+
groupId
|
|
469
404
|
};
|
|
470
405
|
}
|
|
406
|
+
// Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
|
|
471
407
|
case "WebcastSocialMessage": {
|
|
472
408
|
const userBuf = getBytes(f, 2);
|
|
473
409
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -487,42 +423,19 @@ function parseWebcastMessage(method, payload) {
|
|
|
487
423
|
}
|
|
488
424
|
return { ...base, type: "social", user, action };
|
|
489
425
|
}
|
|
426
|
+
// Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
|
|
490
427
|
case "WebcastRoomUserSeqMessage": {
|
|
491
428
|
const viewerCount = getInt(f, 3) || getInt(f, 2);
|
|
492
429
|
const totalViewers = getInt(f, 1) || viewerCount;
|
|
493
430
|
return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
|
|
494
431
|
}
|
|
432
|
+
// Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
|
|
433
|
+
// battleUsers → { battleGroup=2 → { LinkUser user=1 } }
|
|
495
434
|
case "WebcastLinkMicBattle": {
|
|
496
|
-
const battleId =
|
|
497
|
-
const
|
|
435
|
+
const battleId = String(getInt(f, 1) || "");
|
|
436
|
+
const status = getInt(f, 2) || 1;
|
|
437
|
+
const battleDuration = getInt(f, 3);
|
|
498
438
|
const teams = [];
|
|
499
|
-
let battleDuration = 0;
|
|
500
|
-
let battleSettings;
|
|
501
|
-
const settingsBuf = getBytes(f, 3);
|
|
502
|
-
if (settingsBuf) {
|
|
503
|
-
try {
|
|
504
|
-
const sf = decodeProto(settingsBuf);
|
|
505
|
-
const startTimeMs = getInt(sf, 2);
|
|
506
|
-
const duration = getInt(sf, 3);
|
|
507
|
-
const phase = getInt(sf, 5);
|
|
508
|
-
const endTimeMs = getInt(sf, 10);
|
|
509
|
-
battleDuration = duration;
|
|
510
|
-
battleSettings = {
|
|
511
|
-
startTimeMs: startTimeMs || void 0,
|
|
512
|
-
duration: duration || void 0,
|
|
513
|
-
endTimeMs: endTimeMs || void 0
|
|
514
|
-
};
|
|
515
|
-
} catch {
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
const settingsPhase = settingsBuf ? (() => {
|
|
519
|
-
try {
|
|
520
|
-
return getInt(decodeProto(settingsBuf), 5);
|
|
521
|
-
} catch {
|
|
522
|
-
return 0;
|
|
523
|
-
}
|
|
524
|
-
})() : 0;
|
|
525
|
-
const status = settingsPhase || (overallStatus > 0 && overallStatus <= 10 ? overallStatus : 0) || 1;
|
|
526
439
|
const battleUserBufs = getAllBytes(f, 10);
|
|
527
440
|
for (const bub of battleUserBufs) {
|
|
528
441
|
try {
|
|
@@ -543,23 +456,6 @@ function parseWebcastMessage(method, payload) {
|
|
|
543
456
|
} catch {
|
|
544
457
|
}
|
|
545
458
|
}
|
|
546
|
-
if (teams.length === 0) {
|
|
547
|
-
const teamBufs5 = getAllBytes(f, 5);
|
|
548
|
-
for (const tb of teamBufs5) {
|
|
549
|
-
try {
|
|
550
|
-
const tf = decodeProto(tb);
|
|
551
|
-
const userId = getIntStr(tf, 1);
|
|
552
|
-
if (userId && userId !== "0") {
|
|
553
|
-
teams.push({
|
|
554
|
-
hostUserId: userId,
|
|
555
|
-
score: 0,
|
|
556
|
-
users: []
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
} catch {
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
459
|
if (teams.length === 0) {
|
|
564
460
|
const teamBufs7 = getAllBytes(f, 7);
|
|
565
461
|
for (const tb of teamBufs7) {
|
|
@@ -569,48 +465,54 @@ function parseWebcastMessage(method, payload) {
|
|
|
569
465
|
}
|
|
570
466
|
}
|
|
571
467
|
}
|
|
572
|
-
return { ...base, type: "battle", battleId, status, battleDuration, teams
|
|
468
|
+
return { ...base, type: "battle", battleId, status, battleDuration, teams };
|
|
573
469
|
}
|
|
470
|
+
// Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
|
|
471
|
+
// battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
|
|
472
|
+
// battleItems → { hostUserId=1, repeated battleGroups=2 }
|
|
473
|
+
// battleGroups → { repeated users=1, int32 points=2 }
|
|
574
474
|
case "WebcastLinkMicArmies": {
|
|
575
|
-
const battleId =
|
|
475
|
+
const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
|
|
576
476
|
const battleStatus = getInt(f, 7);
|
|
577
477
|
const teams = [];
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
if (settingsBuf18) {
|
|
478
|
+
const itemBufs = getAllBytes(f, 3);
|
|
479
|
+
for (const ib of itemBufs) {
|
|
581
480
|
try {
|
|
582
|
-
|
|
583
|
-
const startTimeMs = getInt(sf, 2);
|
|
584
|
-
const duration = getInt(sf, 3);
|
|
585
|
-
const endTimeMs = getInt(sf, 10);
|
|
586
|
-
battleSettings = {
|
|
587
|
-
startTimeMs: startTimeMs || void 0,
|
|
588
|
-
duration: duration || void 0,
|
|
589
|
-
endTimeMs: endTimeMs || void 0
|
|
590
|
-
};
|
|
481
|
+
teams.push(parseBattleTeamFromArmies(ib));
|
|
591
482
|
} catch {
|
|
592
483
|
}
|
|
593
484
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
485
|
+
let battleStartMs = 0;
|
|
486
|
+
let battleDurationSeconds = 0;
|
|
487
|
+
const configBuf = getBytes(f, 18);
|
|
488
|
+
if (configBuf) {
|
|
598
489
|
try {
|
|
599
|
-
|
|
490
|
+
const cf = decodeProto(configBuf);
|
|
491
|
+
battleStartMs = getInt(cf, 2);
|
|
492
|
+
battleDurationSeconds = getInt(cf, 3);
|
|
600
493
|
} catch {
|
|
601
494
|
}
|
|
602
495
|
}
|
|
496
|
+
let endTimeMs = 0;
|
|
497
|
+
let timeLeftSeconds = 0;
|
|
498
|
+
if (battleStartMs > 0 && battleDurationSeconds > 0) {
|
|
499
|
+
endTimeMs = battleStartMs + battleDurationSeconds * 1e3;
|
|
500
|
+
const elapsedMs = Date.now() - battleStartMs;
|
|
501
|
+
timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
|
|
502
|
+
}
|
|
603
503
|
return {
|
|
604
504
|
...base,
|
|
605
505
|
type: "battleArmies",
|
|
606
506
|
battleId,
|
|
607
507
|
teams,
|
|
608
508
|
status: battleStatus,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
509
|
+
battleStartMs,
|
|
510
|
+
battleDurationSeconds,
|
|
511
|
+
timeLeftSeconds,
|
|
512
|
+
endTimeMs
|
|
612
513
|
};
|
|
613
514
|
}
|
|
515
|
+
// Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
|
|
614
516
|
case "WebcastSubNotifyMessage": {
|
|
615
517
|
const userBuf = getBytes(f, 2);
|
|
616
518
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -636,6 +538,8 @@ function parseWebcastMessage(method, payload) {
|
|
|
636
538
|
const envelopeId = String(getInt(f, 1) || getStr(f, 1));
|
|
637
539
|
return { ...base, type: "envelope", envelopeId, diamondCount: getInt(f, 3) };
|
|
638
540
|
}
|
|
541
|
+
// Proto: WebcastQuestionNewMessage { MessageType type=1, QuestionDetails questionDetails=2 }
|
|
542
|
+
// QuestionDetails { string questionText=2, User user=5 }
|
|
639
543
|
case "WebcastQuestionNewMessage": {
|
|
640
544
|
let questionText = "", user = { id: "0", nickname: "", uniqueId: "" };
|
|
641
545
|
const detailBuf = getBytes(f, 2);
|
|
@@ -665,17 +569,20 @@ function parseWebcastMessage(method, payload) {
|
|
|
665
569
|
}
|
|
666
570
|
return { ...base, type: "rankUpdate", rankType, rankList };
|
|
667
571
|
}
|
|
572
|
+
// Proto: WebcastControlMessage { int32 action=2 }
|
|
668
573
|
case "WebcastControlMessage":
|
|
669
574
|
return { ...base, type: "control", action: getInt(f, 2) || getInt(f, 1) };
|
|
670
575
|
case "WebcastRoomMessage":
|
|
671
576
|
case "RoomMessage":
|
|
672
577
|
return { ...base, type: "room", status: getInt(f, 2) };
|
|
578
|
+
// Proto: WebcastLiveIntroMessage { uint64 id=2, string description=4, User user=5 }
|
|
673
579
|
case "WebcastLiveIntroMessage": {
|
|
674
580
|
const roomId = String(getInt(f, 2));
|
|
675
581
|
const title = getStr(f, 4) || getStr(f, 2);
|
|
676
582
|
return { ...base, type: "liveIntro", roomId, title };
|
|
677
583
|
}
|
|
678
|
-
case "WebcastLinkMicMethod":
|
|
584
|
+
case "WebcastLinkMicMethod":
|
|
585
|
+
case "WebcastLinkmicBattleTaskMessage": {
|
|
679
586
|
const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
|
|
680
587
|
const users = [];
|
|
681
588
|
const userBufs = getAllBytes(f, 2);
|
|
@@ -687,152 +594,6 @@ function parseWebcastMessage(method, payload) {
|
|
|
687
594
|
}
|
|
688
595
|
return { ...base, type: "linkMic", action, users };
|
|
689
596
|
}
|
|
690
|
-
case "WebcastLinkmicBattleTaskMessage": {
|
|
691
|
-
const taskAction = getInt(f, 2);
|
|
692
|
-
const battleRefId = getIntStr(f, 20) || "";
|
|
693
|
-
let timerType = 0;
|
|
694
|
-
let remainingSeconds = 0;
|
|
695
|
-
let endTimestampS = 0;
|
|
696
|
-
const timerBuf = getBytes(f, 5);
|
|
697
|
-
if (timerBuf) {
|
|
698
|
-
try {
|
|
699
|
-
const tf = decodeProto(timerBuf);
|
|
700
|
-
timerType = getInt(tf, 1);
|
|
701
|
-
remainingSeconds = getInt(tf, 2);
|
|
702
|
-
endTimestampS = getInt(tf, 3);
|
|
703
|
-
} catch {
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
let multiplier = 0;
|
|
707
|
-
let missionDuration = 0;
|
|
708
|
-
let missionTarget = 0;
|
|
709
|
-
let missionType = "";
|
|
710
|
-
const bonusBuf = getBytes(f, 3);
|
|
711
|
-
if (bonusBuf) {
|
|
712
|
-
try {
|
|
713
|
-
const bf = decodeProto(bonusBuf);
|
|
714
|
-
const missionBuf = getBytes(bf, 1);
|
|
715
|
-
if (missionBuf) {
|
|
716
|
-
const mf = decodeProto(missionBuf);
|
|
717
|
-
missionTarget = getInt(mf, 1);
|
|
718
|
-
const items = getAllBytes(mf, 2);
|
|
719
|
-
for (const item of items) {
|
|
720
|
-
try {
|
|
721
|
-
const itemFields = decodeProto(item);
|
|
722
|
-
const descBuf = getBytes(itemFields, 2);
|
|
723
|
-
if (descBuf) {
|
|
724
|
-
const descFields = decodeProto(descBuf);
|
|
725
|
-
const paramBufs = getAllBytes(descFields, 2);
|
|
726
|
-
for (const pb of paramBufs) {
|
|
727
|
-
try {
|
|
728
|
-
const pf = decodeProto(pb);
|
|
729
|
-
const key = getStr(pf, 1);
|
|
730
|
-
const val = getStr(pf, 2);
|
|
731
|
-
if (key === "multi" && val) multiplier = parseInt(val) || 0;
|
|
732
|
-
if (key === "dur" && val) missionDuration = parseInt(val) || 0;
|
|
733
|
-
if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
|
|
734
|
-
} catch {
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
const typeStr = getStr(descFields, 1);
|
|
738
|
-
if (typeStr) missionType = typeStr;
|
|
739
|
-
}
|
|
740
|
-
} catch {
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const countdownBuf = getBytes(mf, 3);
|
|
744
|
-
if (countdownBuf) {
|
|
745
|
-
try {
|
|
746
|
-
const cf = decodeProto(countdownBuf);
|
|
747
|
-
if (!missionDuration) missionDuration = getInt(cf, 2);
|
|
748
|
-
} catch {
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
} catch {
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (!missionType) {
|
|
756
|
-
const descBuf6 = getBytes(f, 6);
|
|
757
|
-
if (descBuf6) {
|
|
758
|
-
try {
|
|
759
|
-
const d6 = decodeProto(descBuf6);
|
|
760
|
-
const innerBuf = getBytes(d6, 1);
|
|
761
|
-
if (innerBuf) {
|
|
762
|
-
const innerF = decodeProto(innerBuf);
|
|
763
|
-
const typeStr = getStr(innerF, 1);
|
|
764
|
-
if (typeStr) missionType = typeStr;
|
|
765
|
-
const paramBufs = getAllBytes(innerF, 2);
|
|
766
|
-
for (const pb of paramBufs) {
|
|
767
|
-
try {
|
|
768
|
-
const pf = decodeProto(pb);
|
|
769
|
-
const key = getStr(pf, 1);
|
|
770
|
-
const val = getStr(pf, 2);
|
|
771
|
-
if (key === "multi" && val) multiplier = parseInt(val) || 0;
|
|
772
|
-
if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
|
|
773
|
-
} catch {
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
} catch {
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return {
|
|
782
|
-
...base,
|
|
783
|
-
type: "battleTask",
|
|
784
|
-
taskAction,
|
|
785
|
-
battleRefId,
|
|
786
|
-
missionType,
|
|
787
|
-
multiplier,
|
|
788
|
-
missionDuration,
|
|
789
|
-
missionTarget,
|
|
790
|
-
remainingSeconds,
|
|
791
|
-
endTimestampS,
|
|
792
|
-
timerType
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
case "WebcastBarrageMessage": {
|
|
796
|
-
const msgType = getInt(f, 3);
|
|
797
|
-
const duration = getInt(f, 4);
|
|
798
|
-
const displayType = getInt(f, 5);
|
|
799
|
-
const subType = getInt(f, 6);
|
|
800
|
-
let defaultPattern = "";
|
|
801
|
-
let content = "";
|
|
802
|
-
const contentBuf = getBytes(f, 2);
|
|
803
|
-
if (contentBuf) {
|
|
804
|
-
try {
|
|
805
|
-
const cf = decodeProto(contentBuf);
|
|
806
|
-
defaultPattern = getStr(cf, 1) || "";
|
|
807
|
-
const paramBufs = getAllBytes(cf, 2);
|
|
808
|
-
const params = [];
|
|
809
|
-
for (const pb of paramBufs) {
|
|
810
|
-
try {
|
|
811
|
-
const pf = decodeProto(pb);
|
|
812
|
-
const key = getStr(pf, 1);
|
|
813
|
-
const val = getStr(pf, 2);
|
|
814
|
-
if (key && val) params.push(`${key}=${val}`);
|
|
815
|
-
} catch {
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
if (params.length > 0) content = params.join(", ");
|
|
819
|
-
} catch {
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
if (!defaultPattern) {
|
|
823
|
-
defaultPattern = getStr(f, 1) || "";
|
|
824
|
-
}
|
|
825
|
-
return {
|
|
826
|
-
...base,
|
|
827
|
-
type: "barrage",
|
|
828
|
-
msgType,
|
|
829
|
-
subType,
|
|
830
|
-
displayType,
|
|
831
|
-
duration,
|
|
832
|
-
defaultPattern,
|
|
833
|
-
content
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
597
|
default:
|
|
837
598
|
return { ...base, type: "unknown", method };
|
|
838
599
|
}
|
|
@@ -857,12 +618,10 @@ function parseWebcastResponse(payload) {
|
|
|
857
618
|
// src/client.ts
|
|
858
619
|
var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
859
620
|
var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
|
|
860
|
-
function httpGet(url, headers
|
|
621
|
+
function httpGet(url, headers) {
|
|
861
622
|
return new Promise((resolve, reject) => {
|
|
862
623
|
const mod = url.startsWith("https") ? https : http;
|
|
863
|
-
const
|
|
864
|
-
const parsed = new import_url.URL(url);
|
|
865
|
-
const req = mod.get(parsed, opts, (res) => {
|
|
624
|
+
const req = mod.get(url, { headers }, (res) => {
|
|
866
625
|
const chunks = [];
|
|
867
626
|
const enc = res.headers["content-encoding"];
|
|
868
627
|
const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
|
|
@@ -888,15 +647,21 @@ function getWsHost(clusterRegion) {
|
|
|
888
647
|
if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
|
|
889
648
|
return "webcast-ws.tiktok.com";
|
|
890
649
|
}
|
|
891
|
-
var TikTokLive = class extends import_events.EventEmitter {
|
|
650
|
+
var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
|
|
892
651
|
ws = null;
|
|
893
652
|
heartbeatTimer = null;
|
|
894
653
|
reconnectAttempts = 0;
|
|
895
654
|
intentionalClose = false;
|
|
896
655
|
_connected = false;
|
|
656
|
+
_destroyed = false;
|
|
897
657
|
_eventCount = 0;
|
|
898
658
|
_roomId = "";
|
|
899
|
-
|
|
659
|
+
// Cache host identities from battle events for enriching battleArmies
|
|
660
|
+
// Bounded to MAX_BATTLE_HOSTS entries to prevent unbounded memory growth
|
|
661
|
+
static MAX_BATTLE_HOSTS = 50;
|
|
662
|
+
_battleHosts = /* @__PURE__ */ new Map();
|
|
663
|
+
// Track in-flight host resolution requests to avoid duplicate API calls
|
|
664
|
+
_pendingHostResolves = /* @__PURE__ */ new Set();
|
|
900
665
|
uniqueId;
|
|
901
666
|
signServerUrl;
|
|
902
667
|
apiKey;
|
|
@@ -904,13 +669,11 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
904
669
|
maxReconnectAttempts;
|
|
905
670
|
heartbeatInterval;
|
|
906
671
|
debug;
|
|
907
|
-
_sessionId;
|
|
908
|
-
_ttTargetIdc;
|
|
909
|
-
proxyAgent;
|
|
910
672
|
_presetRoomId;
|
|
911
|
-
|
|
673
|
+
_presetSessionId;
|
|
912
674
|
constructor(options) {
|
|
913
675
|
super();
|
|
676
|
+
this.setMaxListeners(20);
|
|
914
677
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
915
678
|
this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
916
679
|
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
@@ -919,56 +682,148 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
919
682
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
920
683
|
this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
|
|
921
684
|
this.debug = options.debug ?? false;
|
|
922
|
-
this.
|
|
923
|
-
this.
|
|
924
|
-
if (options.agent) {
|
|
925
|
-
this.proxyAgent = options.agent;
|
|
926
|
-
}
|
|
927
|
-
this._presetRoomId = options.roomId;
|
|
928
|
-
this._presetTtwid = options.ttwid;
|
|
685
|
+
this._presetRoomId = options.roomId || "";
|
|
686
|
+
this._presetSessionId = options.sessionId || "";
|
|
929
687
|
}
|
|
930
688
|
async connect() {
|
|
689
|
+
if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
|
|
931
690
|
this.intentionalClose = false;
|
|
932
|
-
let ttwid = this.
|
|
933
|
-
let roomId = this._presetRoomId
|
|
934
|
-
let ownerUserId = "";
|
|
691
|
+
let ttwid = this._presetSessionId;
|
|
692
|
+
let roomId = this._presetRoomId;
|
|
935
693
|
let clusterRegion = "";
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
694
|
+
let preSignedUrl = "";
|
|
695
|
+
let preSignedCookies = "";
|
|
696
|
+
let preSignedUA = "";
|
|
697
|
+
if (roomId && ttwid) {
|
|
698
|
+
console.log(`[TikTokLive] Using preset roomId=${roomId} + sessionId (skip TikTok fetch)`);
|
|
699
|
+
} else if (roomId && !ttwid) {
|
|
700
|
+
console.log(`[TikTokLive] Have roomId=${roomId}, fetching ttwid from sign server...`);
|
|
701
|
+
try {
|
|
702
|
+
const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
|
|
705
|
+
body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
|
|
706
|
+
});
|
|
707
|
+
console.log(`[TikTokLive] ws_credentials response: ${credResp.status}`);
|
|
708
|
+
if (credResp.ok) {
|
|
709
|
+
const credData = await credResp.json();
|
|
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)}`);
|
|
718
|
+
}
|
|
948
719
|
}
|
|
720
|
+
} catch (e) {
|
|
721
|
+
console.log(`[TikTokLive] \u274C ws_credentials failed: ${e.message}`);
|
|
949
722
|
}
|
|
950
|
-
if (!ttwid)
|
|
951
|
-
|
|
952
|
-
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
953
|
-
if (sigiMatch) {
|
|
723
|
+
if (!ttwid) {
|
|
724
|
+
console.log(`[TikTokLive] Fallback: direct TikTok fetch for ttwid...`);
|
|
954
725
|
try {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
726
|
+
const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
|
|
727
|
+
"User-Agent": DEFAULT_UA,
|
|
728
|
+
"Accept": "text/html"
|
|
729
|
+
});
|
|
730
|
+
for (const sc of [resp.headers["set-cookie"] || []].flat()) {
|
|
731
|
+
if (typeof sc === "string" && sc.startsWith("ttwid=")) {
|
|
732
|
+
ttwid = sc.split(";")[0].split("=").slice(1).join("=");
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
962
735
|
}
|
|
963
|
-
|
|
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}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
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
|
+
}
|
|
758
|
+
}
|
|
759
|
+
} catch (e) {
|
|
760
|
+
console.log(`[TikTokLive] \u274C room_id request failed: ${e.message}`);
|
|
761
|
+
}
|
|
762
|
+
if (roomId) {
|
|
763
|
+
try {
|
|
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}`);
|
|
964
821
|
}
|
|
965
822
|
}
|
|
966
|
-
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
967
|
-
const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
|
|
968
|
-
clusterRegion = crMatch ? crMatch[1] : "";
|
|
969
823
|
}
|
|
824
|
+
if (!ttwid) throw new Error("Failed to obtain session cookie");
|
|
825
|
+
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
970
826
|
this._roomId = roomId;
|
|
971
|
-
this._ownerUserId = ownerUserId;
|
|
972
827
|
const wsHost = getWsHost(clusterRegion);
|
|
973
828
|
const wsParams = new URLSearchParams({
|
|
974
829
|
version_code: "270000",
|
|
@@ -1022,21 +877,11 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1022
877
|
wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
|
|
1023
878
|
}
|
|
1024
879
|
return new Promise((resolve, reject) => {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
cookieHeader += `; tt-target-idc=${this._ttTargetIdc}`;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
this.ws = new import_ws.default(wsUrl, {
|
|
1034
|
-
headers: {
|
|
1035
|
-
"User-Agent": DEFAULT_UA,
|
|
1036
|
-
"Cookie": cookieHeader,
|
|
1037
|
-
"Origin": "https://www.tiktok.com"
|
|
1038
|
-
},
|
|
1039
|
-
agent: this.proxyAgent
|
|
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
|
|
1040
885
|
});
|
|
1041
886
|
this.ws.on("open", () => {
|
|
1042
887
|
this._connected = true;
|
|
@@ -1048,8 +893,7 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1048
893
|
roomId,
|
|
1049
894
|
wsHost,
|
|
1050
895
|
clusterRegion,
|
|
1051
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1052
|
-
ownerUserId: ownerUserId || void 0
|
|
896
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1053
897
|
};
|
|
1054
898
|
this.emit("connected");
|
|
1055
899
|
this.emit("roomInfo", roomInfo);
|
|
@@ -1079,42 +923,76 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1079
923
|
this.intentionalClose = true;
|
|
1080
924
|
this.stopHeartbeat();
|
|
1081
925
|
if (this.ws) {
|
|
926
|
+
this.ws.removeAllListeners();
|
|
1082
927
|
this.ws.close(1e3);
|
|
1083
928
|
this.ws = null;
|
|
1084
929
|
}
|
|
1085
930
|
this._connected = false;
|
|
1086
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Fully destroy the client, releasing all resources and listeners.
|
|
934
|
+
* After calling destroy(), the instance cannot be reused — create a new one.
|
|
935
|
+
*/
|
|
936
|
+
destroy() {
|
|
937
|
+
this.disconnect();
|
|
938
|
+
this._destroyed = true;
|
|
939
|
+
this._battleHosts.clear();
|
|
940
|
+
this._eventCount = 0;
|
|
941
|
+
this._roomId = "";
|
|
942
|
+
this.removeAllListeners();
|
|
943
|
+
}
|
|
1087
944
|
get connected() {
|
|
1088
945
|
return this._connected;
|
|
1089
946
|
}
|
|
947
|
+
get destroyed() {
|
|
948
|
+
return this._destroyed;
|
|
949
|
+
}
|
|
1090
950
|
get eventCount() {
|
|
1091
951
|
return this._eventCount;
|
|
1092
952
|
}
|
|
1093
953
|
get roomId() {
|
|
1094
954
|
return this._roomId;
|
|
1095
955
|
}
|
|
1096
|
-
/** Get the stored session ID (if any) */
|
|
1097
|
-
get sessionId() {
|
|
1098
|
-
return this._sessionId;
|
|
1099
|
-
}
|
|
1100
|
-
/** Update the session ID at runtime (e.g. after TikTok login) */
|
|
1101
|
-
setSession(sessionId, ttTargetIdc) {
|
|
1102
|
-
this._sessionId = sessionId;
|
|
1103
|
-
if (ttTargetIdc) this._ttTargetIdc = ttTargetIdc;
|
|
1104
|
-
}
|
|
1105
956
|
/**
|
|
1106
|
-
*
|
|
1107
|
-
* Returns
|
|
957
|
+
* Get live stream video URLs for a TikTok user.
|
|
958
|
+
* Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
|
|
959
|
+
* This is a standalone method — no WebSocket connection required.
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```ts
|
|
963
|
+
* const stream = await TikTokLive.getStreamUrl({
|
|
964
|
+
* uniqueId: 'username',
|
|
965
|
+
* apiKey: 'your-api-key',
|
|
966
|
+
* });
|
|
967
|
+
* if (stream.alive) {
|
|
968
|
+
* console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
|
|
969
|
+
* console.log(stream.flvPullUrl); // Best FLV URL
|
|
970
|
+
* }
|
|
971
|
+
* ```
|
|
1108
972
|
*/
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
973
|
+
static async getStreamUrl(options) {
|
|
974
|
+
const serverUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
975
|
+
const resp = await fetch(`${serverUrl}/webcast/room_video`, {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: {
|
|
978
|
+
"Content-Type": "application/json",
|
|
979
|
+
"x-api-key": options.apiKey
|
|
980
|
+
},
|
|
981
|
+
body: JSON.stringify({ unique_id: options.uniqueId.replace(/^@/, "") })
|
|
982
|
+
});
|
|
983
|
+
const data = await resp.json();
|
|
984
|
+
if (data.status_code !== 0) {
|
|
985
|
+
throw new Error(data.error || "Failed to get stream URL");
|
|
986
|
+
}
|
|
987
|
+
const d = data.data;
|
|
988
|
+
return {
|
|
989
|
+
roomId: d.room_id,
|
|
990
|
+
alive: d.alive ?? false,
|
|
991
|
+
streamUrls: d.stream_urls || {},
|
|
992
|
+
defaultQuality: d.default_quality || "",
|
|
993
|
+
flvPullUrl: d.flv_pull_url || void 0,
|
|
994
|
+
hlsPullUrl: d.hls_pull_url || void 0
|
|
995
|
+
};
|
|
1118
996
|
}
|
|
1119
997
|
on(event, listener) {
|
|
1120
998
|
return super.on(event, listener);
|
|
@@ -1149,567 +1027,120 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1149
1027
|
const events = parseWebcastResponse(inner);
|
|
1150
1028
|
for (const evt of events) {
|
|
1151
1029
|
this._eventCount++;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
}, this.heartbeatInterval);
|
|
1166
|
-
}
|
|
1167
|
-
stopHeartbeat() {
|
|
1168
|
-
if (this.heartbeatTimer) {
|
|
1169
|
-
clearInterval(this.heartbeatTimer);
|
|
1170
|
-
this.heartbeatTimer = null;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
|
|
1175
|
-
// src/captions.ts
|
|
1176
|
-
var import_events2 = require("events");
|
|
1177
|
-
var import_ws2 = __toESM(require("ws"));
|
|
1178
|
-
var FLV_TAG_AUDIO = 8;
|
|
1179
|
-
var FLV_HEADER_SIZE = 9;
|
|
1180
|
-
var FLV_PREV_TAG_SIZE = 4;
|
|
1181
|
-
var FlvAudioExtractor = class {
|
|
1182
|
-
buffer = new Uint8Array(0);
|
|
1183
|
-
headerParsed = false;
|
|
1184
|
-
onAudio;
|
|
1185
|
-
aacProfile = 2;
|
|
1186
|
-
sampleRateIndex = 4;
|
|
1187
|
-
channelConfig = 2;
|
|
1188
|
-
ascParsed = false;
|
|
1189
|
-
constructor(onAudio) {
|
|
1190
|
-
this.onAudio = onAudio;
|
|
1191
|
-
}
|
|
1192
|
-
parseASC(asc) {
|
|
1193
|
-
if (asc.length < 2) return;
|
|
1194
|
-
this.aacProfile = asc[0] >> 3 & 31;
|
|
1195
|
-
this.sampleRateIndex = (asc[0] & 7) << 1 | asc[1] >> 7 & 1;
|
|
1196
|
-
this.channelConfig = asc[1] >> 3 & 15;
|
|
1197
|
-
this.ascParsed = true;
|
|
1198
|
-
}
|
|
1199
|
-
buildAdtsHeader(frameLength) {
|
|
1200
|
-
const adts = new Uint8Array(7);
|
|
1201
|
-
const fullLength = frameLength + 7;
|
|
1202
|
-
const profile = this.aacProfile - 1;
|
|
1203
|
-
adts[0] = 255;
|
|
1204
|
-
adts[1] = 241;
|
|
1205
|
-
adts[2] = (profile & 3) << 6 | (this.sampleRateIndex & 15) << 2 | this.channelConfig >> 2 & 1;
|
|
1206
|
-
adts[3] = (this.channelConfig & 3) << 6 | fullLength >> 11 & 3;
|
|
1207
|
-
adts[4] = fullLength >> 3 & 255;
|
|
1208
|
-
adts[5] = (fullLength & 7) << 5 | 31;
|
|
1209
|
-
adts[6] = 252;
|
|
1210
|
-
return adts;
|
|
1211
|
-
}
|
|
1212
|
-
push(chunk) {
|
|
1213
|
-
const newBuf = new Uint8Array(this.buffer.length + chunk.length);
|
|
1214
|
-
newBuf.set(this.buffer, 0);
|
|
1215
|
-
newBuf.set(chunk, this.buffer.length);
|
|
1216
|
-
this.buffer = newBuf;
|
|
1217
|
-
if (!this.headerParsed) {
|
|
1218
|
-
if (this.buffer.length < FLV_HEADER_SIZE + FLV_PREV_TAG_SIZE) return;
|
|
1219
|
-
if (this.buffer[0] !== 70 || this.buffer[1] !== 76 || this.buffer[2] !== 86) return;
|
|
1220
|
-
const dv = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
|
|
1221
|
-
const dataOffset = dv.getUint32(5);
|
|
1222
|
-
this.buffer = this.buffer.subarray(dataOffset + FLV_PREV_TAG_SIZE);
|
|
1223
|
-
this.headerParsed = true;
|
|
1224
|
-
}
|
|
1225
|
-
while (this.buffer.length >= 11) {
|
|
1226
|
-
const tagType = this.buffer[0] & 31;
|
|
1227
|
-
const dataSize = this.buffer[1] << 16 | this.buffer[2] << 8 | this.buffer[3];
|
|
1228
|
-
const totalTagSize = 11 + dataSize + FLV_PREV_TAG_SIZE;
|
|
1229
|
-
if (this.buffer.length < totalTagSize) break;
|
|
1230
|
-
if (tagType === FLV_TAG_AUDIO) {
|
|
1231
|
-
const audioData = this.buffer.subarray(11, 11 + dataSize);
|
|
1232
|
-
if (audioData.length > 0) {
|
|
1233
|
-
const soundFormat = audioData[0] >> 4 & 15;
|
|
1234
|
-
if (soundFormat === 10 && audioData.length > 2) {
|
|
1235
|
-
const aacPacketType = audioData[1];
|
|
1236
|
-
if (aacPacketType === 0) {
|
|
1237
|
-
this.parseASC(audioData.subarray(2));
|
|
1238
|
-
} else if (aacPacketType === 1 && this.ascParsed) {
|
|
1239
|
-
const rawFrame = audioData.subarray(2);
|
|
1240
|
-
const adtsHeader = this.buildAdtsHeader(rawFrame.length);
|
|
1241
|
-
const adtsFrame = new Uint8Array(adtsHeader.length + rawFrame.length);
|
|
1242
|
-
adtsFrame.set(adtsHeader, 0);
|
|
1243
|
-
adtsFrame.set(rawFrame, adtsHeader.length);
|
|
1244
|
-
this.onAudio(adtsFrame);
|
|
1030
|
+
if (evt.type === "battle") {
|
|
1031
|
+
for (const team of evt.teams) {
|
|
1032
|
+
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
1033
|
+
if (host) {
|
|
1034
|
+
this._battleHosts.set(team.hostUserId, host.user);
|
|
1035
|
+
team.hostUser = host.user;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (this._battleHosts.size > _TikTokLive.MAX_BATTLE_HOSTS) {
|
|
1039
|
+
const excess = this._battleHosts.size - _TikTokLive.MAX_BATTLE_HOSTS;
|
|
1040
|
+
const keys = [...this._battleHosts.keys()].slice(0, excess);
|
|
1041
|
+
for (const key of keys) this._battleHosts.delete(key);
|
|
1245
1042
|
}
|
|
1246
1043
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
maxReconnectAttempts;
|
|
1269
|
-
debug;
|
|
1270
|
-
_translate;
|
|
1271
|
-
_diarization;
|
|
1272
|
-
_maxDurationMinutes;
|
|
1273
|
-
_language;
|
|
1274
|
-
streamAbortController = null;
|
|
1275
|
-
flvExtractor = null;
|
|
1276
|
-
streamUrl = null;
|
|
1277
|
-
constructor(options) {
|
|
1278
|
-
super();
|
|
1279
|
-
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
1280
|
-
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
1281
|
-
this.apiKey = options.apiKey;
|
|
1282
|
-
this._language = options.language || "";
|
|
1283
|
-
this._translate = options.translate || "";
|
|
1284
|
-
this._diarization = options.diarization ?? true;
|
|
1285
|
-
this._maxDurationMinutes = options.maxDurationMinutes ?? 60;
|
|
1286
|
-
this.serverUrl = (options.signServerUrl || DEFAULT_CAPTIONS_SERVER).replace(/\/$/, "");
|
|
1287
|
-
this.autoReconnect = options.autoReconnect ?? true;
|
|
1288
|
-
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
1289
|
-
this.debug = options.debug ?? false;
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Start real-time captions for the configured TikTok user.
|
|
1293
|
-
* Connects to the captions WebSocket relay and begins transcription
|
|
1294
|
-
* once the user goes live (or immediately if already live).
|
|
1295
|
-
*/
|
|
1296
|
-
async start() {
|
|
1297
|
-
this.intentionalClose = false;
|
|
1298
|
-
const wsUrl = this.buildWsUrl();
|
|
1299
|
-
if (this.debug) console.log(`[Captions] Connecting to ${wsUrl}`);
|
|
1300
|
-
return new Promise((resolve, reject) => {
|
|
1301
|
-
this.ws = new import_ws2.default(wsUrl);
|
|
1302
|
-
this.ws.on("open", () => {
|
|
1303
|
-
this._connected = true;
|
|
1304
|
-
this.reconnectAttempts = 0;
|
|
1305
|
-
if (this.debug) console.log("[Captions] Connected");
|
|
1306
|
-
this.emit("connected");
|
|
1307
|
-
resolve();
|
|
1308
|
-
});
|
|
1309
|
-
this.ws.on("message", (data) => {
|
|
1310
|
-
this.handleMessage(typeof data === "string" ? data : data.toString());
|
|
1311
|
-
});
|
|
1312
|
-
this.ws.on("close", (code, reason) => {
|
|
1313
|
-
this._connected = false;
|
|
1314
|
-
const reasonStr = reason?.toString() || "";
|
|
1315
|
-
if (this.debug) console.log(`[Captions] Disconnected: ${code} ${reasonStr}`);
|
|
1316
|
-
this.emit("disconnected", code, reasonStr);
|
|
1317
|
-
if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
1318
|
-
this.reconnectAttempts++;
|
|
1319
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
1320
|
-
if (this.debug) console.log(`[Captions] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
1321
|
-
setTimeout(() => this.start().catch((e) => this.emit("error", { code: "RECONNECT_FAILED", message: e.message })), delay);
|
|
1322
|
-
}
|
|
1323
|
-
});
|
|
1324
|
-
this.ws.on("error", (err) => {
|
|
1325
|
-
this.emit("error", { code: "WS_ERROR", message: err.message });
|
|
1326
|
-
if (!this._connected) reject(err);
|
|
1327
|
-
});
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Stop captions and disconnect.
|
|
1332
|
-
*/
|
|
1333
|
-
stop() {
|
|
1334
|
-
this.intentionalClose = true;
|
|
1335
|
-
if (this.streamAbortController) {
|
|
1336
|
-
this.streamAbortController.abort();
|
|
1337
|
-
this.streamAbortController = null;
|
|
1338
|
-
}
|
|
1339
|
-
if (this.flvExtractor) {
|
|
1340
|
-
this.flvExtractor.reset();
|
|
1341
|
-
this.flvExtractor = null;
|
|
1342
|
-
}
|
|
1343
|
-
if (this.ws) {
|
|
1344
|
-
this.send({ action: "stop" });
|
|
1345
|
-
this.ws.close(1e3);
|
|
1346
|
-
this.ws = null;
|
|
1347
|
-
}
|
|
1348
|
-
this._connected = false;
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Switch the translation target language on-the-fly.
|
|
1352
|
-
* Causes a brief interruption while the transcription engine reconfigures.
|
|
1353
|
-
*/
|
|
1354
|
-
setLanguage(language) {
|
|
1355
|
-
this._language = language;
|
|
1356
|
-
this.send({ action: "set_language", language });
|
|
1357
|
-
}
|
|
1358
|
-
/**
|
|
1359
|
-
* Request a credit balance update from the server.
|
|
1360
|
-
*/
|
|
1361
|
-
getCredits() {
|
|
1362
|
-
this.send({ action: "get_credits" });
|
|
1363
|
-
}
|
|
1364
|
-
/** Whether the WebSocket is currently connected */
|
|
1365
|
-
get connected() {
|
|
1366
|
-
return this._connected;
|
|
1367
|
-
}
|
|
1368
|
-
/** The current target language */
|
|
1369
|
-
get language() {
|
|
1370
|
-
return this._language;
|
|
1371
|
-
}
|
|
1372
|
-
on(event, listener) {
|
|
1373
|
-
return super.on(event, listener);
|
|
1374
|
-
}
|
|
1375
|
-
once(event, listener) {
|
|
1376
|
-
return super.once(event, listener);
|
|
1377
|
-
}
|
|
1378
|
-
off(event, listener) {
|
|
1379
|
-
return super.off(event, listener);
|
|
1380
|
-
}
|
|
1381
|
-
emit(event, ...args) {
|
|
1382
|
-
return super.emit(event, ...args);
|
|
1383
|
-
}
|
|
1384
|
-
buildWsUrl() {
|
|
1385
|
-
const base = this.serverUrl.replace(/^http/, "ws");
|
|
1386
|
-
const params = new URLSearchParams({
|
|
1387
|
-
uniqueId: this.uniqueId,
|
|
1388
|
-
apiKey: this.apiKey
|
|
1389
|
-
});
|
|
1390
|
-
if (this._language) params.set("language", this._language);
|
|
1391
|
-
if (this._translate) params.set("translate", this._translate);
|
|
1392
|
-
if (this._diarization !== void 0) params.set("diarization", String(this._diarization));
|
|
1393
|
-
if (this._maxDurationMinutes) params.set("max_duration_minutes", String(this._maxDurationMinutes));
|
|
1394
|
-
return `${base}/captions?${params}`;
|
|
1395
|
-
}
|
|
1396
|
-
send(msg) {
|
|
1397
|
-
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
1398
|
-
this.ws.send(JSON.stringify(msg));
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
handleMessage(raw) {
|
|
1402
|
-
try {
|
|
1403
|
-
const msg = JSON.parse(raw);
|
|
1404
|
-
switch (msg.type) {
|
|
1405
|
-
case "stream_info":
|
|
1406
|
-
if (this.debug) console.log(`[Captions] Received stream_info: flv=${!!msg.flvUrl}, hls=${!!msg.hlsUrl}, ao=${!!msg.audioOnlyUrl}`);
|
|
1407
|
-
this.connectToStream(msg);
|
|
1408
|
-
break;
|
|
1409
|
-
case "caption":
|
|
1410
|
-
this.emit("caption", {
|
|
1411
|
-
text: msg.text,
|
|
1412
|
-
language: msg.language,
|
|
1413
|
-
isFinal: msg.isFinal,
|
|
1414
|
-
confidence: msg.confidence,
|
|
1415
|
-
speaker: msg.speaker,
|
|
1416
|
-
startMs: msg.startMs,
|
|
1417
|
-
endMs: msg.endMs
|
|
1418
|
-
});
|
|
1419
|
-
break;
|
|
1420
|
-
case "translation":
|
|
1421
|
-
this.emit("translation", {
|
|
1422
|
-
text: msg.text,
|
|
1423
|
-
language: msg.language,
|
|
1424
|
-
isFinal: msg.isFinal,
|
|
1425
|
-
confidence: msg.confidence,
|
|
1426
|
-
speaker: msg.speaker
|
|
1427
|
-
});
|
|
1428
|
-
break;
|
|
1429
|
-
case "status":
|
|
1430
|
-
this.emit("status", {
|
|
1431
|
-
status: msg.status,
|
|
1432
|
-
uniqueId: msg.uniqueId,
|
|
1433
|
-
roomId: msg.roomId,
|
|
1434
|
-
language: msg.language,
|
|
1435
|
-
message: msg.message
|
|
1436
|
-
});
|
|
1437
|
-
break;
|
|
1438
|
-
case "credits":
|
|
1439
|
-
this.emit("credits", {
|
|
1440
|
-
remaining: msg.remaining,
|
|
1441
|
-
total: msg.total,
|
|
1442
|
-
used: msg.used,
|
|
1443
|
-
warning: msg.warning
|
|
1444
|
-
});
|
|
1445
|
-
break;
|
|
1446
|
-
case "credits_low":
|
|
1447
|
-
this.emit("credits_low", {
|
|
1448
|
-
remaining: msg.remaining,
|
|
1449
|
-
total: msg.total,
|
|
1450
|
-
percent: msg.percent
|
|
1451
|
-
});
|
|
1452
|
-
break;
|
|
1453
|
-
case "error":
|
|
1454
|
-
this.emit("error", {
|
|
1455
|
-
code: msg.code,
|
|
1456
|
-
message: msg.message
|
|
1457
|
-
});
|
|
1458
|
-
break;
|
|
1459
|
-
// Handle interim/final captions from server (sentence-level accumulation)
|
|
1460
|
-
case "interim":
|
|
1461
|
-
this.emit("caption", {
|
|
1462
|
-
text: msg.text,
|
|
1463
|
-
language: msg.language,
|
|
1464
|
-
isFinal: false,
|
|
1465
|
-
confidence: msg.confidence || 0,
|
|
1466
|
-
speaker: msg.speaker
|
|
1467
|
-
});
|
|
1468
|
-
break;
|
|
1469
|
-
case "final":
|
|
1470
|
-
this.emit("caption", {
|
|
1471
|
-
text: msg.text,
|
|
1472
|
-
language: msg.language,
|
|
1473
|
-
isFinal: true,
|
|
1474
|
-
confidence: msg.confidence || 0,
|
|
1475
|
-
speaker: msg.speaker
|
|
1476
|
-
});
|
|
1477
|
-
break;
|
|
1478
|
-
case "translation_interim":
|
|
1479
|
-
this.emit("translation", {
|
|
1480
|
-
text: msg.text,
|
|
1481
|
-
language: msg.language,
|
|
1482
|
-
isFinal: false,
|
|
1483
|
-
confidence: msg.confidence || 0,
|
|
1484
|
-
speaker: msg.speaker
|
|
1485
|
-
});
|
|
1486
|
-
break;
|
|
1487
|
-
case "translation_final":
|
|
1488
|
-
this.emit("translation", {
|
|
1489
|
-
text: msg.text,
|
|
1490
|
-
language: msg.language,
|
|
1491
|
-
isFinal: true,
|
|
1492
|
-
confidence: msg.confidence || 0,
|
|
1493
|
-
speaker: msg.speaker
|
|
1494
|
-
});
|
|
1495
|
-
break;
|
|
1496
|
-
default:
|
|
1497
|
-
if (this.debug) {
|
|
1498
|
-
if (this.debug) console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
|
|
1044
|
+
if (evt.type === "battleArmies") {
|
|
1045
|
+
const unresolvedIds = [];
|
|
1046
|
+
for (const team of evt.teams) {
|
|
1047
|
+
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
1048
|
+
if (host) {
|
|
1049
|
+
team.hostUser = host.user;
|
|
1050
|
+
this._battleHosts.set(team.hostUserId, host.user);
|
|
1051
|
+
} else {
|
|
1052
|
+
const cached = this._battleHosts.get(team.hostUserId);
|
|
1053
|
+
if (cached) {
|
|
1054
|
+
team.hostUser = cached;
|
|
1055
|
+
} else if (team.hostUserId && team.hostUserId !== "0") {
|
|
1056
|
+
unresolvedIds.push(team.hostUserId);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (unresolvedIds.length > 0) {
|
|
1061
|
+
this.resolveHostUsers(unresolvedIds, evt).catch(() => {
|
|
1062
|
+
});
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1499
1065
|
}
|
|
1066
|
+
this.emit("event", evt);
|
|
1067
|
+
this.emit(evt.type, evt);
|
|
1068
|
+
}
|
|
1500
1069
|
}
|
|
1501
1070
|
} catch {
|
|
1502
|
-
if (this.debug) console.error("[Captions] Failed to parse message:", raw);
|
|
1503
1071
|
}
|
|
1504
1072
|
}
|
|
1505
1073
|
/**
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1074
|
+
* Resolve unknown host user IDs via the sign server API.
|
|
1075
|
+
* Caches results and re-emits the battleArmies event with enriched hostUser data.
|
|
1508
1076
|
*/
|
|
1509
|
-
async
|
|
1510
|
-
const
|
|
1511
|
-
if (
|
|
1512
|
-
this.emit("
|
|
1077
|
+
async resolveHostUsers(userIds, originalEvt) {
|
|
1078
|
+
const toResolve = userIds.filter((id) => !this._pendingHostResolves.has(id));
|
|
1079
|
+
if (toResolve.length === 0) {
|
|
1080
|
+
this.emit("event", originalEvt);
|
|
1081
|
+
this.emit(originalEvt.type, originalEvt);
|
|
1513
1082
|
return;
|
|
1514
1083
|
}
|
|
1515
|
-
this.
|
|
1516
|
-
if (this.debug) console.log(`[Captions] connectToStream: URL selected: ${url.substring(0, 80)}...`);
|
|
1517
|
-
if (this.streamAbortController) {
|
|
1518
|
-
this.streamAbortController.abort();
|
|
1519
|
-
}
|
|
1520
|
-
this.streamAbortController = new AbortController();
|
|
1521
|
-
let audioFramesSent = 0;
|
|
1522
|
-
let audioBytesSent = 0;
|
|
1523
|
-
this.flvExtractor = new FlvAudioExtractor((adtsFrame) => {
|
|
1524
|
-
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
1525
|
-
this.ws.send(adtsFrame);
|
|
1526
|
-
audioFramesSent++;
|
|
1527
|
-
audioBytesSent += adtsFrame.length;
|
|
1528
|
-
if (this.debug && (audioFramesSent <= 3 || audioFramesSent % 100 === 0)) {
|
|
1529
|
-
if (this.debug) console.log(`[Captions] Audio frame #${audioFramesSent}: ${adtsFrame.length}b (total: ${audioBytesSent}b)`);
|
|
1530
|
-
}
|
|
1531
|
-
} else if (this.debug && audioFramesSent === 0) {
|
|
1532
|
-
if (this.debug) console.log(`[Captions] WARNING: WS not open (readyState=${this.ws?.readyState}), cannot send audio`);
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1084
|
+
for (const id of toResolve) this._pendingHostResolves.add(id);
|
|
1535
1085
|
try {
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1086
|
+
const url = `${this.signServerUrl}/webcast/resolve_user_ids`;
|
|
1087
|
+
const res = await fetch(url, {
|
|
1088
|
+
method: "POST",
|
|
1539
1089
|
headers: {
|
|
1540
|
-
"
|
|
1541
|
-
|
|
1090
|
+
"Content-Type": "application/json",
|
|
1091
|
+
"x-api-key": this.apiKey
|
|
1092
|
+
},
|
|
1093
|
+
body: JSON.stringify({ user_ids: toResolve }),
|
|
1094
|
+
signal: AbortSignal.timeout(5e3)
|
|
1542
1095
|
});
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const { done, value } = await reader.read();
|
|
1556
|
-
if (done || this.intentionalClose) {
|
|
1557
|
-
if (this.debug) console.log(`[Captions] FLV stream ended (done=${done}, intentionalClose=${this.intentionalClose}), chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1558
|
-
break;
|
|
1559
|
-
}
|
|
1560
|
-
chunks++;
|
|
1561
|
-
if (value && this.flvExtractor) {
|
|
1562
|
-
this.flvExtractor.push(value);
|
|
1563
|
-
}
|
|
1564
|
-
if (this.debug && chunks <= 3) {
|
|
1565
|
-
if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${value?.length || 0}b`);
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
} catch (err) {
|
|
1569
|
-
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1570
|
-
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1571
|
-
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1572
|
-
} else if (this.debug) {
|
|
1573
|
-
if (this.debug) console.log(`[Captions] FLV stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1096
|
+
if (res.ok) {
|
|
1097
|
+
const json = await res.json();
|
|
1098
|
+
if (json.data) {
|
|
1099
|
+
for (const [uid, info] of Object.entries(json.data)) {
|
|
1100
|
+
if (info.username) {
|
|
1101
|
+
const hostUser = {
|
|
1102
|
+
id: uid,
|
|
1103
|
+
uniqueId: info.username,
|
|
1104
|
+
nickname: info.nickname || info.username,
|
|
1105
|
+
profilePicture: info.avatarUrl || void 0
|
|
1106
|
+
};
|
|
1107
|
+
this._battleHosts.set(uid, hostUser);
|
|
1574
1108
|
}
|
|
1575
1109
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
try {
|
|
1582
|
-
for await (const chunk of resp.body) {
|
|
1583
|
-
if (this.intentionalClose) break;
|
|
1584
|
-
chunks++;
|
|
1585
|
-
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
1586
|
-
if (this.flvExtractor) {
|
|
1587
|
-
this.flvExtractor.push(u8);
|
|
1588
|
-
}
|
|
1589
|
-
if (this.debug && chunks <= 3) {
|
|
1590
|
-
if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
|
|
1110
|
+
if (originalEvt.type === "battleArmies") {
|
|
1111
|
+
for (const team of originalEvt.teams) {
|
|
1112
|
+
if (!team.hostUser) {
|
|
1113
|
+
const cached = this._battleHosts.get(team.hostUserId);
|
|
1114
|
+
if (cached) team.hostUser = cached;
|
|
1591
1115
|
}
|
|
1592
1116
|
}
|
|
1593
|
-
if (this.debug) console.log(`[Captions] Node stream ended, chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1594
|
-
} catch (err) {
|
|
1595
|
-
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1596
|
-
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1597
|
-
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1598
|
-
} else if (this.debug) {
|
|
1599
|
-
if (this.debug) console.log(`[Captions] FLV node stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1600
|
-
}
|
|
1601
1117
|
}
|
|
1602
|
-
}
|
|
1603
|
-
processNodeStream();
|
|
1604
|
-
} else {
|
|
1605
|
-
if (this.debug) console.error(`[Captions] ERROR: resp.body has no getReader() and no asyncIterator!`);
|
|
1606
|
-
}
|
|
1607
|
-
} catch (err) {
|
|
1608
|
-
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1609
|
-
if (this.debug) console.error("[Captions] FLV stream connect error:", err.message);
|
|
1610
|
-
this.emit("error", { code: "STREAM_CONNECT_ERROR", message: err.message });
|
|
1118
|
+
}
|
|
1611
1119
|
}
|
|
1120
|
+
} catch {
|
|
1121
|
+
} finally {
|
|
1122
|
+
for (const id of toResolve) this._pendingHostResolves.delete(id);
|
|
1123
|
+
this.emit("event", originalEvt);
|
|
1124
|
+
this.emit(originalEvt.type, originalEvt);
|
|
1612
1125
|
}
|
|
1613
1126
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
const ak = encodeURIComponent(opts.apiKey);
|
|
1622
|
-
const body = {};
|
|
1623
|
-
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1624
|
-
if (opts.roomId) body.room_id = opts.roomId;
|
|
1625
|
-
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1626
|
-
if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
|
|
1627
|
-
if (opts.type) body.type = opts.type;
|
|
1628
|
-
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1629
|
-
const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
|
|
1630
|
-
method: "POST",
|
|
1631
|
-
headers: { "Content-Type": "application/json" },
|
|
1632
|
-
body: JSON.stringify(body)
|
|
1633
|
-
});
|
|
1634
|
-
const data = await resp.json();
|
|
1635
|
-
if (data.status_code === 20003) {
|
|
1636
|
-
throw new Error(
|
|
1637
|
-
data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
if (data.status_code !== 0) {
|
|
1641
|
-
throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
|
|
1642
|
-
}
|
|
1643
|
-
if (data.sign_and_return) {
|
|
1644
|
-
return data;
|
|
1645
|
-
}
|
|
1646
|
-
return data.data;
|
|
1647
|
-
}
|
|
1648
|
-
async function getRegionalRanklist(opts) {
|
|
1649
|
-
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1650
|
-
const ak = encodeURIComponent(opts.apiKey);
|
|
1651
|
-
const body = {};
|
|
1652
|
-
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1653
|
-
if (opts.roomId) body.room_id = opts.roomId;
|
|
1654
|
-
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1655
|
-
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1656
|
-
if (opts.type) body.type = opts.type;
|
|
1657
|
-
if (opts.gapInterval) body.gap_interval = opts.gapInterval;
|
|
1658
|
-
if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
|
|
1659
|
-
if (opts.clientIp) body.client_ip = opts.clientIp;
|
|
1660
|
-
const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
|
|
1661
|
-
method: "POST",
|
|
1662
|
-
headers: { "Content-Type": "application/json" },
|
|
1663
|
-
body: JSON.stringify(body)
|
|
1664
|
-
});
|
|
1665
|
-
const data = await resp.json();
|
|
1666
|
-
if (data.status_code !== 0) {
|
|
1667
|
-
throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
|
|
1668
|
-
}
|
|
1669
|
-
return data;
|
|
1670
|
-
}
|
|
1671
|
-
async function getLiveFeed(opts) {
|
|
1672
|
-
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1673
|
-
const params = new URLSearchParams();
|
|
1674
|
-
params.set("apiKey", opts.apiKey);
|
|
1675
|
-
if (opts.region) params.set("region", opts.region);
|
|
1676
|
-
if (opts.channelId) params.set("channel_id", opts.channelId);
|
|
1677
|
-
if (opts.count !== void 0) params.set("count", String(Math.min(opts.count, 50)));
|
|
1678
|
-
if (opts.maxTime) params.set("max_time", opts.maxTime);
|
|
1679
|
-
if (opts.sessionId) params.set("session_id", opts.sessionId);
|
|
1680
|
-
if (opts.ttwid) params.set("ttwid", opts.ttwid);
|
|
1681
|
-
if (opts.msToken) params.set("ms_token", opts.msToken);
|
|
1682
|
-
const resp = await fetch(`${base}/webcast/feed?${params.toString()}`);
|
|
1683
|
-
const data = await resp.json();
|
|
1684
|
-
if (resp.status === 429) {
|
|
1685
|
-
throw new Error(data.error || "Feed daily limit reached. Upgrade your plan for more calls.");
|
|
1686
|
-
}
|
|
1687
|
-
if (!resp.ok) {
|
|
1688
|
-
throw new Error(data.error || `Feed request failed (HTTP ${resp.status})`);
|
|
1689
|
-
}
|
|
1690
|
-
return data;
|
|
1691
|
-
}
|
|
1692
|
-
async function fetchFeed(opts) {
|
|
1693
|
-
const signed = await getLiveFeed(opts);
|
|
1694
|
-
const headers = { ...signed.headers || {} };
|
|
1695
|
-
if (signed.cookies) {
|
|
1696
|
-
headers["Cookie"] = signed.cookies;
|
|
1127
|
+
startHeartbeat(roomId) {
|
|
1128
|
+
this.stopHeartbeat();
|
|
1129
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1130
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1131
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
1132
|
+
}
|
|
1133
|
+
}, this.heartbeatInterval);
|
|
1697
1134
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
return null;
|
|
1135
|
+
stopHeartbeat() {
|
|
1136
|
+
if (this.heartbeatTimer) {
|
|
1137
|
+
clearInterval(this.heartbeatTimer);
|
|
1138
|
+
this.heartbeatTimer = null;
|
|
1139
|
+
}
|
|
1704
1140
|
}
|
|
1705
|
-
}
|
|
1141
|
+
};
|
|
1706
1142
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1707
1143
|
0 && (module.exports = {
|
|
1708
|
-
|
|
1709
|
-
TikTokLive,
|
|
1710
|
-
fetchFeed,
|
|
1711
|
-
getLiveFeed,
|
|
1712
|
-
getRanklist,
|
|
1713
|
-
getRegionalRanklist
|
|
1144
|
+
TikTokLive
|
|
1714
1145
|
});
|
|
1715
1146
|
//# sourceMappingURL=index.js.map
|