@tiktool/live 2.6.2 → 2.6.4
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 +68 -5
- package/dist/index.d.mts +150 -1
- package/dist/index.d.ts +150 -1
- package/dist/index.js +281 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +279 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,8 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
TikTokCaptions: () => TikTokCaptions,
|
|
34
34
|
TikTokLive: () => TikTokLive,
|
|
35
|
+
fetchFeed: () => fetchFeed,
|
|
36
|
+
getLiveFeed: () => getLiveFeed,
|
|
35
37
|
getRanklist: () => getRanklist,
|
|
36
38
|
getRegionalRanklist: () => getRegionalRanklist
|
|
37
39
|
});
|
|
@@ -1173,6 +1175,86 @@ var TikTokLive = class extends import_events.EventEmitter {
|
|
|
1173
1175
|
// src/captions.ts
|
|
1174
1176
|
var import_events2 = require("events");
|
|
1175
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);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
this.buffer = this.buffer.subarray(totalTagSize);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
reset() {
|
|
1253
|
+
this.buffer = new Uint8Array(0);
|
|
1254
|
+
this.headerParsed = false;
|
|
1255
|
+
this.ascParsed = false;
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1176
1258
|
var DEFAULT_CAPTIONS_SERVER = "wss://api.tik.tools";
|
|
1177
1259
|
var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
1178
1260
|
ws = null;
|
|
@@ -1189,6 +1271,9 @@ var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
|
1189
1271
|
_diarization;
|
|
1190
1272
|
_maxDurationMinutes;
|
|
1191
1273
|
_language;
|
|
1274
|
+
streamAbortController = null;
|
|
1275
|
+
flvExtractor = null;
|
|
1276
|
+
streamUrl = null;
|
|
1192
1277
|
constructor(options) {
|
|
1193
1278
|
super();
|
|
1194
1279
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
@@ -1247,6 +1332,14 @@ var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
|
1247
1332
|
*/
|
|
1248
1333
|
stop() {
|
|
1249
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
|
+
}
|
|
1250
1343
|
if (this.ws) {
|
|
1251
1344
|
this.send({ action: "stop" });
|
|
1252
1345
|
this.ws.close(1e3);
|
|
@@ -1309,6 +1402,10 @@ var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
|
1309
1402
|
try {
|
|
1310
1403
|
const msg = JSON.parse(raw);
|
|
1311
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;
|
|
1312
1409
|
case "caption":
|
|
1313
1410
|
this.emit("caption", {
|
|
1314
1411
|
text: msg.text,
|
|
@@ -1359,15 +1456,161 @@ var TikTokCaptions = class extends import_events2.EventEmitter {
|
|
|
1359
1456
|
message: msg.message
|
|
1360
1457
|
});
|
|
1361
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;
|
|
1362
1496
|
default:
|
|
1363
1497
|
if (this.debug) {
|
|
1364
|
-
console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
|
|
1498
|
+
if (this.debug) console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
|
|
1365
1499
|
}
|
|
1366
1500
|
}
|
|
1367
1501
|
} catch {
|
|
1368
1502
|
if (this.debug) console.error("[Captions] Failed to parse message:", raw);
|
|
1369
1503
|
}
|
|
1370
1504
|
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Connect to the TikTok FLV stream and extract audio.
|
|
1507
|
+
* Sends binary audio buffers to the server via WebSocket.
|
|
1508
|
+
*/
|
|
1509
|
+
async connectToStream(streamInfo) {
|
|
1510
|
+
const url = streamInfo.audioOnlyUrl || streamInfo.flvUrl;
|
|
1511
|
+
if (!url) {
|
|
1512
|
+
this.emit("error", { code: "NO_STREAM_URL", message: "Server did not provide a usable stream URL" });
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
this.streamUrl = url;
|
|
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
|
+
});
|
|
1535
|
+
try {
|
|
1536
|
+
if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
|
|
1537
|
+
const resp = await fetch(url, {
|
|
1538
|
+
signal: this.streamAbortController.signal,
|
|
1539
|
+
headers: {
|
|
1540
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
if (this.debug) console.log(`[Captions] connectToStream: fetch returned status=${resp.status}, hasBody=${!!resp.body}`);
|
|
1544
|
+
if (!resp.ok || !resp.body) {
|
|
1545
|
+
throw new Error(`FLV stream HTTP ${resp.status}`);
|
|
1546
|
+
}
|
|
1547
|
+
if (this.debug) console.log(`[Captions] FLV stream connected (${resp.status})`);
|
|
1548
|
+
const reader = resp.body.getReader ? resp.body.getReader() : null;
|
|
1549
|
+
if (this.debug) console.log(`[Captions] connectToStream: hasReader=${!!reader}, hasAsyncIterator=${typeof resp.body[Symbol.asyncIterator] === "function"}`);
|
|
1550
|
+
if (reader) {
|
|
1551
|
+
const processStream = async () => {
|
|
1552
|
+
let chunks = 0;
|
|
1553
|
+
try {
|
|
1554
|
+
while (true) {
|
|
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`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
processStream();
|
|
1578
|
+
} else if (typeof resp.body[Symbol.asyncIterator] === "function") {
|
|
1579
|
+
const processNodeStream = async () => {
|
|
1580
|
+
let chunks = 0;
|
|
1581
|
+
try {
|
|
1582
|
+
for await (const chunk of resp.body) {
|
|
1583
|
+
if (this.intentionalClose) break;
|
|
1584
|
+
chunks++;
|
|
1585
|
+
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
1586
|
+
if (this.flvExtractor) {
|
|
1587
|
+
this.flvExtractor.push(u8);
|
|
1588
|
+
}
|
|
1589
|
+
if (this.debug && chunks <= 3) {
|
|
1590
|
+
if (this.debug) console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
|
|
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
|
+
}
|
|
1601
|
+
}
|
|
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 });
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1371
1614
|
};
|
|
1372
1615
|
|
|
1373
1616
|
// src/api.ts
|
|
@@ -1425,10 +1668,47 @@ async function getRegionalRanklist(opts) {
|
|
|
1425
1668
|
}
|
|
1426
1669
|
return data;
|
|
1427
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;
|
|
1697
|
+
}
|
|
1698
|
+
const resp = await fetch(signed.signed_url, { headers, redirect: "follow" });
|
|
1699
|
+
const text = await resp.text();
|
|
1700
|
+
try {
|
|
1701
|
+
return JSON.parse(text);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1428
1706
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1429
1707
|
0 && (module.exports = {
|
|
1430
1708
|
TikTokCaptions,
|
|
1431
1709
|
TikTokLive,
|
|
1710
|
+
fetchFeed,
|
|
1711
|
+
getLiveFeed,
|
|
1432
1712
|
getRanklist,
|
|
1433
1713
|
getRegionalRanklist
|
|
1434
1714
|
});
|