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