UNPKG

mstf-kit

Version:

一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数

1,594 lines 76.1 kB
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)