279 lines
7.4 KiB
JavaScript
279 lines
7.4 KiB
JavaScript
/**
|
||
* Realtime ASR websocket client.
|
||
* Browser audio is sent only to the Speakr backend proxy.
|
||
*/
|
||
|
||
function getBackendAsrWebSocketUrl(mode) {
|
||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||
const basePath = "/tool/speakr/ws/asr/live";
|
||
return `${protocol}//${window.location.host}${basePath}?mode=${encodeURIComponent(mode || "online")}`;
|
||
}
|
||
|
||
function WebSocketConnectMethod(config) {
|
||
//定义socket连接方法类
|
||
|
||
var speechSokt;
|
||
var connKeeperID;
|
||
|
||
var msgHandle = config.msgHandle;
|
||
var stateHandle = config.stateHandle;
|
||
let modeType = "offline";
|
||
|
||
// 发送队列和流量控制
|
||
var sendQueue = [];
|
||
var isSending = false;
|
||
var maxQueueSize = 1000; // 最大队列长度,防止内存溢出
|
||
var queueWarningThreshold = 500; // 队列警告阈值
|
||
var lastDisconnWarnTime = 0; // WebSocket未连接警告节流
|
||
var gracefulStopResolver = null;
|
||
var gracefulStopTimer = null;
|
||
function trimQueueIfNeeded() {
|
||
if (sendQueue.length <= maxQueueSize) {
|
||
return false;
|
||
}
|
||
|
||
const dropCount = sendQueue.length - maxQueueSize;
|
||
console.warn(
|
||
`⚠️ 发送队列超过最大限制 (${maxQueueSize}),丢弃最旧的 ${dropCount} 个数据包`
|
||
);
|
||
sendQueue = sendQueue.slice(-maxQueueSize);
|
||
return true;
|
||
}
|
||
|
||
function enqueueData(data) {
|
||
sendQueue.push(data);
|
||
trimQueueIfNeeded();
|
||
|
||
if (sendQueue.length > queueWarningThreshold) {
|
||
console.warn(`⚠️ WebSocket发送队列堆积: ${sendQueue.length} 个数据包待发送`);
|
||
}
|
||
}
|
||
|
||
this.wsStart = function (e) {
|
||
if (speechSokt) {
|
||
speechSokt.close();
|
||
}
|
||
if (e === "offline" || (e && e !== "online")) {
|
||
modeType = "offline";
|
||
} else {
|
||
modeType = "online";
|
||
}
|
||
var Uri = getBackendAsrWebSocketUrl(modeType);
|
||
if (Uri.match(/wss:\S*|ws:\S*/)) {
|
||
console.log("Uri" + Uri);
|
||
} else {
|
||
alert("请检查wss地址正确性");
|
||
return 0;
|
||
}
|
||
|
||
if ("WebSocket" in window) {
|
||
speechSokt = new WebSocket(Uri); // 定义socket连接对象
|
||
speechSokt.onopen = function (e) {
|
||
onOpen(e);
|
||
}; // 定义响应函数
|
||
speechSokt.onclose = function (e) {
|
||
console.log("onclose ws!");
|
||
onClose(e);
|
||
};
|
||
speechSokt.onmessage = function (e) {
|
||
onMessage(e);
|
||
};
|
||
speechSokt.onerror = function (e) {
|
||
onError(e);
|
||
};
|
||
return 1;
|
||
} else {
|
||
alert("当前浏览器不支持 WebSocket");
|
||
return 0;
|
||
}
|
||
};
|
||
|
||
// 定义停止与发送函数
|
||
function resolveGracefulStop() {
|
||
if (gracefulStopTimer) {
|
||
clearTimeout(gracefulStopTimer);
|
||
gracefulStopTimer = null;
|
||
}
|
||
if (gracefulStopResolver) {
|
||
gracefulStopResolver();
|
||
gracefulStopResolver = null;
|
||
}
|
||
}
|
||
|
||
function closeSocketNow() {
|
||
if (speechSokt != undefined) {
|
||
try {
|
||
speechSokt.close();
|
||
} catch (e) {
|
||
console.warn("Failed to close WebSocket", e);
|
||
}
|
||
speechSokt = undefined;
|
||
}
|
||
sendQueue = [];
|
||
isSending = false;
|
||
}
|
||
|
||
this.wsStop = function () {
|
||
if (speechSokt != undefined) {
|
||
console.log("stop ws!");
|
||
try {
|
||
if (speechSokt.readyState === 1) {
|
||
speechSokt.send(JSON.stringify({ type: "stop", is_speaking: false }));
|
||
}
|
||
} catch (e) {
|
||
console.warn("Failed to send WebSocket stop frame", e);
|
||
}
|
||
}
|
||
resolveGracefulStop();
|
||
closeSocketNow();
|
||
};
|
||
|
||
this.wsFinish = function (timeoutMs = 3000) {
|
||
if (speechSokt == undefined || speechSokt.readyState >= 2) {
|
||
resolveGracefulStop();
|
||
return Promise.resolve();
|
||
}
|
||
|
||
sendQueue = [];
|
||
isSending = false;
|
||
|
||
return new Promise((resolve) => {
|
||
gracefulStopResolver = resolve;
|
||
gracefulStopTimer = setTimeout(() => {
|
||
// Give the server ownership of the close. Timeout only unblocks UI.
|
||
resolveGracefulStop();
|
||
}, timeoutMs);
|
||
|
||
try {
|
||
if (speechSokt.readyState === 1) {
|
||
speechSokt.send(JSON.stringify({ type: "stop", is_speaking: false }));
|
||
}
|
||
} catch (e) {
|
||
console.warn("Failed to send WebSocket finish frame", e);
|
||
resolveGracefulStop();
|
||
}
|
||
});
|
||
};
|
||
|
||
// 处理发送队列
|
||
function processQueue() {
|
||
if (
|
||
isSending ||
|
||
sendQueue.length === 0 ||
|
||
!speechSokt ||
|
||
speechSokt.readyState !== 1
|
||
) {
|
||
return;
|
||
}
|
||
|
||
isSending = true;
|
||
const data = sendQueue.shift();
|
||
|
||
try {
|
||
speechSokt.send(data);
|
||
isSending = false;
|
||
|
||
// 队列警告
|
||
if (sendQueue.length > queueWarningThreshold) {
|
||
console.warn(`⚠️ WebSocket发送队列堆积: ${sendQueue.length} 个数据包待发送`);
|
||
}
|
||
|
||
// 继续处理队列
|
||
if (sendQueue.length > 0) {
|
||
// 使用 setTimeout 避免阻塞
|
||
setTimeout(() => processQueue(), 0);
|
||
}
|
||
} catch (e) {
|
||
console.error("❌ WebSocket发送失败,数据已放回队列:", e);
|
||
sendQueue.unshift(data); // 放回队列头部
|
||
isSending = false;
|
||
trimQueueIfNeeded();
|
||
}
|
||
}
|
||
|
||
this.wsSend = function (oneData) {
|
||
if (speechSokt == undefined) {
|
||
// WebSocket未初始化,缓存数据
|
||
enqueueData(oneData);
|
||
return true;
|
||
}
|
||
|
||
if (speechSokt.readyState === 1) {
|
||
// WebSocket已打开
|
||
if (sendQueue.length > 0) {
|
||
// 队列中有数据,先加入队列
|
||
enqueueData(oneData);
|
||
processQueue();
|
||
return true;
|
||
} else {
|
||
// 队列为空,尝试直接发送
|
||
try {
|
||
speechSokt.send(oneData);
|
||
return true;
|
||
} catch (e) {
|
||
console.error("❌ WebSocket直接发送失败,加入队列:", e);
|
||
enqueueData(oneData);
|
||
processQueue();
|
||
return true;
|
||
}
|
||
}
|
||
} else if (speechSokt.readyState === 0) {
|
||
// WebSocket正在连接,缓存数据
|
||
enqueueData(oneData);
|
||
return true;
|
||
} else {
|
||
// WebSocket已关闭或关闭中,丢弃数据并警告(节流 5 秒)
|
||
var now = Date.now();
|
||
if (now - lastDisconnWarnTime > 5000) {
|
||
console.warn("⚠️ WebSocket未连接,数据无法发送 (state:", speechSokt.readyState, ")");
|
||
lastDisconnWarnTime = now;
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 获取队列状态(用于调试)
|
||
this.getQueueStatus = function () {
|
||
return {
|
||
length: sendQueue.length,
|
||
isSending: isSending,
|
||
readyState: speechSokt ? speechSokt.readyState : -1
|
||
};
|
||
};
|
||
|
||
|
||
// SOCEKT连接中的消息与状态响应
|
||
function onOpen(e) {
|
||
console.log(e, "e");
|
||
console.log("WebSocket connected to backend ASR proxy");
|
||
stateHandle(0);
|
||
|
||
if (sendQueue.length > 0) {
|
||
console.log(`Processing ${sendQueue.length} queued audio chunks`);
|
||
processQueue();
|
||
}
|
||
}
|
||
|
||
function onClose(e) {
|
||
resolveGracefulStop();
|
||
speechSokt = undefined;
|
||
stateHandle(1);
|
||
}
|
||
|
||
function onMessage(e) {
|
||
msgHandle(e);
|
||
try {
|
||
const payload = JSON.parse(e.data);
|
||
if (payload && payload.type === "state" && payload.state === "closed") {
|
||
resolveGracefulStop();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function onError(e) {
|
||
// info_div.innerHTML="连接"+e;
|
||
console.log(e);
|
||
stateHandle(2);
|
||
}
|
||
}
|