diff --git a/src/voice/s2s-ws-client.js b/src/voice/s2s-ws-client.js index 5915f9f..cfb3faa 100644 --- a/src/voice/s2s-ws-client.js +++ b/src/voice/s2s-ws-client.js @@ -484,25 +484,60 @@ export class S2sWsRealtimeClient extends EventTarget { } } - // AudioWorklet 加载(WebView2/打包兼容): - // 源码已由 Vite `?raw` 内联进 bundle,运行时直接从内存字符串建 Blob → addModule。 - // 彻底摆脱运行时 fetch/网络/CORS/后端可达性依赖,任何 WebView2 都能加载。 - const addWorkletFromSrc = async (name, src) => { - const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" })); + // AudioWorklet 加载(WebView2/打包兼容)——多策略 + 详细日志: + // 源码已由 Vite `?raw` 内联进 bundle。不同 WebView2 版本对 + // blob: / data: / http: 三种 URL 的 addModule 支持不一致,逐一尝试, + // 并打印每次失败的真实 name/message,便于定位真正原因。 + // 策略1 内联源码 → Blob URL(无网络,多数环境最稳) + // 策略2 内联源码 → data: URL(部分 WebView2 拒绝 blob: 但接受 data:) + // 策略3 后端真实 HTTP(IP 可达 + CORS *,直连 addModule) + const attempts = []; + const tryModule = async (label, url) => { try { await ctx.audioWorklet.addModule(url); return true; - } finally { - URL.revokeObjectURL(url); + } catch (err) { + const detail = `${err?.name ?? ""}: ${err?.message ?? String(err)}`; + attempts.push(`${label} -> ${detail}`); + console.warn(`[ws] worklet addModule 失败(${label}):`, err); + return false; } }; + + const addWorkletFromSrc = async (name, src) => { + // 策略1:Blob URL + const blobUrl = URL.createObjectURL(new Blob([src], { type: "text/javascript" })); + let ok = await tryModule(`${name}@blob`, blobUrl); + URL.revokeObjectURL(blobUrl); + if (ok) return true; + + // 策略2:data: URL(base64,UTF-8 安全) + try { + const b64 = btoa(unescape(encodeURIComponent(src))); + ok = await tryModule(`${name}@data`, `data:text/javascript;base64,${b64}`); + if (ok) return true; + } catch (err) { + attempts.push(`${name}@data-encode -> ${err?.message ?? String(err)}`); + } + + // 策略3:后端 HTTP(IP 可达 + CORS *) + const httpBase = window.VOICE_WORKLETS_BASE; + if (httpBase) { + ok = await tryModule(`${name}@http`, `${httpBase}${name}.js`); + if (ok) return true; + } + return false; + }; + const workletOk = (await addWorkletFromSrc("mic-capture", micWorkletSrc)) && (await addWorkletFromSrc("audio-playback", playbackWorkletSrc)); if (workletOk) { - console.info("[ws] AudioWorklet 已通过内联源码 Blob URL 加载(WebView2 兼容模式)"); + console.info("[ws] AudioWorklet 加载成功(WebView2 兼容模式)"); } else { - throw new Error("AudioWorklet 模块加载失败(内联源码 Blob addModule 失败)"); + throw new Error( + `AudioWorklet 模块加载失败。尝试详情:${attempts.join(" | ") || "无"}`, + ); } const captureNode = new AudioWorkletNode(ctx, "mic-capture", {