mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
1,594 lines • 76.1 kB
JavaScript
class AudioDebugger {
/**
* 创建调试器
* @param options 调试器选项
*/
constructor(options) {
this._enabled = false;
this._prefix = "[AudioDebug]";
this._level = "all";
if (options) {
if (options.enabled !== void 0) this._enabled = options.enabled;
if (options.prefix) this._prefix = options.prefix;
if (options.level) this._level = options.level;
}
}
/**
* 启用调试
*/
enable() {
this._enabled = true;
}
/**
* 禁用调试
*/
disable() {
this._enabled = false;
}
/**
* 设置调试级别
* @param level 调试级别
*/
setLevel(level) {
this._level = level;
}
/**
* 设置日志前缀
* @param prefix 日志前缀
*/
setPrefix(prefix) {
this._prefix = prefix;
}
/**
* 输出信息日志
* @param message 日志消息
* @param data 附加数据
*/
log(message, ...data) {
if (this._enabled && (this._level === "info" || this._level === "all")) {
console.log(`${this._prefix} ${message}`, ...data);
}
}
/**
* 输出警告日志
* @param message 日志消息
* @param data 附加数据
*/
warn(message, ...data) {
if (this._enabled && (this._level === "warn" || this._level === "all")) {
console.warn(`${this._prefix} ${message}`, ...data);
}
}
/**
* 输出错误日志
* @param message 日志消息
* @param data 附加数据
*/
error(message, ...data) {
if (this._enabled && (this._level === "error" || this._level === "all")) {
console.error(`${this._prefix} ${message}`, ...data);
}
}
/**
* 获取当前是否启用
*/
get enabled() {
return this._enabled;
}
/**
* 获取当前日志级别
*/
get level() {
return this._level;
}
}
function createAudioDebugger(options) {
return new AudioDebugger(options);
}
const globalAudioDebugger = createAudioDebugger({
enabled: false,
prefix: "[AudioStream]"
});
class StreamAudioProcessor {
/**
* 创建流式音频处理器
* @param options 处理器配置
* @param callbacks 回调函数
*/
constructor(options = {}, callbacks = {}) {
this._isProcessing = true;
this._isPlaying = false;
this._currentAudio = null;
this._buffer = "";
this._audioChunks = [];
this._audioQueue = [];
this._isPlayingQueue = false;
this._shouldContinueQueue = true;
this._decoder = new TextDecoder();
this._isFirstChunk = true;
this._pendingFirstChunk = null;
this._accumulatedChunks = [];
this._playbackDelayCount = 0;
this._isProcessingQueue = false;
this._playLock = false;
this._processedAudioHashes = /* @__PURE__ */ new Set();
this._lastResponseSize = 0;
this._debugger = options.debugger || globalAudioDebugger;
if (options.debug === true && !options.debugger) {
this._debugger = createAudioDebugger({
enabled: true,
prefix: "[StreamAudioProcessor]"
});
}
this._options = {
mimeType: options.mimeType || "audio/mpeg",
autoPlay: options.autoPlay !== false,
audioDataField: options.audioDataField || "",
textDataField: options.textDataField || "",
processRawData: options.processRawData || false,
directAudioResponse: options.directAudioResponse || false,
playbackDelayChunks: options.playbackDelayChunks || 0,
playFirstChunkImmediately: options.playFirstChunkImmediately || false,
playInSequentialOrder: options.playInSequentialOrder !== false
};
this._callbacks = callbacks || {};
this._debugger.log("音频处理器已初始化", {
options: this._options,
hasCallbacks: !!callbacks,
callbackTypes: callbacks ? Object.keys(callbacks) : []
});
this._isPlaying = this._options.autoPlay;
this._isFirstChunk = true;
this._shouldContinueQueue = true;
this.onDownloadProgress = this.onDownloadProgress.bind(this);
this.addAudioToQueue = this.addAudioToQueue.bind(this);
this.playAudioBlob = this.playAudioBlob.bind(this);
}
/**
* 处理下载进度回调
* @param progressEvent 进度事件
*/
onDownloadProgress(progressEvent) {
var _a, _b, _c;
this._debugger.log("处理下载进度事件", {
eventType: typeof progressEvent,
hasEvent: !!progressEvent.event,
hasTarget: ((_a = progressEvent.event) == null ? void 0 : _a.target) ? true : false,
responseType: ((_c = (_b = progressEvent.event) == null ? void 0 : _b.target) == null ? void 0 : _c.response) ? typeof progressEvent.event.target.response : "undefined"
});
try {
if (!this._isProcessing) {
this._debugger.log("下载处理已暂停,跳过此事件");
return;
}
if (this._options.directAudioResponse) {
this._debugger.log("使用直接音频响应模式处理");
this._processDirectAudioResponse(progressEvent);
return;
}
if (progressEvent.event && progressEvent.event.target && progressEvent.event.target.response) {
this._debugger.log("从进度事件获取响应数据");
const response = progressEvent.event.target.response;
this._debugger.log(`响应数据类型: ${typeof response}`);
if (response instanceof ArrayBuffer || response instanceof Blob) {
this._debugger.log("响应是二进制数据,直接处理");
this._processRawAudio(response);
return;
}
if (typeof response === "string") {
this._debugger.log(`接收到字符串数据,长度: ${response.length}`);
let newContent = "";
if (this._buffer.length > 0) {
if (response.startsWith(this._buffer)) {
this._debugger.log(`响应包含现有缓冲区,提取新内容`);
newContent = response.slice(this._buffer.length);
} else {
this._debugger.log(`响应与缓冲区不匹配,使用整个响应`);
newContent = response;
}
} else {
this._debugger.log(`首次接收数据`);
newContent = response;
}
this._buffer = response;
if (newContent.length > 0) {
this._debugger.log(`处理新增内容,长度: ${newContent.length}`);
if (this._callbacks.onRawChunk) {
this._callbacks.onRawChunk(newContent);
}
if (this._options.processRawData) {
this._debugger.log("使用原始数据处理模式");
this._processRawAudio(newContent);
} else {
this._debugger.log("使用标准数据处理模式");
this._processNewData();
}
} else {
this._debugger.log("没有新增内容,跳过处理");
}
} else {
this._debugger.log(`无法处理未知类型的响应: ${typeof response}`);
}
} else {
this._debugger.log("进度事件中无响应数据");
}
} catch (error) {
this._debugger.error("处理下载进度事件失败:", error);
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 处理流式数据
* @param stream ReadableStream
*/
async _handleStream(stream) {
try {
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = this._decoder.decode(value, { stream: true });
this._buffer += chunk;
this._processNewData();
}
} catch (error) {
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 处理直接的二进制音频响应
* @param progressEvent 进度事件
*/
_processDirectAudioResponse(progressEvent) {
var _a, _b, _c;
this._debugger.log("处理直接音频响应", {
eventType: typeof progressEvent,
hasEvent: !!progressEvent.event,
hasTarget: ((_a = progressEvent.event) == null ? void 0 : _a.target) ? true : false,
responseType: ((_c = (_b = progressEvent.event) == null ? void 0 : _b.target) == null ? void 0 : _c.response) ? typeof progressEvent.event.target.response : "undefined"
});
try {
if (!progressEvent.event || !progressEvent.event.target) {
this._debugger.log("直接音频响应事件中没有 target");
return;
}
const response = progressEvent.event.target.response;
if (!response) {
this._debugger.log("直接音频响应中没有实际数据");
return;
}
this._debugger.log(`直接音频响应数据类型: ${typeof response}`);
const responseSize = (() => {
if (response instanceof Blob) return response.size;
if (response instanceof ArrayBuffer) return response.byteLength;
if (typeof response === "string") return response.length;
return 0;
})();
this._debugger.log(`响应大小: ${responseSize}字节, 上次大小: ${this._lastResponseSize}字节`);
if (responseSize > 0 && responseSize === this._lastResponseSize) {
this._debugger.log(`跳过可能重复的响应数据: ${responseSize}字节`);
return;
}
this._lastResponseSize = responseSize;
if (response instanceof Blob) {
this._debugger.log(`处理Blob响应: ${response.size}字节, 类型: ${response.type}`);
this._processRawAudio(response);
} else if (response instanceof ArrayBuffer) {
this._debugger.log(`处理ArrayBuffer响应: ${response.byteLength}字节`);
this._processRawAudio(response);
} else if (typeof response === "string") {
this._debugger.log(`处理字符串响应: ${response.length}字节`);
let newContent = "";
if (this._buffer.length > 0) {
if (response.startsWith(this._buffer)) {
newContent = response.slice(this._buffer.length);
this._debugger.log(`从响应中提取新内容: ${newContent.length}字节`);
} else {
newContent = response;
this._debugger.log(`使用整个响应作为新内容: ${newContent.length}字节`);
}
} else {
newContent = response;
this._debugger.log(`首次接收数据: ${newContent.length}字节`);
}
this._buffer = response;
if (newContent.length > 0) {
if (this._callbacks.onRawChunk) {
this._callbacks.onRawChunk(newContent);
}
this._processRawAudio(newContent);
} else {
this._debugger.log("没有新增内容,跳过处理");
}
} else {
this._debugger.log(`尝试处理未知类型的响应`);
this._processRawAudio(response);
}
} catch (error) {
this._debugger.error("处理直接音频响应失败:", error);
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 处理新收到的数据
*/
_processNewData() {
this._debugger.log(`处理新数据,当前缓冲区大小: ${this._buffer.length}字节`);
const lines = this._buffer.split("\n");
const lastLine = lines.pop() || "";
this._buffer = lastLine;
let processedLines = 0;
for (const line of lines) {
if (line.trim()) {
this._processLine(line.trim());
processedLines++;
}
}
this._debugger.log(`已处理${processedLines}行数据,剩余缓冲区大小: ${this._buffer.length}字节`);
if (this._buffer.trim()) {
try {
const isCompleteJson = (() => {
try {
JSON.parse(this._buffer);
return true;
} catch (e) {
return false;
}
})();
if (isCompleteJson || this._buffer.includes('"audio"') && this._buffer.includes('"}') || this._buffer.includes("'audio'") && this._buffer.includes("'}")) {
this._processLine(this._buffer.trim());
this._buffer = "";
this._debugger.log("已处理最后一行完整数据");
}
} catch (e) {
this._debugger.log("最后一行数据可能不完整,保留在缓冲区");
}
}
}
/**
* 处理单行数据
* @param line 数据行
*/
_processLine(line) {
try {
this._debugger.log(`处理数据行: 长度=${line.length}字节,前30个字符: ${line.substring(0, 30).replace(/\n/g, "\\n")}...`);
const dataPrefix = line.startsWith("data:") ? "data:" : "";
const content = dataPrefix ? line.substring(dataPrefix.length).trim() : line.trim();
if (!content) return;
if (this._callbacks.onRawChunk) {
this._callbacks.onRawChunk(content);
}
let audioData = this._extractAudioDataFromLine(content);
if (audioData) {
this._debugger.log(`提取到音频数据,大小=${audioData.length}`);
this._processAudioData(audioData);
} else {
try {
const jsonData = JSON.parse(content);
const findAudioField = (obj, path = "") => {
if (!obj || typeof obj !== "object") return null;
for (const key in obj) {
const currentPath = path ? `${path}.${key}` : key;
if (key === "audio" && typeof obj[key] === "string") {
return obj[key];
} else if (typeof obj[key] === "object" && obj[key] !== null) {
const result = findAudioField(obj[key], currentPath);
if (result) return result;
}
}
return null;
};
const foundAudio = findAudioField(jsonData);
if (foundAudio) {
this._debugger.log(`通过递归搜索找到音频数据: ${foundAudio.substring(0, 30)}...`);
this._processAudioData(foundAudio);
}
} catch (e) {
this._debugger.log("JSON解析失败,尝试其他方法提取数据");
}
}
if (this._options.textDataField) {
const textData = this._extractTextDataFromLine(content);
if (textData) {
this._processTextData(textData);
}
}
} catch (error) {
this._debugger.error("处理数据行失败:", error);
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 从数据行中提取音频数据
* @param line 数据行
* @returns 提取的音频数据
*/
_extractAudioDataFromLine(line) {
try {
try {
const jsonData = JSON.parse(line);
const audioData = this._extractAudioData(jsonData);
if (audioData) {
return audioData;
}
} catch (e) {
}
const fieldPath = this._options.audioDataField.split(".");
const lastField = fieldPath[fieldPath.length - 1];
const patterns = [
// 标准JSON格式
new RegExp(`["']${lastField}["']\\s*:\\s*(["']([^"']*)["'])`, "i"),
// Python风格
new RegExp(`["']${lastField}["']\\s*:\\s*b["']([^"']*)["']`, "i"),
// 无引号键
new RegExp(`${lastField}\\s*:\\s*(["']([^"']*)["'])`, "i"),
// 无引号键+Python风格
new RegExp(`${lastField}\\s*:\\s*b["']([^"']*)["']`, "i"),
// 直接内嵌的Base64字符串匹配 (长字符串可能是音频)
new RegExp("([A-Za-z0-9+/]{100,}={0,2})", "i"),
// 查找包含Base64头部的字符串
new RegExp("data:audio/[^;]+;base64,([A-Za-z0-9+/]+={0,2})", "i"),
// 完整的audio对象模式
new RegExp(`audio\\s*:\\s*{\\s*data\\s*:\\s*["'](.*?)["']\\s*,?\\s*}`, "i"),
// 双层嵌套的音频数据
new RegExp(`audio\\s*:\\s*{\\s*[^}]*\\s*data\\s*:\\s*["'](.*?)["']\\s*[^}]*\\s*}`, "i")
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match && (match[1] || match[2])) {
const data = match[2] || match[1];
if (/^[A-Za-z0-9+/=]+$/.test(data)) {
this._debugger.log(`通过正则表达式匹配找到可能的音频数据: ${pattern.toString()}`);
return data;
}
}
}
if (line.length > 100) {
const base64Matches = line.match(/["']([A-Za-z0-9+/]{100,}={0,2})["']/g);
if (base64Matches && base64Matches.length > 0) {
for (const match of base64Matches) {
const cleanMatch = match.replace(/^["']|["']$/g, "");
if (/^[A-Za-z0-9+/=]+$/.test(cleanMatch)) {
this._debugger.log(`找到可能的Base64音频数据,长度: ${cleanMatch.length}`);
return cleanMatch;
}
}
}
}
return null;
} catch (error) {
this._debugger.error("提取音频数据出错:", error);
return null;
}
}
/**
* 从对象中提取音频数据
* @param data JSON对象
* @returns 提取的音频数据字符串
*/
_extractAudioData(data) {
if (!data) return null;
const fieldPath = this._options.audioDataField.split(".");
let result = data;
for (const field of fieldPath) {
if (result && typeof result === "object" && field in result) {
result = result[field];
} else {
return this._recursiveExtractAudioData(data);
}
}
if (result && typeof result !== "string") {
if (typeof result === "object" && result.data && typeof result.data === "string") {
return result.data;
}
if (Array.isArray(result)) {
try {
const uint8Array = new Uint8Array(result);
const base64String = btoa(String.fromCharCode.apply(null, Array.from(uint8Array)));
return base64String;
} catch (e) {
this._debugger.error("无法将数组转换为Base64字符串:", e);
}
}
return this._recursiveExtractAudioData(result);
}
return typeof result === "string" ? result : null;
}
/**
* 递归搜索对象中的音频数据
* @param data 要搜索的对象
* @returns 找到的音频数据字符串
*/
_recursiveExtractAudioData(data) {
if (!data || typeof data !== "object") return null;
const audioFieldNames = ["audio", "data", "content", "value", "base64", "sound", "voice"];
for (const field of audioFieldNames) {
if (field in data && typeof data[field] === "string") {
const value = data[field];
if (value.length > 50 && /^[A-Za-z0-9+/=]+$/.test(value)) {
this._debugger.log(`在对象中找到可能的音频字段: ${field}`);
return value;
}
}
}
for (const key in data) {
if (typeof data[key] === "object" && data[key] !== null) {
if (key === "audio" || key === "sound" || key === "voice") {
const audioObj = data[key];
if (typeof audioObj === "object") {
for (const subKey in audioObj) {
if (typeof audioObj[subKey] === "string") {
const value = audioObj[subKey];
if (value.length > 50 && /^[A-Za-z0-9+/=]+$/.test(value)) {
this._debugger.log(`在嵌套对象${key}.${subKey}中找到可能的音频数据`);
return value;
}
}
}
}
}
const result = this._recursiveExtractAudioData(data[key]);
if (result) return result;
}
}
return null;
}
/**
* 从数据行中提取文本数据
* @param line 数据行
* @returns 提取的文本数据
*/
_extractTextDataFromLine(line) {
try {
const jsonData = JSON.parse(line);
return this._extractTextData(jsonData);
} catch (e) {
}
const fieldPath = this._options.textDataField.split(".");
const lastField = fieldPath[fieldPath.length - 1];
const patterns = [
// 标准JSON格式
new RegExp(`["']${lastField}["']\\s*:\\s*["']([^"']*)["']`, "i"),
// 无引号键
new RegExp(`${lastField}\\s*:\\s*["']([^"']*)["']`, "i")
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match && match[1]) {
return match[1];
}
}
return null;
}
/**
* 从对象中提取文本数据
* @param data JSON对象
* @returns 提取的文本数据字符串
*/
_extractTextData(data) {
if (!data || !this._options.textDataField) return null;
const fieldPath = this._options.textDataField.split(".");
let result = data;
for (const field of fieldPath) {
if (result && typeof result === "object" && field in result) {
result = result[field];
} else {
return null;
}
}
return typeof result === "string" ? result : null;
}
/**
* 处理文本数据
* @param textData 文本数据
*/
_processTextData(textData) {
try {
if (this._callbacks.onTextData) {
this._callbacks.onTextData(textData);
}
} catch (error) {
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 处理音频数据
* @param base64Data Base64编码的音频数据
*/
_processAudioData(base64Data) {
try {
this._debugger.log(`处理音频数据: 长度=${base64Data.length}`);
const binaryString = atob(base64Data.replace(/^data:[^;]+;base64,/, ""));
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: this._options.mimeType });
this._debugger.log(`创建音频Blob: ${blob.size}字节`);
this._audioChunks.push(blob);
if (this._options.playbackDelayChunks > 0) {
this._accumulatedChunks.push(blob);
this._playbackDelayCount++;
this._debugger.log(`音频块已缓存: ${this._playbackDelayCount}/${this._options.playbackDelayChunks}`);
if (this._playbackDelayCount >= this._options.playbackDelayChunks) {
this._debugger.log("已达到延迟播放阈值,准备播放");
if (this._options.autoPlay) {
const combinedBlob = new Blob(this._accumulatedChunks, { type: this._options.mimeType });
this._debugger.log(`播放累积的音频块: ${combinedBlob.size}字节`);
this._playAudio(combinedBlob);
this._accumulatedChunks = [];
this._playbackDelayCount = 0;
}
}
} else if (this._options.autoPlay) {
if (this._isFirstChunk && this._options.playFirstChunkImmediately) {
this._debugger.log("立即播放第一个音频块");
this._pendingFirstChunk = blob;
this._playAudio(blob);
this._isFirstChunk = false;
} else {
this._debugger.log(`添加音频块到播放队列: ${blob.size}字节`);
this._addToPlayQueue(blob);
}
}
if (this._callbacks.onAudioData) {
this._debugger.log(`触发 onAudioData 回调: ${blob.size}字节`);
this._callbacks.onAudioData(blob);
} else {
this._debugger.warn("警告: 未提供 onAudioData 回调");
}
} catch (error) {
this._debugger.error("处理音频数据时出错:", error);
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 确保音频队列正在被处理
* 该方法在收到新的音频数据块时调用,确保音频开始播放
*/
_ensureQueueIsProcessing() {
if (this._audioQueue.length > 0 && !this._isPlayingQueue && !this._isProcessingQueue && this._shouldContinueQueue && this._isPlaying) {
this._debugger.log("确保音频队列开始处理");
setTimeout(() => this._processAudioQueue(), 10);
}
}
/**
* 处理音频队列
* 只有在没有正在播放的音频时,才会播放下一个
*/
_processAudioQueue() {
this._debugger.log(`处理队列状态: 队列长度=${this._audioQueue.length}, 是否处理中=${this._isProcessingQueue}, 是否正在播放=${this._isPlayingQueue}, 是否应继续=${this._shouldContinueQueue}, 是否允许播放=${this._isPlaying}`);
if (this._isProcessingQueue || this._audioQueue.length === 0 || !this._isPlaying || !this._shouldContinueQueue) {
this._debugger.log("不满足处理队列条件,退出处理");
return;
}
this._cleanupAudioQueue();
if (this._audioQueue.length === 0) {
this._debugger.log("队列清理后为空,退出处理");
return;
}
this._isProcessingQueue = true;
if (this._currentAudio && !this._currentAudio.paused && !this._currentAudio.ended) {
this._debugger.log("有音频正在播放,等待完成后再处理队列");
this._isProcessingQueue = false;
if (!this._isPlayingQueue) {
setTimeout(() => {
if (this._audioQueue.length > 0 && !this._isPlayingQueue && !this._isProcessingQueue && this._shouldContinueQueue && this._isPlaying) {
this._debugger.log("播放完成回调没有触发,重新开始处理队列");
this._processAudioQueue();
}
}, 500);
}
return;
}
this._debugger.log("开始播放队列中的第一个音频");
if (this._playLock) {
this._debugger.warn("检测到播放锁仍然活跃,强制重置");
this._playLock = false;
}
this._playNextInQueue();
}
/**
* 清理音频队列,去除重复或相似的音频块
*/
_cleanupAudioQueue() {
if (this._audioQueue.length <= 1) return;
this._debugger.log(`清理音频队列前: ${this._audioQueue.length}个项目`);
const processedSizes = [];
const newQueue = [];
for (const blob of this._audioQueue) {
const hasSimilarSize = processedSizes.some(
(size) => Math.abs(size - blob.size) < 10
);
if (!hasSimilarSize) {
newQueue.push(blob);
processedSizes.push(blob.size);
} else {
this._debugger.log(`从队列中移除相似大小的Blob: ${blob.size}字节`);
}
}
this._audioQueue = newQueue;
this._debugger.log(`清理音频队列后: ${this._audioQueue.length}个项目`);
}
/**
* 播放队列中的下一个音频
*/
_playNextInQueue() {
if (!this._isPlaying || this._audioQueue.length === 0 || !this._shouldContinueQueue) {
this._isPlayingQueue = false;
this._isProcessingQueue = false;
return;
}
if (this._playLock) {
this._isProcessingQueue = false;
setTimeout(() => this._processAudioQueue(), 100);
return;
}
this._isPlayingQueue = true;
let blob;
if (this._options.playInSequentialOrder) {
blob = this._audioQueue.shift();
} else {
blob = this._audioQueue.shift();
}
this._debugger.log(`播放队列: 正在播放队列中的音频,剩余队列长度: ${this._audioQueue.length}`);
this._playAudioFromQueue(blob);
}
/**
* 从队列中播放音频
* @param blob 音频Blob
*/
_playAudioFromQueue(blob) {
if (!this._shouldContinueQueue || !this._isPlaying) {
this._isPlayingQueue = false;
this._isProcessingQueue = false;
return;
}
this._playLock = true;
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
this._currentAudio = audio;
let retryCount = 0;
const maxRetries = 2;
const handleCanPlayThrough = () => {
audio.removeEventListener("canplaythrough", handleCanPlayThrough);
if (this._shouldContinueQueue && this._isPlaying) {
this._debugger.log("开始播放音频");
audio.play().catch((error) => {
this._debugger.error("播放音频失败:", error);
if (retryCount < maxRetries) {
retryCount++;
this._debugger.log(`尝试重新播放音频, 第${retryCount}次重试`);
setTimeout(() => audio.play().catch(() => handlePlayComplete()), 300);
} else {
handlePlayComplete();
}
});
} else {
handlePlayComplete();
}
};
const handleError = (event) => {
this._debugger.error("音频加载或播放错误:", event);
handlePlayComplete();
};
const handlePlayComplete = () => {
this._debugger.log("音频播放完成");
if (this._currentAudio === audio) {
this._currentAudio = null;
}
URL.revokeObjectURL(url);
audio.removeEventListener("canplaythrough", handleCanPlayThrough);
audio.removeEventListener("error", handleError);
audio.removeEventListener("ended", handleEnded);
this._playLock = false;
this._isProcessingQueue = false;
if (this._audioQueue.length > 0 && this._shouldContinueQueue && this._isPlaying) {
this._debugger.log(`准备播放队列中的下一个音频,剩余队列长度: ${this._audioQueue.length}`);
setTimeout(() => {
this._playNextInQueue();
}, 100);
} else {
this._isPlayingQueue = false;
setTimeout(() => {
if (this._audioQueue.length > 0 && !this._isPlayingQueue && !this._isProcessingQueue && this._shouldContinueQueue && this._isPlaying) {
this._debugger.log("检测到队列中有未播放的音频,重新开始播放");
this._processAudioQueue();
}
}, 200);
}
};
const handleEnded = () => {
this._debugger.log("音频播放到末尾");
handlePlayComplete();
};
const loadTimeoutId = setTimeout(() => {
this._debugger.warn("音频加载超时,强制进入下一个");
if (this._currentAudio === audio) {
handlePlayComplete();
}
}, 5e3);
audio.addEventListener("canplaythrough", () => {
clearTimeout(loadTimeoutId);
handleCanPlayThrough();
});
audio.addEventListener("error", handleError);
audio.addEventListener("ended", handleEnded);
audio.load();
}
/**
* 将音频块添加到播放队列
* @param blob 音频Blob
*/
_addToPlayQueue(blob) {
const hasSimilarBlob = this._audioQueue.some(
(queuedBlob) => Math.abs(queuedBlob.size - blob.size) < 10
// 允许10字节的误差
);
if (hasSimilarBlob) {
this._debugger.log(`跳过相似大小的音频块添加: ${blob.size}字节`);
return;
}
if (this._options.playInSequentialOrder) {
this._audioQueue.push(blob);
} else {
this._audioQueue.unshift(blob);
}
this._debugger.log(`音频队列: 添加新音频到队列,当前队列长度: ${this._audioQueue.length}`);
if (!this._isProcessingQueue && !this._isPlayingQueue) {
if (this._currentAudio && !this._currentAudio.paused && !this._currentAudio.ended) {
this._debugger.log("有音频正在播放,等待它完成后再处理队列");
const audio = this._currentAudio;
const originalOnEnded = audio.onended;
audio.onended = (event) => {
if (originalOnEnded && typeof originalOnEnded === "function") {
originalOnEnded.call(audio, event);
}
setTimeout(() => {
if (this._audioQueue.length > 0 && !this._isPlayingQueue && !this._isProcessingQueue) {
this._debugger.log("音频结束后处理队列");
this._processAudioQueue();
}
}, 10);
};
return;
}
this._debugger.log("开始处理队列");
setTimeout(() => this._processAudioQueue(), 10);
} else {
this._debugger.log(`队列处理状态: isProcessingQueue=${this._isProcessingQueue}, isPlayingQueue=${this._isPlayingQueue}`);
}
}
/**
* 开始播放
*/
play() {
this._isPlaying = true;
if (this._pendingFirstChunk) {
this._audioQueue.push(this._pendingFirstChunk);
this._pendingFirstChunk = null;
if (!this._isPlayingQueue) {
this._playNextInQueue();
}
}
}
/**
* 停止播放
*/
stop() {
this.stopPlayback();
this.stopProcessing();
}
/**
* 开始处理
*/
startProcessing() {
this._isProcessing = true;
}
/**
* 停止处理
* 这将停止接收和处理新数据
*/
stopProcessing() {
this._isProcessing = false;
}
/**
* 获取播放状态
*/
get isPlaying() {
return this._isPlaying;
}
/**
* 获取处理状态
*/
get isProcessing() {
return this._isProcessing;
}
/**
* 获取所有处理过的音频块
* @returns 所有音频块的数组
*/
get audioChunks() {
return [...this._audioChunks];
}
/**
* 获取合并后的完整音频
* @returns 合并后的音频Blob
*/
get completeAudio() {
if (this._audioChunks.length === 0) {
return new Blob([], { type: this._options.mimeType });
}
return new Blob(this._audioChunks, { type: this._options.mimeType });
}
/**
* 清空已处理的音频数据
*/
clearAudio() {
this._audioChunks = [];
}
/**
* 播放音频
* @param blob 音频Blob
* @deprecated 使用_addToPlayQueue代替
*/
_playAudio(blob) {
this._addToPlayQueue(blob);
}
/**
* 处理原始音频数据
* @param data 原始数据
*/
_processRawAudio(data) {
var _a;
this._debugger.log(`处理原始音频数据: 类型=${typeof data}`);
try {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
this._debugger.log(`处理二进制数据: 大小=${data.byteLength || ((_a = data.buffer) == null ? void 0 : _a.byteLength) || "unknown"}字节`);
const blob = new Blob([data], { type: this._options.mimeType });
this._debugger.log(`从二进制数据创建音频Blob: ${blob.size}字节`);
this._audioChunks.push(blob);
if (this._options.autoPlay) {
this._addToPlayQueue(blob);
this._ensureQueueIsProcessing();
}
if (this._callbacks.onAudioData) {
this._debugger.log(`触发原始二进制数据 onAudioData 回调: ${blob.size}字节`);
this._callbacks.onAudioData(blob);
}
return;
}
if (data instanceof Blob) {
this._debugger.log(`处理Blob数据: 大小=${data.size}字节`);
this._audioChunks.push(data);
if (this._options.autoPlay) {
this._addToPlayQueue(data);
this._ensureQueueIsProcessing();
}
if (this._callbacks.onAudioData) {
this._debugger.log(`触发Blob onAudioData 回调: ${data.size}字节`);
this._callbacks.onAudioData(data);
}
return;
}
if (Array.isArray(data) || data instanceof Uint8Array) {
this._debugger.log(`处理数组数据: 长度=${data.length}`);
const blob = new Blob(
// 确保数据是有效的 BlobPart 类型
Array.isArray(data) ? [new Uint8Array(data)] : [data],
{ type: this._options.mimeType }
);
this._debugger.log(`从数组创建音频Blob: ${blob.size}字节`);
this._audioChunks.push(blob);
if (this._options.autoPlay) {
this._addToPlayQueue(blob);
this._ensureQueueIsProcessing();
}
if (this._callbacks.onAudioData) {
this._debugger.log(`触发数组 onAudioData 回调: ${blob.size}字节`);
this._callbacks.onAudioData(blob);
}
return;
}
if (typeof data === "string") {
this._debugger.log(`处理字符串数据: 长度=${data.length}`);
const isBase64 = /^[A-Za-z0-9+/=]+$/.test(data.trim());
if (isBase64) {
this._debugger.log("检测到可能是Base64字符串,尝试解码");
this._processAudioData(data);
return;
} else {
this._debugger.log("将字符串数据作为原始文本处理");
const blob = new Blob([data], { type: "text/plain" });
this._audioChunks.push(blob);
if (this._callbacks.onTextData) {
this._debugger.log("触发 onTextData 回调");
this._callbacks.onTextData(data);
} else if (this._callbacks.onAudioData) {
this._debugger.log(`触发文本数据的 onAudioData 回调: ${blob.size}字节`);
this._callbacks.onAudioData(blob);
}
return;
}
}
if (typeof data === "object" && data !== null) {
this._debugger.log("处理对象数据:", Object.keys(data));
let processedAudio = false;
const commonAudioFields = ["audio", "sound", "voice", "data", "content", "audioData", "soundData"];
for (const field of commonAudioFields) {
if (field in data && typeof data[field] === "string") {
const potentialAudio = data[field];
if (/^[A-Za-z0-9+/=]+$/.test(potentialAudio)) {
this._debugger.log(`从对象中提取到可能的音频数据,字段: ${field}`);
this._processAudioData(potentialAudio);
processedAudio = true;
break;
}
} else if (field in data && (data[field] instanceof ArrayBuffer || ArrayBuffer.isView(data[field]) || data[field] instanceof Blob)) {
this._debugger.log(`从对象中提取到二进制音频数据,字段: ${field}`);
this._processRawAudio(data[field]);
processedAudio = true;
break;
}
}
if (!processedAudio) {
this._debugger.log("未找到可识别的音频数据,将对象转为JSON");
const jsonStr = JSON.stringify(data);
if (this._callbacks.onTextData) {
this._debugger.log("触发JSON对象的 onTextData 回调");
this._callbacks.onTextData(jsonStr);
}
const blob = new Blob([jsonStr], { type: "application/json" });
if (this._callbacks.onAudioData) {
this._debugger.log(`触发JSON对象的 onAudioData 回调: ${blob.size}字节`);
this._callbacks.onAudioData(blob);
}
}
return;
}
this._debugger.log(`无法处理未知类型的数据: ${typeof data}`);
} catch (error) {
this._debugger.error("处理原始音频数据时出错:", error);
if (this._callbacks.onError) {
this._callbacks.onError(error instanceof Error ? error : new Error(String(error)));
}
}
}
/**
* 停止播放当前音频
* 仅停止播放,但继续处理数据
*/
stopPlayback() {
this._isPlaying = false;
this._shouldContinueQueue = false;
if (this._currentAudio) {
try {
this._currentAudio.pause();
this._currentAudio.currentTime = 0;
} catch (e) {
}
this._currentAudio = null;
}
this._audioQueue = [];
this._pendingFirstChunk = null;
this._isPlayingQueue = false;
this._isProcessingQueue = false;
this._playLock = false;
}
/**
* 恢复播放
*/
resumePlayback() {
this._isPlaying = true;
this._shouldContinueQueue = true;
this._cleanupAudioQueue();
if (this._pendingFirstChunk) {
const hasSimilarBlob = this._audioQueue.some(
(queuedBlob) => Math.abs(queuedBlob.size - this._pendingFirstChunk.size) < 10
);
if (!hasSimilarBlob) {
this._audioQueue.push(this._pendingFirstChunk);
} else {
this._debugger.log("跳过添加相似大小的首个待处理音频");
}
this._pendingFirstChunk = null;
}
if (this._audioQueue.length > 0 && !this._isProcessingQueue && !this._isPlayingQueue) {
this._debugger.log("恢复播放: 开始处理音频队列");
this._processAudioQueue();
} else {
this._debugger.log(`恢复播放: 队列状态 - 长度=${this._audioQueue.length}, 处理中=${this._isProcessingQueue}, 播放中=${this._isPlayingQueue}`);
}
}
/**
* 将音频添加到播放队列
* 用户可以在onAudioData回调中使用此方法将音频添加到无缝播放队列
* @param blob 音频Blob对象
*/
addAudioToQueue(blob) {
if (!this._isPlaying) return;
this._addToPlayQueue(blob);
}
/**
* 立即播放音频Blob
* 注意:这会打断当前的播放队列,可能导致不连贯的体验
* 建议使用addAudioToQueue实现无缝播放
* @param blob 音频Blob对象
*/
playAudioBlob(blob) {
if (!blob) return;
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
if (this._currentAudio) {
this._currentAudio.pause();
}
this._currentAudio = audio;
audio.onended = () => {
URL.revokeObjectURL(url);
if (this._currentAudio === audio) {
this._currentAudio = null;
}
};
audio.onerror = () => {
URL.revokeObjectURL(url);
if (this._currentAudio === audio) {
this._currentAudio = null;
}
};
audio.load();
audio.play().catch((error) => {
this._debugger.error("播放音频失败:", error);
URL.revokeObjectURL(url);
if (this._currentAudio === audio) {
this._currentAudio = null;
}
});
}
/**
* 创建一个已准备好直接播放的音频元素
* 用户可以使用此方法获取准备好的音频元素,自行控制播放时机
* @param blob 音频Blob对象
* @returns 准备好的Audio元素
*/
createAudioElement(blob) {
if (!blob) throw new Error("无效的音频数据");
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.onended = () => {
URL.revokeObjectURL(url);
};
audio.onerror = () => {
URL.revokeObjectURL(url);
};
audio.load();
return audio;
}
/**
* 获取当前缓冲的音频数量
* 用户可以基于此信息决定何时开始播放
*/
get bufferedChunksCount() {
return this._audioQueue.length;
}
/**
* 获取第一段音频数据
* 如果第一段音频还未收到,则返回null
*/
get firstAudioChunk() {
return this._pendingFirstChunk;
}
/**
* 完成处理,处理剩余的缓冲数据
* 用于确保处理一次性响应或流结束时的剩余数据
*/
finalizeProcessing() {
if (!this._isProcessing) return;
this._debugger.log("完成音频处理,处理剩余数据", {
缓冲区长度: this._buffer.length,
音频块数量: this._audioChunks.length,
队列长度: this._audioQueue.length,
播放状态: this._isPlaying,
自动播放: this._options.autoPlay,
有待处理首个音频: !!this._pendingFirstChunk
});
try {
if (this._buffer.trim()) {
this._processLine(this._buffer.trim());
this._buffer = "";
}
if (this._pendingFirstChunk && this._isPlaying && this._options.autoPlay) {
this._debugger.log("处理待播放的首个音频片段");
if (this._options.playFirstChunkImmediately) {
if (this._audioQueue.length === 0 && (!this._currentAudio || this._currentAudio.paused || this._currentAudio.ended)) {
this._debugger.log("立即播放首个待播放音频区块");
this.playAudioBlob(this._pendingFirstChunk);
} else {
this._addToPlayQueue(this._pendingFirstChunk);
}
} else {
this._addToPlayQueue(this._pendingFirstChunk);
}
this._pendingFirstChunk = null;
if (!this._isProcessingQueue && !this._isPlayingQueue) {
setTimeout(() => this._processAudioQueue(), 10);
}
}
if (this._options.playInSequentialOrder && this._audioQueue.length === 0 && this._audioChunks.length > 0 && this._isPlaying && this._options.autoPlay) {
this._debugger.log("没有队列中的音频,但有处理过的音频块,尝试播放最后一个");
if (!this._currentAudio || this._currentAudio.paused || this._currentAudio.ended) {
const lastChunk = this._audioChunks[this._audioChunks.length - 1];
this.playAudioBlob(lastChunk);
}
}
if (this._audioQueue.length > 0 && !this._isPlayingQueue && !this._isProcessingQueue && this._shouldContinueQueue && this._isPlaying) {
this._debugger.log("检测到队列中可能有未处理的音频,尝试重新启动队列处理");
setTimeout(() => this._processAudioQueue(), 100);
}
} catch (error) {
this._debugger.error("完成处理时出错:", error);
if (this._audioQueue.length > 0 && !this._isPlayingQueue && this._isPlaying && this._shouldContinueQueue) {
this._debugger.log("尽管有错误,仍尝试处理音频队列");
setTimeout(() => {
this._isProcessingQueue = false;
this._processAudioQueue();
}, 200);
}
}
}
/**
* 对字符串进行简单哈希处理
* @param str 要哈希的字符串
* @returns 哈希字符串
*/
_hashString(str) {
if (str.length > 1e3) {
const samples = [
str.substring(0, 100),
// 开头
str.substring(Math.floor(str.length / 2) - 50, Math.floor(str.length / 2) + 50),
// 中间
str.substring(str.length - 100)
// 结尾
];
str = samples.join("|") + str.length;
}
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return (hash >>> 0).toString(16);
}
}
function fetchAudioStream(options) {
const controller = new AbortController();
const signal = controller.signal;
const {
url,
method = "GET",
headers = {},
body,
timeout = 3e4,
directAudioResponse = false,
retries = 0,
retryDelay = 1e3,
processor,
onProgress,
onSuccess,
onError,
onComplete
} = options;
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
const fetchOptions = {
method,
headers: {
"Accept": directAudioResponse ? "audio/*" : "application/json, text/plain, */*",
...headers
},
signal,
credentials: "same-origin"
};
if (body) {
if (typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob)) {
fetchOptions.body = JSON.stringify(body);
const headerObj = fetchOptions.headers;
if (!headerObj["Content-Type"]) {
headerObj["Content-Type"] = "application/json";
}
} else {
fetchOptions.body = body;
}
}
let currentRetry = 0;
let lastReceivedDataSize = 0;
const performFetch = async () => {
var _a;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
if (directAudioResponse) {
const blob = await response.blob();
if (processor) {
const mockProgressEvent = {
event: { target: { response: blob } }
};
processor.onDownloadProgress(mockProgressEvent);
processor.finalizeProcessing();
}
if (onProgress) {
onProgress({ loaded: blob.size, total: blob.size, event: { target: { response: blob } } });
}
if (onSuccess) {
onSuccess(blob);
}
return;
}
const reader = (_a = response.body) == null ? void 0 : _a.getReader();
if (!reader) {
const text = await response.text();
if (processor) {
const mockProgressEvent = {
event: { target: { response: text } }
};
processor.onDownloadProgress(mockProgressEvent);
processor.finalizeProcessing();
}
if (onProgress) {
onProgress({ loaded: text.length, total: text.length });
}
if (onSuccess) {
onSuccess(text);
}
return;
}
while (true) {
const { done, value } = await reader.read();
if (done) {
if (processor) {
processor.finalizeProcessing();
}
break;
}
lastReceivedDataSize += value.length;
if (onProgress || processor) {
const progressEvent = {
loaded: lastReceivedDataSize,
total: void 0,
event: {
target: {
response: value
}
}
};
if (onProgress) {
onProgress(progressEvent);
}
if (processor) {
processor.onDownloadProgress(progressEvent);
}
}
}
if (onSuccess) {
onSuccess(response);
}
} catch (error) {
if (error.name === "AbortError") {
if (onError) {
onError(new Error("请求被取消"));
}
return;
}
if (currentRetry < retries) {
currentRetry++;
await new Promise((resolve) => setTimeout(resolve, retryDelay));
return performFetch();
}
if (onError) {
onError(error instanceof Error ? error : new Error(String(error)));
}
} finally {
clearTimeout(timeoutId);
if (onComplete) {
onComplete();
}
}
};
performFetch();
return {
abort: () => {
controller.abort();
}
};
}
function createAudioStream(fetchOptions, processorOptions = {}, callbacks = {}) {
const processor = createStreamAudioProcessor(processorOptions, callbacks);
const controller = fetchAudioStream({
...fetchOptions,
processor
});
return { processor, controller };
}
function createStreamAudioProcessor(options, callbacks) {
return new StreamAudioProcessor(options || {}, callbacks);
}
function createAudioRequest(defaultConfig = {}) {
const request = async function(config) {
const mergedConfig = { ...request.defaults, ...config };
const {
url,
method,
headers,
body,
data,
params,
responseType,
timeout,
directAudioResponse,
retries,
retryDelay,
processor,
processorOptions,
processorCallbacks,
onDownloadProgress,
onSuccess,
onError,
onComplete
} = mergedConfig;
if (!url) {
throw new Error("URL不能为空");
}
let fullUrl = url;
if (params) {
const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
fullUrl += (url.includes("?") ? "&" : "?") + queryString;
}
const requestBody = data || body;
let audioProcessor = processor;
if (!audioProcessor && (processorOptions || processorCallbacks)) {
audioProcessor = createStreamAudioProcessor(processorOptions || {}, processorCallbacks || {});
}
const requestMethod = method;
return new Promise((resolve, reject) => {
const controller = fetchAudioStream({
url: fullUrl,
method: requestMethod,
headers,
body: requestBody,
timeout,
directAudioResponse: directAudioResponse || (responseType === "blob" || responseType === "arraybuffer"),
retries,
retryDelay,
processor: audioProcessor,
onProgress: onDownloadProgress,
onSuccess: (response) => {
if (onSuccess) onSuccess(response);
const responseObject = {
data: response,
status: 200,
// fetch API成功时默认为200
statusText: "OK",
headers: {},
// 无法获取headers,因为response已经被处理
config: mergedConfig,
processor: audioProcessor
};
resolve(responseObject);
},
onError: (error) => {
if (onError) onError(error);
reject(error);
},
onComplete
});
config.cancelController = controller;
});
};
request.defaults = {
method: "GET",
headers: {},
timeout: 3e4,
...defaultConfig
};
request.get = function(url, config = {}) {
return request({ ...config, url, method: "GET" });
};
request.post = function(url, data, config = {}) {
return request({ ...config, url, method: "POST", data });
};
request.createProcessor = function(options, callbacks) {
return createStreamAudioProcessor(options, callbacks);
};
return request;
}
const msAudio = createAudioRequest({
headers: {
"Content-Type": "application/json"
}
});
function createAudioInstance(config = {}) {
return createAudioRequest(config);
}
async function handleStreamResponse(response, processor, options = {}) {
const controller = new AbortController();
const signal = controller.signal;
const {
directAudioResponse = false,
mimeType,
onProgress,
onComplete,
onError
} = options;
if (mimeType) {
processor._options.mimeType = mimeType;
}
try {
if (directAudioResponse) {
let blob;
if ("blob" in response && typeof response.blob === "function") {
blob = await response.blob();
} else {
throw new Error("无法从响应中获取Blob数据");
}
const mockProgressEvent = {
event: { target: { response: blob } }
};
processor.onDownloadProgress(mockProgressEvent);
if (onProgress) {
onProgress(blob.size);
}
if (onComplete) {
onComplete();
}
return { abort: () => controller.abort() };
}
const body = "body" in response ? response.body : null;
if (!body) {
if ("text" in response && typeof response.text === "function") {
const text = await response.text();
if (text) {
const mockProgressEvent = {
loaded: text.length,
total: text.length,
event: {
target: {
response: text
}
}
};
processor.onDownloadProgress(mockProgressEvent);
processor.finalizeProcessing();
if (onProgress) {
onProgress(text.length);
}
if (onComplete) {
onComplete();
}
return { abort: () => controller.abort() };
}
}
throw new Error("响应对象不包含可读流或文本数据");
}
const reader = body.getReader();
let loaded = 0;
(async () => {
try {
while (!signal.aborted) {
const { done, value } = await reader.read();
if (done)