@tiktool/live 2.6.1 → 2.6.3
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 +124 -19
- package/dist/index.d.mts +186 -1
- package/dist/index.d.ts +186 -1
- package/dist/index.js +446 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +445 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -3
package/dist/index.js
CHANGED
|
@@ -30,6 +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
|
+
TikTokCaptions: () => TikTokCaptions,
|
|
33
34
|
TikTokLive: () => TikTokLive,
|
|
34
35
|
getRanklist: () => getRanklist,
|
|
35
36
|
getRegionalRanklist: () => getRegionalRanklist
|
|
@@ -335,8 +336,10 @@ function parseBattleTeamFromArmies(itemBuf) {
|
|
|
335
336
|
const userBufs = getAllBytes(gf, 1);
|
|
336
337
|
for (const ub of userBufs) {
|
|
337
338
|
try {
|
|
339
|
+
const userFields = decodeProto(ub);
|
|
340
|
+
const individualScore = getInt(userFields, 2);
|
|
338
341
|
const user = parseUser(ub);
|
|
339
|
-
users.push({ user, score:
|
|
342
|
+
users.push({ user, score: individualScore });
|
|
340
343
|
} catch {
|
|
341
344
|
}
|
|
342
345
|
}
|
|
@@ -1167,6 +1170,447 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1167
1170
|
}
|
|
1168
1171
|
};
|
|
1169
1172
|
|
|
1173
|
+
// src/captions.ts
|
|
1174
|
+
var import_events2 = require("events");
|
|
1175
|
+
var import_ws2 = __toESM(require("ws"));
|
|
1176
|
+
var FLV_TAG_AUDIO = 8;
|
|
1177
|
+
var FLV_HEADER_SIZE = 9;
|
|
1178
|
+
var FLV_PREV_TAG_SIZE = 4;
|
|
1179
|
+
var FlvAudioExtractor = class {
|
|
1180
|
+
buffer = new Uint8Array(0);
|
|
1181
|
+
headerParsed = false;
|
|
1182
|
+
onAudio;
|
|
1183
|
+
aacProfile = 2;
|
|
1184
|
+
sampleRateIndex = 4;
|
|
1185
|
+
channelConfig = 2;
|
|
1186
|
+
ascParsed = false;
|
|
1187
|
+
constructor(onAudio) {
|
|
1188
|
+
this.onAudio = onAudio;
|
|
1189
|
+
}
|
|
1190
|
+
parseASC(asc) {
|
|
1191
|
+
if (asc.length < 2) return;
|
|
1192
|
+
this.aacProfile = asc[0] >> 3 & 31;
|
|
1193
|
+
this.sampleRateIndex = (asc[0] & 7) << 1 | asc[1] >> 7 & 1;
|
|
1194
|
+
this.channelConfig = asc[1] >> 3 & 15;
|
|
1195
|
+
this.ascParsed = true;
|
|
1196
|
+
}
|
|
1197
|
+
buildAdtsHeader(frameLength) {
|
|
1198
|
+
const adts = new Uint8Array(7);
|
|
1199
|
+
const fullLength = frameLength + 7;
|
|
1200
|
+
const profile = this.aacProfile - 1;
|
|
1201
|
+
adts[0] = 255;
|
|
1202
|
+
adts[1] = 241;
|
|
1203
|
+
adts[2] = (profile & 3) << 6 | (this.sampleRateIndex & 15) << 2 | this.channelConfig >> 2 & 1;
|
|
1204
|
+
adts[3] = (this.channelConfig & 3) << 6 | fullLength >> 11 & 3;
|
|
1205
|
+
adts[4] = fullLength >> 3 & 255;
|
|
1206
|
+
adts[5] = (fullLength & 7) << 5 | 31;
|
|
1207
|
+
adts[6] = 252;
|
|
1208
|
+
return adts;
|
|
1209
|
+
}
|
|
1210
|
+
push(chunk) {
|
|
1211
|
+
const newBuf = new Uint8Array(this.buffer.length + chunk.length);
|
|
1212
|
+
newBuf.set(this.buffer, 0);
|
|
1213
|
+
newBuf.set(chunk, this.buffer.length);
|
|
1214
|
+
this.buffer = newBuf;
|
|
1215
|
+
if (!this.headerParsed) {
|
|
1216
|
+
if (this.buffer.length < FLV_HEADER_SIZE + FLV_PREV_TAG_SIZE) return;
|
|
1217
|
+
if (this.buffer[0] !== 70 || this.buffer[1] !== 76 || this.buffer[2] !== 86) return;
|
|
1218
|
+
const dv = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
|
|
1219
|
+
const dataOffset = dv.getUint32(5);
|
|
1220
|
+
this.buffer = this.buffer.subarray(dataOffset + FLV_PREV_TAG_SIZE);
|
|
1221
|
+
this.headerParsed = true;
|
|
1222
|
+
}
|
|
1223
|
+
while (this.buffer.length >= 11) {
|
|
1224
|
+
const tagType = this.buffer[0] & 31;
|
|
1225
|
+
const dataSize = this.buffer[1] << 16 | this.buffer[2] << 8 | this.buffer[3];
|
|
1226
|
+
const totalTagSize = 11 + dataSize + FLV_PREV_TAG_SIZE;
|
|
1227
|
+
if (this.buffer.length < totalTagSize) break;
|
|
1228
|
+
if (tagType === FLV_TAG_AUDIO) {
|
|
1229
|
+
const audioData = this.buffer.subarray(11, 11 + dataSize);
|
|
1230
|
+
if (audioData.length > 0) {
|
|
1231
|
+
const soundFormat = audioData[0] >> 4 & 15;
|
|
1232
|
+
if (soundFormat === 10 && audioData.length > 2) {
|
|
1233
|
+
const aacPacketType = audioData[1];
|
|
1234
|
+
if (aacPacketType === 0) {
|
|
1235
|
+
this.parseASC(audioData.subarray(2));
|
|
1236
|
+
} else if (aacPacketType === 1 && this.ascParsed) {
|
|
1237
|
+
const rawFrame = audioData.subarray(2);
|
|
1238
|
+
const adtsHeader = this.buildAdtsHeader(rawFrame.length);
|
|
1239
|
+
const adtsFrame = new Uint8Array(adtsHeader.length + rawFrame.length);
|
|
1240
|
+
adtsFrame.set(adtsHeader, 0);
|
|
1241
|
+
adtsFrame.set(rawFrame, adtsHeader.length);
|
|
1242
|
+
this.onAudio(adtsFrame);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
this.buffer = this.buffer.subarray(totalTagSize);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
reset() {
|
|
1251
|
+
this.buffer = new Uint8Array(0);
|
|
1252
|
+
this.headerParsed = false;
|
|
1253
|
+
this.ascParsed = false;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
var DEFAULT_CAPTIONS_SERVER = "wss://api.tik.tools";
|
|
1257
|
+
var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
1258
|
+
ws = null;
|
|
1259
|
+
_connected = false;
|
|
1260
|
+
intentionalClose = false;
|
|
1261
|
+
reconnectAttempts = 0;
|
|
1262
|
+
uniqueId;
|
|
1263
|
+
apiKey;
|
|
1264
|
+
serverUrl;
|
|
1265
|
+
autoReconnect;
|
|
1266
|
+
maxReconnectAttempts;
|
|
1267
|
+
debug;
|
|
1268
|
+
_translate;
|
|
1269
|
+
_diarization;
|
|
1270
|
+
_maxDurationMinutes;
|
|
1271
|
+
_language;
|
|
1272
|
+
streamAbortController = null;
|
|
1273
|
+
flvExtractor = null;
|
|
1274
|
+
streamUrl = null;
|
|
1275
|
+
constructor(options) {
|
|
1276
|
+
super();
|
|
1277
|
+
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
1278
|
+
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
1279
|
+
this.apiKey = options.apiKey;
|
|
1280
|
+
this._language = options.language || "";
|
|
1281
|
+
this._translate = options.translate || "";
|
|
1282
|
+
this._diarization = options.diarization ?? true;
|
|
1283
|
+
this._maxDurationMinutes = options.maxDurationMinutes ?? 60;
|
|
1284
|
+
this.serverUrl = (options.signServerUrl || DEFAULT_CAPTIONS_SERVER).replace(/\/$/, "");
|
|
1285
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
1286
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
1287
|
+
this.debug = options.debug ?? false;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Start real-time captions for the configured TikTok user.
|
|
1291
|
+
* Connects to the captions WebSocket relay and begins transcription
|
|
1292
|
+
* once the user goes live (or immediately if already live).
|
|
1293
|
+
*/
|
|
1294
|
+
async start() {
|
|
1295
|
+
this.intentionalClose = false;
|
|
1296
|
+
const wsUrl = this.buildWsUrl();
|
|
1297
|
+
if (this.debug) console.log(`[Captions] Connecting to ${wsUrl}`);
|
|
1298
|
+
return new Promise((resolve, reject) => {
|
|
1299
|
+
this.ws = new import_ws2.default(wsUrl);
|
|
1300
|
+
this.ws.on("open", () => {
|
|
1301
|
+
this._connected = true;
|
|
1302
|
+
this.reconnectAttempts = 0;
|
|
1303
|
+
if (this.debug) console.log("[Captions] Connected");
|
|
1304
|
+
this.emit("connected");
|
|
1305
|
+
resolve();
|
|
1306
|
+
});
|
|
1307
|
+
this.ws.on("message", (data) => {
|
|
1308
|
+
this.handleMessage(typeof data === "string" ? data : data.toString());
|
|
1309
|
+
});
|
|
1310
|
+
this.ws.on("close", (code, reason) => {
|
|
1311
|
+
this._connected = false;
|
|
1312
|
+
const reasonStr = reason?.toString() || "";
|
|
1313
|
+
if (this.debug) console.log(`[Captions] Disconnected: ${code} ${reasonStr}`);
|
|
1314
|
+
this.emit("disconnected", code, reasonStr);
|
|
1315
|
+
if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
1316
|
+
this.reconnectAttempts++;
|
|
1317
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
1318
|
+
if (this.debug) console.log(`[Captions] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
1319
|
+
setTimeout(() => this.start().catch((e) => this.emit("error", { code: "RECONNECT_FAILED", message: e.message })), delay);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
this.ws.on("error", (err) => {
|
|
1323
|
+
this.emit("error", { code: "WS_ERROR", message: err.message });
|
|
1324
|
+
if (!this._connected) reject(err);
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Stop captions and disconnect.
|
|
1330
|
+
*/
|
|
1331
|
+
stop() {
|
|
1332
|
+
this.intentionalClose = true;
|
|
1333
|
+
if (this.streamAbortController) {
|
|
1334
|
+
this.streamAbortController.abort();
|
|
1335
|
+
this.streamAbortController = null;
|
|
1336
|
+
}
|
|
1337
|
+
if (this.flvExtractor) {
|
|
1338
|
+
this.flvExtractor.reset();
|
|
1339
|
+
this.flvExtractor = null;
|
|
1340
|
+
}
|
|
1341
|
+
if (this.ws) {
|
|
1342
|
+
this.send({ action: "stop" });
|
|
1343
|
+
this.ws.close(1e3);
|
|
1344
|
+
this.ws = null;
|
|
1345
|
+
}
|
|
1346
|
+
this._connected = false;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Switch the translation target language on-the-fly.
|
|
1350
|
+
* Causes a brief interruption while the transcription engine reconfigures.
|
|
1351
|
+
*/
|
|
1352
|
+
setLanguage(language) {
|
|
1353
|
+
this._language = language;
|
|
1354
|
+
this.send({ action: "set_language", language });
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Request a credit balance update from the server.
|
|
1358
|
+
*/
|
|
1359
|
+
getCredits() {
|
|
1360
|
+
this.send({ action: "get_credits" });
|
|
1361
|
+
}
|
|
1362
|
+
/** Whether the WebSocket is currently connected */
|
|
1363
|
+
get connected() {
|
|
1364
|
+
return this._connected;
|
|
1365
|
+
}
|
|
1366
|
+
/** The current target language */
|
|
1367
|
+
get language() {
|
|
1368
|
+
return this._language;
|
|
1369
|
+
}
|
|
1370
|
+
on(event, listener) {
|
|
1371
|
+
return super.on(event, listener);
|
|
1372
|
+
}
|
|
1373
|
+
once(event, listener) {
|
|
1374
|
+
return super.once(event, listener);
|
|
1375
|
+
}
|
|
1376
|
+
off(event, listener) {
|
|
1377
|
+
return super.off(event, listener);
|
|
1378
|
+
}
|
|
1379
|
+
emit(event, ...args) {
|
|
1380
|
+
return super.emit(event, ...args);
|
|
1381
|
+
}
|
|
1382
|
+
buildWsUrl() {
|
|
1383
|
+
const base = this.serverUrl.replace(/^http/, "ws");
|
|
1384
|
+
const params = new URLSearchParams({
|
|
1385
|
+
uniqueId: this.uniqueId,
|
|
1386
|
+
apiKey: this.apiKey
|
|
1387
|
+
});
|
|
1388
|
+
if (this._language) params.set("language", this._language);
|
|
1389
|
+
if (this._translate) params.set("translate", this._translate);
|
|
1390
|
+
if (this._diarization !== void 0) params.set("diarization", String(this._diarization));
|
|
1391
|
+
if (this._maxDurationMinutes) params.set("max_duration_minutes", String(this._maxDurationMinutes));
|
|
1392
|
+
return `${base}/captions?${params}`;
|
|
1393
|
+
}
|
|
1394
|
+
send(msg) {
|
|
1395
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
1396
|
+
this.ws.send(JSON.stringify(msg));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
handleMessage(raw) {
|
|
1400
|
+
try {
|
|
1401
|
+
const msg = JSON.parse(raw);
|
|
1402
|
+
switch (msg.type) {
|
|
1403
|
+
case "stream_info":
|
|
1404
|
+
if (this.debug) console.log(`[Captions] Received stream_info: flv=${!!msg.flvUrl}, hls=${!!msg.hlsUrl}, ao=${!!msg.audioOnlyUrl}`);
|
|
1405
|
+
this.connectToStream(msg);
|
|
1406
|
+
break;
|
|
1407
|
+
case "caption":
|
|
1408
|
+
this.emit("caption", {
|
|
1409
|
+
text: msg.text,
|
|
1410
|
+
language: msg.language,
|
|
1411
|
+
isFinal: msg.isFinal,
|
|
1412
|
+
confidence: msg.confidence,
|
|
1413
|
+
speaker: msg.speaker,
|
|
1414
|
+
startMs: msg.startMs,
|
|
1415
|
+
endMs: msg.endMs
|
|
1416
|
+
});
|
|
1417
|
+
break;
|
|
1418
|
+
case "translation":
|
|
1419
|
+
this.emit("translation", {
|
|
1420
|
+
text: msg.text,
|
|
1421
|
+
language: msg.language,
|
|
1422
|
+
isFinal: msg.isFinal,
|
|
1423
|
+
confidence: msg.confidence,
|
|
1424
|
+
speaker: msg.speaker
|
|
1425
|
+
});
|
|
1426
|
+
break;
|
|
1427
|
+
case "status":
|
|
1428
|
+
this.emit("status", {
|
|
1429
|
+
status: msg.status,
|
|
1430
|
+
uniqueId: msg.uniqueId,
|
|
1431
|
+
roomId: msg.roomId,
|
|
1432
|
+
language: msg.language,
|
|
1433
|
+
message: msg.message
|
|
1434
|
+
});
|
|
1435
|
+
break;
|
|
1436
|
+
case "credits":
|
|
1437
|
+
this.emit("credits", {
|
|
1438
|
+
remaining: msg.remaining,
|
|
1439
|
+
total: msg.total,
|
|
1440
|
+
used: msg.used,
|
|
1441
|
+
warning: msg.warning
|
|
1442
|
+
});
|
|
1443
|
+
break;
|
|
1444
|
+
case "credits_low":
|
|
1445
|
+
this.emit("credits_low", {
|
|
1446
|
+
remaining: msg.remaining,
|
|
1447
|
+
total: msg.total,
|
|
1448
|
+
percent: msg.percent
|
|
1449
|
+
});
|
|
1450
|
+
break;
|
|
1451
|
+
case "error":
|
|
1452
|
+
this.emit("error", {
|
|
1453
|
+
code: msg.code,
|
|
1454
|
+
message: msg.message
|
|
1455
|
+
});
|
|
1456
|
+
break;
|
|
1457
|
+
// Handle interim/final captions from server (sentence-level accumulation)
|
|
1458
|
+
case "interim":
|
|
1459
|
+
this.emit("caption", {
|
|
1460
|
+
text: msg.text,
|
|
1461
|
+
language: msg.language,
|
|
1462
|
+
isFinal: false,
|
|
1463
|
+
confidence: msg.confidence || 0,
|
|
1464
|
+
speaker: msg.speaker
|
|
1465
|
+
});
|
|
1466
|
+
break;
|
|
1467
|
+
case "final":
|
|
1468
|
+
this.emit("caption", {
|
|
1469
|
+
text: msg.text,
|
|
1470
|
+
language: msg.language,
|
|
1471
|
+
isFinal: true,
|
|
1472
|
+
confidence: msg.confidence || 0,
|
|
1473
|
+
speaker: msg.speaker
|
|
1474
|
+
});
|
|
1475
|
+
break;
|
|
1476
|
+
case "translation_interim":
|
|
1477
|
+
this.emit("translation", {
|
|
1478
|
+
text: msg.text,
|
|
1479
|
+
language: msg.language,
|
|
1480
|
+
isFinal: false,
|
|
1481
|
+
confidence: msg.confidence || 0,
|
|
1482
|
+
speaker: msg.speaker
|
|
1483
|
+
});
|
|
1484
|
+
break;
|
|
1485
|
+
case "translation_final":
|
|
1486
|
+
this.emit("translation", {
|
|
1487
|
+
text: msg.text,
|
|
1488
|
+
language: msg.language,
|
|
1489
|
+
isFinal: true,
|
|
1490
|
+
confidence: msg.confidence || 0,
|
|
1491
|
+
speaker: msg.speaker
|
|
1492
|
+
});
|
|
1493
|
+
break;
|
|
1494
|
+
default:
|
|
1495
|
+
if (this.debug) {
|
|
1496
|
+
console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
} catch {
|
|
1500
|
+
if (this.debug) console.error("[Captions] Failed to parse message:", raw);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Connect to the TikTok FLV stream and extract audio.
|
|
1505
|
+
* Sends binary audio buffers to the server via WebSocket.
|
|
1506
|
+
*/
|
|
1507
|
+
async connectToStream(streamInfo) {
|
|
1508
|
+
const url = streamInfo.audioOnlyUrl || streamInfo.flvUrl;
|
|
1509
|
+
if (!url) {
|
|
1510
|
+
this.emit("error", { code: "NO_STREAM_URL", message: "Server did not provide a usable stream URL" });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.streamUrl = url;
|
|
1514
|
+
if (this.debug) console.log(`[Captions] connectToStream: URL selected: ${url.substring(0, 80)}...`);
|
|
1515
|
+
if (this.streamAbortController) {
|
|
1516
|
+
this.streamAbortController.abort();
|
|
1517
|
+
}
|
|
1518
|
+
this.streamAbortController = new AbortController();
|
|
1519
|
+
let audioFramesSent = 0;
|
|
1520
|
+
let audioBytesSent = 0;
|
|
1521
|
+
this.flvExtractor = new FlvAudioExtractor((adtsFrame) => {
|
|
1522
|
+
if (this.ws?.readyState === import_ws2.default.OPEN) {
|
|
1523
|
+
this.ws.send(adtsFrame);
|
|
1524
|
+
audioFramesSent++;
|
|
1525
|
+
audioBytesSent += adtsFrame.length;
|
|
1526
|
+
if (this.debug && (audioFramesSent <= 3 || audioFramesSent % 100 === 0)) {
|
|
1527
|
+
console.log(`[Captions] Audio frame #${audioFramesSent}: ${adtsFrame.length}b (total: ${audioBytesSent}b)`);
|
|
1528
|
+
}
|
|
1529
|
+
} else if (this.debug && audioFramesSent === 0) {
|
|
1530
|
+
console.log(`[Captions] WARNING: WS not open (readyState=${this.ws?.readyState}), cannot send audio`);
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
try {
|
|
1534
|
+
if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
|
|
1535
|
+
const resp = await fetch(url, {
|
|
1536
|
+
signal: this.streamAbortController.signal,
|
|
1537
|
+
headers: {
|
|
1538
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
if (this.debug) console.log(`[Captions] connectToStream: fetch returned status=${resp.status}, hasBody=${!!resp.body}`);
|
|
1542
|
+
if (!resp.ok || !resp.body) {
|
|
1543
|
+
throw new Error(`FLV stream HTTP ${resp.status}`);
|
|
1544
|
+
}
|
|
1545
|
+
if (this.debug) console.log(`[Captions] FLV stream connected (${resp.status})`);
|
|
1546
|
+
const reader = resp.body.getReader ? resp.body.getReader() : null;
|
|
1547
|
+
if (this.debug) console.log(`[Captions] connectToStream: hasReader=${!!reader}, hasAsyncIterator=${typeof resp.body[Symbol.asyncIterator] === "function"}`);
|
|
1548
|
+
if (reader) {
|
|
1549
|
+
const processStream = async () => {
|
|
1550
|
+
let chunks = 0;
|
|
1551
|
+
try {
|
|
1552
|
+
while (true) {
|
|
1553
|
+
const { done, value } = await reader.read();
|
|
1554
|
+
if (done || this.intentionalClose) {
|
|
1555
|
+
if (this.debug) console.log(`[Captions] FLV stream ended (done=${done}, intentionalClose=${this.intentionalClose}), chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1556
|
+
break;
|
|
1557
|
+
}
|
|
1558
|
+
chunks++;
|
|
1559
|
+
if (value && this.flvExtractor) {
|
|
1560
|
+
this.flvExtractor.push(value);
|
|
1561
|
+
}
|
|
1562
|
+
if (this.debug && chunks <= 3) {
|
|
1563
|
+
console.log(`[Captions] FLV chunk #${chunks}: ${value?.length || 0}b`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1568
|
+
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1569
|
+
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1570
|
+
} else if (this.debug) {
|
|
1571
|
+
console.log(`[Captions] FLV stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
processStream();
|
|
1576
|
+
} else if (typeof resp.body[Symbol.asyncIterator] === "function") {
|
|
1577
|
+
const processNodeStream = async () => {
|
|
1578
|
+
let chunks = 0;
|
|
1579
|
+
try {
|
|
1580
|
+
for await (const chunk of resp.body) {
|
|
1581
|
+
if (this.intentionalClose) break;
|
|
1582
|
+
chunks++;
|
|
1583
|
+
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
1584
|
+
if (this.flvExtractor) {
|
|
1585
|
+
this.flvExtractor.push(u8);
|
|
1586
|
+
}
|
|
1587
|
+
if (this.debug && chunks <= 3) {
|
|
1588
|
+
console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (this.debug) console.log(`[Captions] Node stream ended, chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1594
|
+
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1595
|
+
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1596
|
+
} else if (this.debug) {
|
|
1597
|
+
console.log(`[Captions] FLV node stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
processNodeStream();
|
|
1602
|
+
} else {
|
|
1603
|
+
if (this.debug) console.error(`[Captions] ERROR: resp.body has no getReader() and no asyncIterator!`);
|
|
1604
|
+
}
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1607
|
+
console.error("[Captions] FLV stream connect error:", err.message);
|
|
1608
|
+
this.emit("error", { code: "STREAM_CONNECT_ERROR", message: err.message });
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1170
1614
|
// src/api.ts
|
|
1171
1615
|
var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
|
|
1172
1616
|
var PAGE_CACHE_TTL = 5 * 60 * 1e3;
|
|
@@ -1224,6 +1668,7 @@ async function getRegionalRanklist(opts) {
|
|
|
1224
1668
|
}
|
|
1225
1669
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1226
1670
|
0 && (module.exports = {
|
|
1671
|
+
TikTokCaptions,
|
|
1227
1672
|
TikTokLive,
|
|
1228
1673
|
getRanklist,
|
|
1229
1674
|
getRegionalRanklist
|