@tiktool/live 2.6.7 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -323
- package/dist/index.d.mts +81 -707
- package/dist/index.d.ts +81 -707
- package/dist/index.js +335 -978
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +334 -972
- 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 {
|
|
@@ -338,29 +266,15 @@ function parseBattleTeamFromArmies(itemBuf) {
|
|
|
338
266
|
const userBufs = getAllBytes(gf, 1);
|
|
339
267
|
for (const ub of userBufs) {
|
|
340
268
|
try {
|
|
341
|
-
const userFields = decodeProto(ub);
|
|
342
|
-
const individualScore = getInt(userFields, 2);
|
|
343
269
|
const user = parseUser(ub);
|
|
344
|
-
users.push({ user, score:
|
|
270
|
+
users.push({ user, score: points });
|
|
345
271
|
} catch {
|
|
346
272
|
}
|
|
347
273
|
}
|
|
348
274
|
} catch {
|
|
349
275
|
}
|
|
350
276
|
}
|
|
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 };
|
|
277
|
+
return { hostUserId, score: teamScore, users };
|
|
364
278
|
}
|
|
365
279
|
function parseWebcastMessage(method, payload) {
|
|
366
280
|
const f = decodeProto(payload);
|
|
@@ -375,29 +289,42 @@ function parseWebcastMessage(method, payload) {
|
|
|
375
289
|
}
|
|
376
290
|
}
|
|
377
291
|
switch (method) {
|
|
292
|
+
// Proto: WebcastChatMessage { MessageType type=1, User user=2, string comment=3, ..., repeated WebcastSubEmote emotes=12 }
|
|
293
|
+
// WebcastSubEmote { int32 placeInComment=1, EmoteDetails emote=2 }
|
|
294
|
+
// EmoteDetails { string emoteId=1, EmoteImage image=2 }
|
|
295
|
+
// EmoteImage { string imageUrl=1 }
|
|
378
296
|
case "WebcastChatMessage": {
|
|
379
297
|
const userBuf = getBytes(f, 2);
|
|
380
298
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
299
|
+
const comment = getStr(f, 3);
|
|
300
|
+
const emotes = [];
|
|
301
|
+
const emoteBufs12 = getAllBytes(f, 12);
|
|
302
|
+
const emoteBufs13 = getAllBytes(f, 13);
|
|
303
|
+
const emoteBufs = emoteBufs13.length > 0 ? emoteBufs13 : emoteBufs12;
|
|
304
|
+
for (const eb of emoteBufs) {
|
|
305
|
+
try {
|
|
306
|
+
const ef = decodeProto(eb);
|
|
307
|
+
const placeInComment = getInt(ef, 1);
|
|
308
|
+
const detailBuf = getBytes(ef, 2);
|
|
309
|
+
if (detailBuf) {
|
|
310
|
+
const df = decodeProto(detailBuf);
|
|
311
|
+
const emoteId = getStr(df, 1);
|
|
312
|
+
let imageUrl = "";
|
|
313
|
+
const imgBuf = getBytes(df, 2);
|
|
314
|
+
if (imgBuf) {
|
|
315
|
+
const imgf = decodeProto(imgBuf);
|
|
316
|
+
imageUrl = getStr(imgf, 1);
|
|
317
|
+
}
|
|
318
|
+
if (emoteId) {
|
|
319
|
+
emotes.push({ emoteId, imageUrl, placeInComment });
|
|
393
320
|
}
|
|
394
|
-
} catch {
|
|
395
321
|
}
|
|
322
|
+
} catch {
|
|
396
323
|
}
|
|
397
324
|
}
|
|
398
|
-
|
|
399
|
-
return { ...base, type: "chat", user, comment };
|
|
325
|
+
return { ...base, type: "chat", user, comment, emotes };
|
|
400
326
|
}
|
|
327
|
+
// Proto: WebcastMemberMessage { User user=2, WebcastMessageEvent event=1 }
|
|
401
328
|
case "WebcastMemberMessage": {
|
|
402
329
|
const userBuf = getBytes(f, 2);
|
|
403
330
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -415,6 +342,7 @@ function parseWebcastMessage(method, payload) {
|
|
|
415
342
|
}
|
|
416
343
|
return { ...base, type: "member", user, action };
|
|
417
344
|
}
|
|
345
|
+
// Proto: WebcastLikeMessage { User user=5, WebcastMessageEvent event=1, int32 likeCount=2, int32 totalLikeCount=3 }
|
|
418
346
|
case "WebcastLikeMessage": {
|
|
419
347
|
const userBuf = getBytes(f, 5);
|
|
420
348
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -426,6 +354,13 @@ function parseWebcastMessage(method, payload) {
|
|
|
426
354
|
totalLikes: getInt(f, 3)
|
|
427
355
|
};
|
|
428
356
|
}
|
|
357
|
+
// Proto: WebcastGiftMessage {
|
|
358
|
+
// User user=7, int32 giftId=2, int32 repeatCount=5, int32 repeatEnd=9,
|
|
359
|
+
// GiftDetails giftDetails=15, GiftExtra giftExtra=23
|
|
360
|
+
// }
|
|
361
|
+
// GiftDetails { GiftImage giftImage=1, string giftName=16, string describe=2,
|
|
362
|
+
// int32 giftType=11, int32 diamondCount=12 }
|
|
363
|
+
// GiftExtra { uint64 timestamp=6, uint64 toUserId=8 }
|
|
429
364
|
case "WebcastGiftMessage": {
|
|
430
365
|
const userBuf = getBytes(f, 7);
|
|
431
366
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -450,7 +385,7 @@ function parseWebcastMessage(method, payload) {
|
|
|
450
385
|
const extraBuf = getBytes(f, 23);
|
|
451
386
|
if (extraBuf) {
|
|
452
387
|
const ef = decodeProto(extraBuf);
|
|
453
|
-
toUserId =
|
|
388
|
+
toUserId = String(getInt(ef, 8));
|
|
454
389
|
}
|
|
455
390
|
const groupId = toUserId || getStr(f, 11);
|
|
456
391
|
return {
|
|
@@ -464,10 +399,10 @@ function parseWebcastMessage(method, payload) {
|
|
|
464
399
|
repeatEnd,
|
|
465
400
|
combo: repeatCount > 1 && !repeatEnd,
|
|
466
401
|
giftType,
|
|
467
|
-
groupId
|
|
468
|
-
giftPictureUrl: giftImageUrl
|
|
402
|
+
groupId
|
|
469
403
|
};
|
|
470
404
|
}
|
|
405
|
+
// Proto: WebcastSocialMessage { User user=2, WebcastMessageEvent event=1 }
|
|
471
406
|
case "WebcastSocialMessage": {
|
|
472
407
|
const userBuf = getBytes(f, 2);
|
|
473
408
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -487,42 +422,19 @@ function parseWebcastMessage(method, payload) {
|
|
|
487
422
|
}
|
|
488
423
|
return { ...base, type: "social", user, action };
|
|
489
424
|
}
|
|
425
|
+
// Proto: WebcastRoomUserSeqMessage { int32 viewerCount=3 }
|
|
490
426
|
case "WebcastRoomUserSeqMessage": {
|
|
491
427
|
const viewerCount = getInt(f, 3) || getInt(f, 2);
|
|
492
428
|
const totalViewers = getInt(f, 1) || viewerCount;
|
|
493
429
|
return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
|
|
494
430
|
}
|
|
431
|
+
// Proto: WebcastLinkMicBattle { repeated WebcastLinkMicBattleItems battleUsers=10 }
|
|
432
|
+
// battleUsers → { battleGroup=2 → { LinkUser user=1 } }
|
|
495
433
|
case "WebcastLinkMicBattle": {
|
|
496
|
-
const battleId =
|
|
497
|
-
const
|
|
434
|
+
const battleId = String(getInt(f, 1) || "");
|
|
435
|
+
const status = getInt(f, 2) || 1;
|
|
436
|
+
const battleDuration = getInt(f, 3);
|
|
498
437
|
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
438
|
const battleUserBufs = getAllBytes(f, 10);
|
|
527
439
|
for (const bub of battleUserBufs) {
|
|
528
440
|
try {
|
|
@@ -543,23 +455,6 @@ function parseWebcastMessage(method, payload) {
|
|
|
543
455
|
} catch {
|
|
544
456
|
}
|
|
545
457
|
}
|
|
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
458
|
if (teams.length === 0) {
|
|
564
459
|
const teamBufs7 = getAllBytes(f, 7);
|
|
565
460
|
for (const tb of teamBufs7) {
|
|
@@ -569,48 +464,51 @@ function parseWebcastMessage(method, payload) {
|
|
|
569
464
|
}
|
|
570
465
|
}
|
|
571
466
|
}
|
|
572
|
-
return { ...base, type: "battle", battleId, status, battleDuration, teams
|
|
467
|
+
return { ...base, type: "battle", battleId, status, battleDuration, teams };
|
|
573
468
|
}
|
|
469
|
+
// Proto: WebcastLinkMicArmies { msg_header=1, session_id=2, repeated battleItems=3, status=7,
|
|
470
|
+
// battle_config=18 { session_id=1, start_time_ms=2, duration_s=3 }, battle_id=17 }
|
|
471
|
+
// battleItems → { hostUserId=1, repeated battleGroups=2 }
|
|
472
|
+
// battleGroups → { repeated users=1, int32 points=2 }
|
|
574
473
|
case "WebcastLinkMicArmies": {
|
|
575
|
-
const battleId =
|
|
474
|
+
const battleId = getStr(f, 17) || String(getInt(f, 2) || "");
|
|
576
475
|
const battleStatus = getInt(f, 7);
|
|
577
476
|
const teams = [];
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
if (settingsBuf18) {
|
|
477
|
+
const itemBufs = getAllBytes(f, 3);
|
|
478
|
+
for (const ib of itemBufs) {
|
|
581
479
|
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
|
-
};
|
|
480
|
+
teams.push(parseBattleTeamFromArmies(ib));
|
|
591
481
|
} catch {
|
|
592
482
|
}
|
|
593
483
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
484
|
+
let battleStartMs = 0;
|
|
485
|
+
let battleDurationSeconds = 0;
|
|
486
|
+
const configBuf = getBytes(f, 18);
|
|
487
|
+
if (configBuf) {
|
|
598
488
|
try {
|
|
599
|
-
|
|
489
|
+
const cf = decodeProto(configBuf);
|
|
490
|
+
battleStartMs = getInt(cf, 2);
|
|
491
|
+
battleDurationSeconds = getInt(cf, 3);
|
|
600
492
|
} catch {
|
|
601
493
|
}
|
|
602
494
|
}
|
|
495
|
+
let timeLeftSeconds = 0;
|
|
496
|
+
if (battleStartMs > 0 && battleDurationSeconds > 0) {
|
|
497
|
+
const elapsedMs = Date.now() - battleStartMs;
|
|
498
|
+
timeLeftSeconds = Math.max(0, battleDurationSeconds - Math.floor(elapsedMs / 1e3));
|
|
499
|
+
}
|
|
603
500
|
return {
|
|
604
501
|
...base,
|
|
605
502
|
type: "battleArmies",
|
|
606
503
|
battleId,
|
|
607
504
|
teams,
|
|
608
505
|
status: battleStatus,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
506
|
+
battleStartMs,
|
|
507
|
+
battleDurationSeconds,
|
|
508
|
+
timeLeftSeconds
|
|
612
509
|
};
|
|
613
510
|
}
|
|
511
|
+
// Proto: WebcastSubNotifyMessage { User user=2, ... subMonth=3 }
|
|
614
512
|
case "WebcastSubNotifyMessage": {
|
|
615
513
|
const userBuf = getBytes(f, 2);
|
|
616
514
|
const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
|
|
@@ -636,6 +534,8 @@ function parseWebcastMessage(method, payload) {
|
|
|
636
534
|
const envelopeId = String(getInt(f, 1) || getStr(f, 1));
|
|
637
535
|
return { ...base, type: "envelope", envelopeId, diamondCount: getInt(f, 3) };
|
|
638
536
|
}
|
|
537
|
+
// Proto: WebcastQuestionNewMessage { MessageType type=1, QuestionDetails questionDetails=2 }
|
|
538
|
+
// QuestionDetails { string questionText=2, User user=5 }
|
|
639
539
|
case "WebcastQuestionNewMessage": {
|
|
640
540
|
let questionText = "", user = { id: "0", nickname: "", uniqueId: "" };
|
|
641
541
|
const detailBuf = getBytes(f, 2);
|
|
@@ -665,17 +565,20 @@ function parseWebcastMessage(method, payload) {
|
|
|
665
565
|
}
|
|
666
566
|
return { ...base, type: "rankUpdate", rankType, rankList };
|
|
667
567
|
}
|
|
568
|
+
// Proto: WebcastControlMessage { int32 action=2 }
|
|
668
569
|
case "WebcastControlMessage":
|
|
669
570
|
return { ...base, type: "control", action: getInt(f, 2) || getInt(f, 1) };
|
|
670
571
|
case "WebcastRoomMessage":
|
|
671
572
|
case "RoomMessage":
|
|
672
573
|
return { ...base, type: "room", status: getInt(f, 2) };
|
|
574
|
+
// Proto: WebcastLiveIntroMessage { uint64 id=2, string description=4, User user=5 }
|
|
673
575
|
case "WebcastLiveIntroMessage": {
|
|
674
576
|
const roomId = String(getInt(f, 2));
|
|
675
577
|
const title = getStr(f, 4) || getStr(f, 2);
|
|
676
578
|
return { ...base, type: "liveIntro", roomId, title };
|
|
677
579
|
}
|
|
678
|
-
case "WebcastLinkMicMethod":
|
|
580
|
+
case "WebcastLinkMicMethod":
|
|
581
|
+
case "WebcastLinkmicBattleTaskMessage": {
|
|
679
582
|
const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
|
|
680
583
|
const users = [];
|
|
681
584
|
const userBufs = getAllBytes(f, 2);
|
|
@@ -687,152 +590,6 @@ function parseWebcastMessage(method, payload) {
|
|
|
687
590
|
}
|
|
688
591
|
return { ...base, type: "linkMic", action, users };
|
|
689
592
|
}
|
|
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
593
|
default:
|
|
837
594
|
return { ...base, type: "unknown", method };
|
|
838
595
|
}
|
|
@@ -857,12 +614,10 @@ function parseWebcastResponse(payload) {
|
|
|
857
614
|
// src/client.ts
|
|
858
615
|
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
616
|
var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
|
|
860
|
-
function httpGet(url, headers
|
|
617
|
+
function httpGet(url, headers) {
|
|
861
618
|
return new Promise((resolve, reject) => {
|
|
862
619
|
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) => {
|
|
620
|
+
const req = mod.get(url, { headers }, (res) => {
|
|
866
621
|
const chunks = [];
|
|
867
622
|
const enc = res.headers["content-encoding"];
|
|
868
623
|
const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
|
|
@@ -888,15 +643,21 @@ function getWsHost(clusterRegion) {
|
|
|
888
643
|
if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
|
|
889
644
|
return "webcast-ws.tiktok.com";
|
|
890
645
|
}
|
|
891
|
-
var TikTokLive = class extends import_events.EventEmitter {
|
|
646
|
+
var TikTokLive = class _TikTokLive extends import_events.EventEmitter {
|
|
892
647
|
ws = null;
|
|
893
648
|
heartbeatTimer = null;
|
|
894
649
|
reconnectAttempts = 0;
|
|
895
650
|
intentionalClose = false;
|
|
896
651
|
_connected = false;
|
|
652
|
+
_destroyed = false;
|
|
897
653
|
_eventCount = 0;
|
|
898
654
|
_roomId = "";
|
|
899
|
-
|
|
655
|
+
// Cache host identities from battle events for enriching battleArmies
|
|
656
|
+
// Bounded to MAX_BATTLE_HOSTS entries to prevent unbounded memory growth
|
|
657
|
+
static MAX_BATTLE_HOSTS = 50;
|
|
658
|
+
_battleHosts = /* @__PURE__ */ new Map();
|
|
659
|
+
// Track in-flight host resolution requests to avoid duplicate API calls
|
|
660
|
+
_pendingHostResolves = /* @__PURE__ */ new Set();
|
|
900
661
|
uniqueId;
|
|
901
662
|
signServerUrl;
|
|
902
663
|
apiKey;
|
|
@@ -904,13 +665,11 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
904
665
|
maxReconnectAttempts;
|
|
905
666
|
heartbeatInterval;
|
|
906
667
|
debug;
|
|
907
|
-
_sessionId;
|
|
908
|
-
_ttTargetIdc;
|
|
909
|
-
proxyAgent;
|
|
910
668
|
_presetRoomId;
|
|
911
|
-
|
|
669
|
+
_presetSessionId;
|
|
912
670
|
constructor(options) {
|
|
913
671
|
super();
|
|
672
|
+
this.setMaxListeners(20);
|
|
914
673
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
915
674
|
this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
916
675
|
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
@@ -919,35 +678,60 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
919
678
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
920
679
|
this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
|
|
921
680
|
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;
|
|
681
|
+
this._presetRoomId = options.roomId || "";
|
|
682
|
+
this._presetSessionId = options.sessionId || "";
|
|
929
683
|
}
|
|
930
684
|
async connect() {
|
|
685
|
+
if (this._destroyed) throw new Error("Client has been destroyed. Create a new instance.");
|
|
931
686
|
this.intentionalClose = false;
|
|
932
|
-
let ttwid = this.
|
|
933
|
-
let roomId = this._presetRoomId
|
|
934
|
-
let ownerUserId = "";
|
|
687
|
+
let ttwid = this._presetSessionId;
|
|
688
|
+
let roomId = this._presetRoomId;
|
|
935
689
|
let clusterRegion = "";
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
|
|
690
|
+
if (roomId && ttwid) {
|
|
691
|
+
} else if (roomId && !ttwid) {
|
|
692
|
+
try {
|
|
693
|
+
const credResp = await fetch(`${this.signServerUrl}/webcast/ws_credentials`, {
|
|
694
|
+
method: "POST",
|
|
695
|
+
headers: { "Content-Type": "application/json", "x-api-key": this.apiKey },
|
|
696
|
+
body: JSON.stringify({ unique_id: this.uniqueId, room_id: roomId })
|
|
697
|
+
});
|
|
698
|
+
if (credResp.ok) {
|
|
699
|
+
const credData = await credResp.json();
|
|
700
|
+
if (credData.data?.cookies) {
|
|
701
|
+
const m = credData.data.cookies.match(/ttwid=([^;]+)/);
|
|
702
|
+
if (m) ttwid = m[1];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
if (!ttwid) {
|
|
708
|
+
try {
|
|
709
|
+
const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
|
|
710
|
+
"User-Agent": DEFAULT_UA,
|
|
711
|
+
"Accept": "text/html"
|
|
712
|
+
});
|
|
713
|
+
for (const sc of [resp.headers["set-cookie"] || []].flat()) {
|
|
714
|
+
if (typeof sc === "string" && sc.startsWith("ttwid=")) {
|
|
715
|
+
ttwid = sc.split(";")[0].split("=").slice(1).join("=");
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
|
|
939
724
|
"User-Agent": DEFAULT_UA,
|
|
940
725
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
941
726
|
"Accept-Encoding": "gzip, deflate, br",
|
|
942
727
|
"Accept-Language": "en-US,en;q=0.9"
|
|
943
|
-
}
|
|
728
|
+
});
|
|
944
729
|
for (const sc of [resp.headers["set-cookie"] || []].flat()) {
|
|
945
730
|
if (typeof sc === "string" && sc.startsWith("ttwid=")) {
|
|
946
731
|
ttwid = sc.split(";")[0].split("=").slice(1).join("=");
|
|
947
732
|
break;
|
|
948
733
|
}
|
|
949
734
|
}
|
|
950
|
-
if (!ttwid) throw new Error("Failed to obtain session cookie");
|
|
951
735
|
const html = resp.body.toString();
|
|
952
736
|
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
953
737
|
if (sigiMatch) {
|
|
@@ -956,19 +740,15 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
956
740
|
const jsonStr = JSON.stringify(json);
|
|
957
741
|
const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
|
|
958
742
|
if (m) roomId = m[1];
|
|
959
|
-
const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
|
|
960
|
-
if (ownerUser?.id) {
|
|
961
|
-
ownerUserId = String(ownerUser.id);
|
|
962
|
-
}
|
|
963
743
|
} catch {
|
|
964
744
|
}
|
|
965
745
|
}
|
|
966
|
-
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
967
746
|
const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
|
|
968
747
|
clusterRegion = crMatch ? crMatch[1] : "";
|
|
969
748
|
}
|
|
749
|
+
if (!ttwid) throw new Error("Failed to obtain session cookie");
|
|
750
|
+
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
970
751
|
this._roomId = roomId;
|
|
971
|
-
this._ownerUserId = ownerUserId;
|
|
972
752
|
const wsHost = getWsHost(clusterRegion);
|
|
973
753
|
const wsParams = new URLSearchParams({
|
|
974
754
|
version_code: "270000",
|
|
@@ -1022,21 +802,12 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1022
802
|
wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
|
|
1023
803
|
}
|
|
1024
804
|
return new Promise((resolve, reject) => {
|
|
1025
|
-
let cookieHeader = ttwid ? `ttwid=${ttwid}` : "";
|
|
1026
|
-
if (this._sessionId) {
|
|
1027
|
-
const sessionCookies = `sessionid=${this._sessionId}; sessionid_ss=${this._sessionId}; sid_tt=${this._sessionId}`;
|
|
1028
|
-
cookieHeader = cookieHeader ? `${cookieHeader}; ${sessionCookies}` : sessionCookies;
|
|
1029
|
-
if (this._ttTargetIdc) {
|
|
1030
|
-
cookieHeader += `; tt-target-idc=${this._ttTargetIdc}`;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
805
|
this.ws = new import_ws.default(wsUrl, {
|
|
1034
806
|
headers: {
|
|
1035
807
|
"User-Agent": DEFAULT_UA,
|
|
1036
|
-
"Cookie":
|
|
808
|
+
"Cookie": `ttwid=${ttwid}`,
|
|
1037
809
|
"Origin": "https://www.tiktok.com"
|
|
1038
|
-
}
|
|
1039
|
-
agent: this.proxyAgent
|
|
810
|
+
}
|
|
1040
811
|
});
|
|
1041
812
|
this.ws.on("open", () => {
|
|
1042
813
|
this._connected = true;
|
|
@@ -1048,8 +819,7 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1048
819
|
roomId,
|
|
1049
820
|
wsHost,
|
|
1050
821
|
clusterRegion,
|
|
1051
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1052
|
-
ownerUserId: ownerUserId || void 0
|
|
822
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1053
823
|
};
|
|
1054
824
|
this.emit("connected");
|
|
1055
825
|
this.emit("roomInfo", roomInfo);
|
|
@@ -1079,42 +849,76 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1079
849
|
this.intentionalClose = true;
|
|
1080
850
|
this.stopHeartbeat();
|
|
1081
851
|
if (this.ws) {
|
|
852
|
+
this.ws.removeAllListeners();
|
|
1082
853
|
this.ws.close(1e3);
|
|
1083
854
|
this.ws = null;
|
|
1084
855
|
}
|
|
1085
856
|
this._connected = false;
|
|
1086
857
|
}
|
|
858
|
+
/**
|
|
859
|
+
* Fully destroy the client, releasing all resources and listeners.
|
|
860
|
+
* After calling destroy(), the instance cannot be reused — create a new one.
|
|
861
|
+
*/
|
|
862
|
+
destroy() {
|
|
863
|
+
this.disconnect();
|
|
864
|
+
this._destroyed = true;
|
|
865
|
+
this._battleHosts.clear();
|
|
866
|
+
this._eventCount = 0;
|
|
867
|
+
this._roomId = "";
|
|
868
|
+
this.removeAllListeners();
|
|
869
|
+
}
|
|
1087
870
|
get connected() {
|
|
1088
871
|
return this._connected;
|
|
1089
872
|
}
|
|
873
|
+
get destroyed() {
|
|
874
|
+
return this._destroyed;
|
|
875
|
+
}
|
|
1090
876
|
get eventCount() {
|
|
1091
877
|
return this._eventCount;
|
|
1092
878
|
}
|
|
1093
879
|
get roomId() {
|
|
1094
880
|
return this._roomId;
|
|
1095
881
|
}
|
|
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
882
|
/**
|
|
1106
|
-
*
|
|
1107
|
-
* Returns
|
|
883
|
+
* Get live stream video URLs for a TikTok user.
|
|
884
|
+
* Returns FLV & HLS URLs segmented by quality (FULL_HD1, HD1, SD1, SD2).
|
|
885
|
+
* This is a standalone method — no WebSocket connection required.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```ts
|
|
889
|
+
* const stream = await TikTokLive.getStreamUrl({
|
|
890
|
+
* uniqueId: 'username',
|
|
891
|
+
* apiKey: 'your-api-key',
|
|
892
|
+
* });
|
|
893
|
+
* if (stream.alive) {
|
|
894
|
+
* console.log(stream.streamUrls.HD1?.hls); // HLS URL for HD quality
|
|
895
|
+
* console.log(stream.flvPullUrl); // Best FLV URL
|
|
896
|
+
* }
|
|
897
|
+
* ```
|
|
1108
898
|
*/
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
899
|
+
static async getStreamUrl(options) {
|
|
900
|
+
const serverUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
901
|
+
const resp = await fetch(`${serverUrl}/webcast/room_video`, {
|
|
902
|
+
method: "POST",
|
|
903
|
+
headers: {
|
|
904
|
+
"Content-Type": "application/json",
|
|
905
|
+
"x-api-key": options.apiKey
|
|
906
|
+
},
|
|
907
|
+
body: JSON.stringify({ unique_id: options.uniqueId.replace(/^@/, "") })
|
|
908
|
+
});
|
|
909
|
+
const data = await resp.json();
|
|
910
|
+
if (data.status_code !== 0) {
|
|
911
|
+
throw new Error(data.error || "Failed to get stream URL");
|
|
912
|
+
}
|
|
913
|
+
const d = data.data;
|
|
914
|
+
return {
|
|
915
|
+
roomId: d.room_id,
|
|
916
|
+
alive: d.alive ?? false,
|
|
917
|
+
streamUrls: d.stream_urls || {},
|
|
918
|
+
defaultQuality: d.default_quality || "",
|
|
919
|
+
flvPullUrl: d.flv_pull_url || void 0,
|
|
920
|
+
hlsPullUrl: d.hls_pull_url || void 0
|
|
921
|
+
};
|
|
1118
922
|
}
|
|
1119
923
|
on(event, listener) {
|
|
1120
924
|
return super.on(event, listener);
|
|
@@ -1149,567 +953,120 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1149
953
|
const events = parseWebcastResponse(inner);
|
|
1150
954
|
for (const evt of events) {
|
|
1151
955
|
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);
|
|
956
|
+
if (evt.type === "battle") {
|
|
957
|
+
for (const team of evt.teams) {
|
|
958
|
+
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
959
|
+
if (host) {
|
|
960
|
+
this._battleHosts.set(team.hostUserId, host.user);
|
|
961
|
+
team.hostUser = host.user;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (this._battleHosts.size > _TikTokLive.MAX_BATTLE_HOSTS) {
|
|
965
|
+
const excess = this._battleHosts.size - _TikTokLive.MAX_BATTLE_HOSTS;
|
|
966
|
+
const keys = [...this._battleHosts.keys()].slice(0, excess);
|
|
967
|
+
for (const key of keys) this._battleHosts.delete(key);
|
|
1245
968
|
}
|
|
1246
969
|
}
|
|
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);
|
|
970
|
+
if (evt.type === "battleArmies") {
|
|
971
|
+
const unresolvedIds = [];
|
|
972
|
+
for (const team of evt.teams) {
|
|
973
|
+
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
974
|
+
if (host) {
|
|
975
|
+
team.hostUser = host.user;
|
|
976
|
+
this._battleHosts.set(team.hostUserId, host.user);
|
|
977
|
+
} else {
|
|
978
|
+
const cached = this._battleHosts.get(team.hostUserId);
|
|
979
|
+
if (cached) {
|
|
980
|
+
team.hostUser = cached;
|
|
981
|
+
} else if (team.hostUserId && team.hostUserId !== "0") {
|
|
982
|
+
unresolvedIds.push(team.hostUserId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (unresolvedIds.length > 0) {
|
|
987
|
+
this.resolveHostUsers(unresolvedIds, evt).catch(() => {
|
|
988
|
+
});
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
1499
991
|
}
|
|
992
|
+
this.emit("event", evt);
|
|
993
|
+
this.emit(evt.type, evt);
|
|
994
|
+
}
|
|
1500
995
|
}
|
|
1501
996
|
} catch {
|
|
1502
|
-
if (this.debug) console.error("[Captions] Failed to parse message:", raw);
|
|
1503
997
|
}
|
|
1504
998
|
}
|
|
1505
999
|
/**
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1000
|
+
* Resolve unknown host user IDs via the sign server API.
|
|
1001
|
+
* Caches results and re-emits the battleArmies event with enriched hostUser data.
|
|
1508
1002
|
*/
|
|
1509
|
-
async
|
|
1510
|
-
const
|
|
1511
|
-
if (
|
|
1512
|
-
this.emit("
|
|
1003
|
+
async resolveHostUsers(userIds, originalEvt) {
|
|
1004
|
+
const toResolve = userIds.filter((id) => !this._pendingHostResolves.has(id));
|
|
1005
|
+
if (toResolve.length === 0) {
|
|
1006
|
+
this.emit("event", originalEvt);
|
|
1007
|
+
this.emit(originalEvt.type, originalEvt);
|
|
1513
1008
|
return;
|
|
1514
1009
|
}
|
|
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
|
-
});
|
|
1010
|
+
for (const id of toResolve) this._pendingHostResolves.add(id);
|
|
1535
1011
|
try {
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1012
|
+
const url = `${this.signServerUrl}/webcast/resolve_user_ids`;
|
|
1013
|
+
const res = await fetch(url, {
|
|
1014
|
+
method: "POST",
|
|
1539
1015
|
headers: {
|
|
1540
|
-
"
|
|
1541
|
-
|
|
1016
|
+
"Content-Type": "application/json",
|
|
1017
|
+
"x-api-key": this.apiKey
|
|
1018
|
+
},
|
|
1019
|
+
body: JSON.stringify({ user_ids: toResolve }),
|
|
1020
|
+
signal: AbortSignal.timeout(5e3)
|
|
1542
1021
|
});
|
|
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`);
|
|
1022
|
+
if (res.ok) {
|
|
1023
|
+
const json = await res.json();
|
|
1024
|
+
if (json.data) {
|
|
1025
|
+
for (const [uid, info] of Object.entries(json.data)) {
|
|
1026
|
+
if (info.username) {
|
|
1027
|
+
const hostUser = {
|
|
1028
|
+
id: uid,
|
|
1029
|
+
uniqueId: info.username,
|
|
1030
|
+
nickname: info.nickname || info.username,
|
|
1031
|
+
profilePicture: info.avatarUrl || void 0
|
|
1032
|
+
};
|
|
1033
|
+
this._battleHosts.set(uid, hostUser);
|
|
1574
1034
|
}
|
|
1575
1035
|
}
|
|
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);
|
|
1036
|
+
if (originalEvt.type === "battleArmies") {
|
|
1037
|
+
for (const team of originalEvt.teams) {
|
|
1038
|
+
if (!team.hostUser) {
|
|
1039
|
+
const cached = this._battleHosts.get(team.hostUserId);
|
|
1040
|
+
if (cached) team.hostUser = cached;
|
|
1588
1041
|
}
|
|
1589
|
-
if (this.debug && chunks <= 3) {
|
|
1590
|
-
if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
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
1042
|
}
|
|
1601
1043
|
}
|
|
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 });
|
|
1044
|
+
}
|
|
1611
1045
|
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
} finally {
|
|
1048
|
+
for (const id of toResolve) this._pendingHostResolves.delete(id);
|
|
1049
|
+
this.emit("event", originalEvt);
|
|
1050
|
+
this.emit(originalEvt.type, originalEvt);
|
|
1612
1051
|
}
|
|
1613
1052
|
}
|
|
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;
|
|
1053
|
+
startHeartbeat(roomId) {
|
|
1054
|
+
this.stopHeartbeat();
|
|
1055
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1056
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1057
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
1058
|
+
}
|
|
1059
|
+
}, this.heartbeatInterval);
|
|
1697
1060
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
return null;
|
|
1061
|
+
stopHeartbeat() {
|
|
1062
|
+
if (this.heartbeatTimer) {
|
|
1063
|
+
clearInterval(this.heartbeatTimer);
|
|
1064
|
+
this.heartbeatTimer = null;
|
|
1065
|
+
}
|
|
1704
1066
|
}
|
|
1705
|
-
}
|
|
1067
|
+
};
|
|
1706
1068
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1707
1069
|
0 && (module.exports = {
|
|
1708
|
-
|
|
1709
|
-
TikTokLive,
|
|
1710
|
-
fetchFeed,
|
|
1711
|
-
getLiveFeed,
|
|
1712
|
-
getRanklist,
|
|
1713
|
-
getRegionalRanklist
|
|
1070
|
+
TikTokLive
|
|
1714
1071
|
});
|
|
1715
1072
|
//# sourceMappingURL=index.js.map
|