From 71aa2bc3b630d2bfba6ff68d1e79d14a3663803a Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 18 Aug 2026 08:41:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20AudioWorklet=20?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E7=AD=96=E7=95=A5=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E7=A7=8D=20URL=20=E6=94=AF=E6=8C=81=E5=92=8C=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/voice/s2s-ws-client.js | 53 +++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) 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", {