@tiktool/live 2.6.2 → 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/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +241 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1134,6 +1134,86 @@ var TikTokLive = class extends EventEmitter {
|
|
|
1134
1134
|
// src/captions.ts
|
|
1135
1135
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
1136
1136
|
import WebSocket2 from "ws";
|
|
1137
|
+
var FLV_TAG_AUDIO = 8;
|
|
1138
|
+
var FLV_HEADER_SIZE = 9;
|
|
1139
|
+
var FLV_PREV_TAG_SIZE = 4;
|
|
1140
|
+
var FlvAudioExtractor = class {
|
|
1141
|
+
buffer = new Uint8Array(0);
|
|
1142
|
+
headerParsed = false;
|
|
1143
|
+
onAudio;
|
|
1144
|
+
aacProfile = 2;
|
|
1145
|
+
sampleRateIndex = 4;
|
|
1146
|
+
channelConfig = 2;
|
|
1147
|
+
ascParsed = false;
|
|
1148
|
+
constructor(onAudio) {
|
|
1149
|
+
this.onAudio = onAudio;
|
|
1150
|
+
}
|
|
1151
|
+
parseASC(asc) {
|
|
1152
|
+
if (asc.length < 2) return;
|
|
1153
|
+
this.aacProfile = asc[0] >> 3 & 31;
|
|
1154
|
+
this.sampleRateIndex = (asc[0] & 7) << 1 | asc[1] >> 7 & 1;
|
|
1155
|
+
this.channelConfig = asc[1] >> 3 & 15;
|
|
1156
|
+
this.ascParsed = true;
|
|
1157
|
+
}
|
|
1158
|
+
buildAdtsHeader(frameLength) {
|
|
1159
|
+
const adts = new Uint8Array(7);
|
|
1160
|
+
const fullLength = frameLength + 7;
|
|
1161
|
+
const profile = this.aacProfile - 1;
|
|
1162
|
+
adts[0] = 255;
|
|
1163
|
+
adts[1] = 241;
|
|
1164
|
+
adts[2] = (profile & 3) << 6 | (this.sampleRateIndex & 15) << 2 | this.channelConfig >> 2 & 1;
|
|
1165
|
+
adts[3] = (this.channelConfig & 3) << 6 | fullLength >> 11 & 3;
|
|
1166
|
+
adts[4] = fullLength >> 3 & 255;
|
|
1167
|
+
adts[5] = (fullLength & 7) << 5 | 31;
|
|
1168
|
+
adts[6] = 252;
|
|
1169
|
+
return adts;
|
|
1170
|
+
}
|
|
1171
|
+
push(chunk) {
|
|
1172
|
+
const newBuf = new Uint8Array(this.buffer.length + chunk.length);
|
|
1173
|
+
newBuf.set(this.buffer, 0);
|
|
1174
|
+
newBuf.set(chunk, this.buffer.length);
|
|
1175
|
+
this.buffer = newBuf;
|
|
1176
|
+
if (!this.headerParsed) {
|
|
1177
|
+
if (this.buffer.length < FLV_HEADER_SIZE + FLV_PREV_TAG_SIZE) return;
|
|
1178
|
+
if (this.buffer[0] !== 70 || this.buffer[1] !== 76 || this.buffer[2] !== 86) return;
|
|
1179
|
+
const dv = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
|
|
1180
|
+
const dataOffset = dv.getUint32(5);
|
|
1181
|
+
this.buffer = this.buffer.subarray(dataOffset + FLV_PREV_TAG_SIZE);
|
|
1182
|
+
this.headerParsed = true;
|
|
1183
|
+
}
|
|
1184
|
+
while (this.buffer.length >= 11) {
|
|
1185
|
+
const tagType = this.buffer[0] & 31;
|
|
1186
|
+
const dataSize = this.buffer[1] << 16 | this.buffer[2] << 8 | this.buffer[3];
|
|
1187
|
+
const totalTagSize = 11 + dataSize + FLV_PREV_TAG_SIZE;
|
|
1188
|
+
if (this.buffer.length < totalTagSize) break;
|
|
1189
|
+
if (tagType === FLV_TAG_AUDIO) {
|
|
1190
|
+
const audioData = this.buffer.subarray(11, 11 + dataSize);
|
|
1191
|
+
if (audioData.length > 0) {
|
|
1192
|
+
const soundFormat = audioData[0] >> 4 & 15;
|
|
1193
|
+
if (soundFormat === 10 && audioData.length > 2) {
|
|
1194
|
+
const aacPacketType = audioData[1];
|
|
1195
|
+
if (aacPacketType === 0) {
|
|
1196
|
+
this.parseASC(audioData.subarray(2));
|
|
1197
|
+
} else if (aacPacketType === 1 && this.ascParsed) {
|
|
1198
|
+
const rawFrame = audioData.subarray(2);
|
|
1199
|
+
const adtsHeader = this.buildAdtsHeader(rawFrame.length);
|
|
1200
|
+
const adtsFrame = new Uint8Array(adtsHeader.length + rawFrame.length);
|
|
1201
|
+
adtsFrame.set(adtsHeader, 0);
|
|
1202
|
+
adtsFrame.set(rawFrame, adtsHeader.length);
|
|
1203
|
+
this.onAudio(adtsFrame);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
this.buffer = this.buffer.subarray(totalTagSize);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
reset() {
|
|
1212
|
+
this.buffer = new Uint8Array(0);
|
|
1213
|
+
this.headerParsed = false;
|
|
1214
|
+
this.ascParsed = false;
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1137
1217
|
var DEFAULT_CAPTIONS_SERVER = "wss://api.tik.tools";
|
|
1138
1218
|
var TikTokCaptions = class extends EventEmitter2 {
|
|
1139
1219
|
ws = null;
|
|
@@ -1150,6 +1230,9 @@ var TikTokCaptions = class extends EventEmitter2 {
|
|
|
1150
1230
|
_diarization;
|
|
1151
1231
|
_maxDurationMinutes;
|
|
1152
1232
|
_language;
|
|
1233
|
+
streamAbortController = null;
|
|
1234
|
+
flvExtractor = null;
|
|
1235
|
+
streamUrl = null;
|
|
1153
1236
|
constructor(options) {
|
|
1154
1237
|
super();
|
|
1155
1238
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
@@ -1208,6 +1291,14 @@ var TikTokCaptions = class extends EventEmitter2 {
|
|
|
1208
1291
|
*/
|
|
1209
1292
|
stop() {
|
|
1210
1293
|
this.intentionalClose = true;
|
|
1294
|
+
if (this.streamAbortController) {
|
|
1295
|
+
this.streamAbortController.abort();
|
|
1296
|
+
this.streamAbortController = null;
|
|
1297
|
+
}
|
|
1298
|
+
if (this.flvExtractor) {
|
|
1299
|
+
this.flvExtractor.reset();
|
|
1300
|
+
this.flvExtractor = null;
|
|
1301
|
+
}
|
|
1211
1302
|
if (this.ws) {
|
|
1212
1303
|
this.send({ action: "stop" });
|
|
1213
1304
|
this.ws.close(1e3);
|
|
@@ -1270,6 +1361,10 @@ var TikTokCaptions = class extends EventEmitter2 {
|
|
|
1270
1361
|
try {
|
|
1271
1362
|
const msg = JSON.parse(raw);
|
|
1272
1363
|
switch (msg.type) {
|
|
1364
|
+
case "stream_info":
|
|
1365
|
+
if (this.debug) console.log(`[Captions] Received stream_info: flv=${!!msg.flvUrl}, hls=${!!msg.hlsUrl}, ao=${!!msg.audioOnlyUrl}`);
|
|
1366
|
+
this.connectToStream(msg);
|
|
1367
|
+
break;
|
|
1273
1368
|
case "caption":
|
|
1274
1369
|
this.emit("caption", {
|
|
1275
1370
|
text: msg.text,
|
|
@@ -1320,6 +1415,43 @@ var TikTokCaptions = class extends EventEmitter2 {
|
|
|
1320
1415
|
message: msg.message
|
|
1321
1416
|
});
|
|
1322
1417
|
break;
|
|
1418
|
+
// Handle interim/final captions from server (sentence-level accumulation)
|
|
1419
|
+
case "interim":
|
|
1420
|
+
this.emit("caption", {
|
|
1421
|
+
text: msg.text,
|
|
1422
|
+
language: msg.language,
|
|
1423
|
+
isFinal: false,
|
|
1424
|
+
confidence: msg.confidence || 0,
|
|
1425
|
+
speaker: msg.speaker
|
|
1426
|
+
});
|
|
1427
|
+
break;
|
|
1428
|
+
case "final":
|
|
1429
|
+
this.emit("caption", {
|
|
1430
|
+
text: msg.text,
|
|
1431
|
+
language: msg.language,
|
|
1432
|
+
isFinal: true,
|
|
1433
|
+
confidence: msg.confidence || 0,
|
|
1434
|
+
speaker: msg.speaker
|
|
1435
|
+
});
|
|
1436
|
+
break;
|
|
1437
|
+
case "translation_interim":
|
|
1438
|
+
this.emit("translation", {
|
|
1439
|
+
text: msg.text,
|
|
1440
|
+
language: msg.language,
|
|
1441
|
+
isFinal: false,
|
|
1442
|
+
confidence: msg.confidence || 0,
|
|
1443
|
+
speaker: msg.speaker
|
|
1444
|
+
});
|
|
1445
|
+
break;
|
|
1446
|
+
case "translation_final":
|
|
1447
|
+
this.emit("translation", {
|
|
1448
|
+
text: msg.text,
|
|
1449
|
+
language: msg.language,
|
|
1450
|
+
isFinal: true,
|
|
1451
|
+
confidence: msg.confidence || 0,
|
|
1452
|
+
speaker: msg.speaker
|
|
1453
|
+
});
|
|
1454
|
+
break;
|
|
1323
1455
|
default:
|
|
1324
1456
|
if (this.debug) {
|
|
1325
1457
|
console.log(`[Captions] Unknown message type: ${msg.type}`, msg);
|
|
@@ -1329,6 +1461,115 @@ var TikTokCaptions = class extends EventEmitter2 {
|
|
|
1329
1461
|
if (this.debug) console.error("[Captions] Failed to parse message:", raw);
|
|
1330
1462
|
}
|
|
1331
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Connect to the TikTok FLV stream and extract audio.
|
|
1466
|
+
* Sends binary audio buffers to the server via WebSocket.
|
|
1467
|
+
*/
|
|
1468
|
+
async connectToStream(streamInfo) {
|
|
1469
|
+
const url = streamInfo.audioOnlyUrl || streamInfo.flvUrl;
|
|
1470
|
+
if (!url) {
|
|
1471
|
+
this.emit("error", { code: "NO_STREAM_URL", message: "Server did not provide a usable stream URL" });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
this.streamUrl = url;
|
|
1475
|
+
if (this.debug) console.log(`[Captions] connectToStream: URL selected: ${url.substring(0, 80)}...`);
|
|
1476
|
+
if (this.streamAbortController) {
|
|
1477
|
+
this.streamAbortController.abort();
|
|
1478
|
+
}
|
|
1479
|
+
this.streamAbortController = new AbortController();
|
|
1480
|
+
let audioFramesSent = 0;
|
|
1481
|
+
let audioBytesSent = 0;
|
|
1482
|
+
this.flvExtractor = new FlvAudioExtractor((adtsFrame) => {
|
|
1483
|
+
if (this.ws?.readyState === WebSocket2.OPEN) {
|
|
1484
|
+
this.ws.send(adtsFrame);
|
|
1485
|
+
audioFramesSent++;
|
|
1486
|
+
audioBytesSent += adtsFrame.length;
|
|
1487
|
+
if (this.debug && (audioFramesSent <= 3 || audioFramesSent % 100 === 0)) {
|
|
1488
|
+
console.log(`[Captions] Audio frame #${audioFramesSent}: ${adtsFrame.length}b (total: ${audioBytesSent}b)`);
|
|
1489
|
+
}
|
|
1490
|
+
} else if (this.debug && audioFramesSent === 0) {
|
|
1491
|
+
console.log(`[Captions] WARNING: WS not open (readyState=${this.ws?.readyState}), cannot send audio`);
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
try {
|
|
1495
|
+
if (this.debug) console.log(`[Captions] connectToStream: calling fetch()...`);
|
|
1496
|
+
const resp = await fetch(url, {
|
|
1497
|
+
signal: this.streamAbortController.signal,
|
|
1498
|
+
headers: {
|
|
1499
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
if (this.debug) console.log(`[Captions] connectToStream: fetch returned status=${resp.status}, hasBody=${!!resp.body}`);
|
|
1503
|
+
if (!resp.ok || !resp.body) {
|
|
1504
|
+
throw new Error(`FLV stream HTTP ${resp.status}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (this.debug) console.log(`[Captions] FLV stream connected (${resp.status})`);
|
|
1507
|
+
const reader = resp.body.getReader ? resp.body.getReader() : null;
|
|
1508
|
+
if (this.debug) console.log(`[Captions] connectToStream: hasReader=${!!reader}, hasAsyncIterator=${typeof resp.body[Symbol.asyncIterator] === "function"}`);
|
|
1509
|
+
if (reader) {
|
|
1510
|
+
const processStream = async () => {
|
|
1511
|
+
let chunks = 0;
|
|
1512
|
+
try {
|
|
1513
|
+
while (true) {
|
|
1514
|
+
const { done, value } = await reader.read();
|
|
1515
|
+
if (done || this.intentionalClose) {
|
|
1516
|
+
if (this.debug) console.log(`[Captions] FLV stream ended (done=${done}, intentionalClose=${this.intentionalClose}), chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1517
|
+
break;
|
|
1518
|
+
}
|
|
1519
|
+
chunks++;
|
|
1520
|
+
if (value && this.flvExtractor) {
|
|
1521
|
+
this.flvExtractor.push(value);
|
|
1522
|
+
}
|
|
1523
|
+
if (this.debug && chunks <= 3) {
|
|
1524
|
+
console.log(`[Captions] FLV chunk #${chunks}: ${value?.length || 0}b`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1529
|
+
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1530
|
+
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1531
|
+
} else if (this.debug) {
|
|
1532
|
+
console.log(`[Captions] FLV stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
processStream();
|
|
1537
|
+
} else if (typeof resp.body[Symbol.asyncIterator] === "function") {
|
|
1538
|
+
const processNodeStream = async () => {
|
|
1539
|
+
let chunks = 0;
|
|
1540
|
+
try {
|
|
1541
|
+
for await (const chunk of resp.body) {
|
|
1542
|
+
if (this.intentionalClose) break;
|
|
1543
|
+
chunks++;
|
|
1544
|
+
const u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
1545
|
+
if (this.flvExtractor) {
|
|
1546
|
+
this.flvExtractor.push(u8);
|
|
1547
|
+
}
|
|
1548
|
+
if (this.debug && chunks <= 3) {
|
|
1549
|
+
console.log(`[Captions] FLV chunk #${chunks}: ${u8.length}b`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (this.debug) console.log(`[Captions] Node stream ended, chunks=${chunks}, audioFrames=${audioFramesSent}`);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1555
|
+
if (this.debug) console.error("[Captions] FLV stream read error:", err.message);
|
|
1556
|
+
this.emit("error", { code: "STREAM_READ_ERROR", message: err.message });
|
|
1557
|
+
} else if (this.debug) {
|
|
1558
|
+
console.log(`[Captions] FLV node stream aborted after ${chunks} chunks, ${audioFramesSent} audio frames`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
processNodeStream();
|
|
1563
|
+
} else {
|
|
1564
|
+
if (this.debug) console.error(`[Captions] ERROR: resp.body has no getReader() and no asyncIterator!`);
|
|
1565
|
+
}
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
if (err.name !== "AbortError" && !this.intentionalClose) {
|
|
1568
|
+
console.error("[Captions] FLV stream connect error:", err.message);
|
|
1569
|
+
this.emit("error", { code: "STREAM_CONNECT_ERROR", message: err.message });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1332
1573
|
};
|
|
1333
1574
|
|
|
1334
1575
|
// src/api.ts
|