|
- (function installControlBridge() {
- const allowedOrigins = window.DH_ALLOWED_PARENT_ORIGINS || ["*"];
- const statusElement = document.getElementById("status");
- let audioContext = null;
- let currentSource = null;
- let currentRequestId = null;
- let ready = false;
- let volume = 1;
-
- function isAllowedOrigin(origin) {
- return allowedOrigins.includes("*") || allowedOrigins.includes(origin);
- }
-
- function send(target, targetOrigin, type, payload = {}) {
- if (!target || typeof target.postMessage !== "function") return;
- target.postMessage({ type, avatarId: window.DH_BOOTSTRAP.avatarId, ...payload }, targetOrigin);
- }
-
- function notifyParent(type, payload = {}) {
- if (window.parent === window) return;
- if (allowedOrigins.includes("*")) {
- send(window.parent, "*", type, payload);
- return;
- }
- for (const origin of allowedOrigins) {
- send(window.parent, origin, type, payload);
- }
- }
-
- function requireReady() {
- if (!ready || !window.Module || !window.DigitalHumanRuntime) {
- throw new Error("digital human runtime is not ready");
- }
- }
-
- function markReady() {
- ready = true;
- if (statusElement) statusElement.style.display = "none";
- notifyParent("digital-human.ready", {});
- }
-
- function clearWasmAudio() {
- if (window.Module && typeof window.Module._clearAudio === "function") {
- window.Module._clearAudio();
- }
- }
-
- function stopAudio(emitEvent = true) {
- if (currentSource) {
- currentSource.onended = null;
- try { currentSource.stop(); } catch (_) { /* already stopped */ }
- currentSource.disconnect();
- currentSource = null;
- }
- clearWasmAudio();
- if (emitEvent && currentRequestId) {
- notifyParent("digital-human.speaking-end", {
- requestId: currentRequestId,
- reason: "stopped",
- });
- }
- currentRequestId = null;
- }
-
- async function resolveAudioBuffer(message) {
- if (message.audioBuffer instanceof ArrayBuffer) {
- return message.audioBuffer;
- }
- if (ArrayBuffer.isView(message.audioBuffer)) {
- return message.audioBuffer.buffer.slice(
- message.audioBuffer.byteOffset,
- message.audioBuffer.byteOffset + message.audioBuffer.byteLength
- );
- }
- if (message.audioBase64) {
- const binary = atob(message.audioBase64);
- const bytes = new Uint8Array(binary.length);
- for (let index = 0; index < binary.length; index++) {
- bytes[index] = binary.charCodeAt(index);
- }
- return bytes.buffer;
- }
- if (message.audioUrl) {
- const response = await fetch(message.audioUrl);
- if (!response.ok) {
- throw new Error(`audio download failed with HTTP ${response.status}`);
- }
- return await response.arrayBuffer();
- }
- throw new Error("audioBuffer, audioBase64, or audioUrl is required");
- }
-
- async function playAudio(message) {
- requireReady();
- const compressedAudio = await resolveAudioBuffer(message);
- stopAudio(false);
-
- const wasmBytes = new Uint8Array(compressedAudio);
- const pointer = window.Module._malloc(wasmBytes.byteLength);
- try {
- window.Module.HEAPU8.set(wasmBytes, pointer);
- window.Module._setAudioBuffer(pointer, wasmBytes.byteLength);
- } finally {
- window.Module._free(pointer);
- }
-
- audioContext = audioContext || new (window.AudioContext || window.webkitAudioContext)();
- if (audioContext.state === "suspended") {
- await audioContext.resume();
- }
- const decoded = await audioContext.decodeAudioData(compressedAudio.slice(0));
- const gain = audioContext.createGain();
- gain.gain.value = volume;
- gain.connect(audioContext.destination);
- currentSource = audioContext.createBufferSource();
- currentSource.buffer = decoded;
- currentSource.connect(gain);
- currentRequestId = message.requestId || crypto.randomUUID();
- currentSource.onended = () => {
- const requestId = currentRequestId;
- currentSource = null;
- currentRequestId = null;
- // Natural playback completion must release the same WASM buffer as
- // an explicit stop. Otherwise the browser audio is silent while the
- // lip-sync runtime keeps consuming stale samples.
- clearWasmAudio();
- notifyParent("digital-human.speaking-end", {
- requestId,
- reason: "ended",
- });
- };
- currentSource.start(0);
- notifyParent("digital-human.speaking-start", {
- requestId: currentRequestId,
- duration: decoded.duration,
- });
- }
-
- async function handleCommand(event) {
- if (!isAllowedOrigin(event.origin)) return;
- const message = event.data || {};
- if (typeof message.type !== "string" || !message.type.startsWith("digital-human.")) {
- return;
- }
- try {
- switch (message.type) {
- case "digital-human.play-audio":
- await playAudio(message);
- break;
- case "digital-human.stop":
- stopAudio(true);
- break;
- case "digital-human.pause":
- requireReady();
- window.DigitalHumanRuntime.pause();
- if (audioContext) await audioContext.suspend();
- break;
- case "digital-human.resume":
- requireReady();
- window.DigitalHumanRuntime.resume();
- if (audioContext) await audioContext.resume();
- break;
- case "digital-human.set-background":
- requireReady();
- window.DigitalHumanRuntime.setBackground(
- message.backgroundUrl || null,
- message.backgroundType || "video"
- );
- break;
- case "digital-human.set-volume":
- volume = Math.max(0, Math.min(1, Number(message.volume)));
- break;
- case "digital-human.get-state":
- send(event.source, event.origin, "digital-human.state", {
- requestId: message.requestId,
- ready,
- speaking: Boolean(currentSource),
- paused: window.DigitalHumanRuntime?.isPaused() ?? false,
- });
- return;
- default:
- return;
- }
- send(event.source, event.origin, "digital-human.command-accepted", {
- requestId: message.requestId,
- command: message.type,
- });
- } catch (error) {
- send(event.source, event.origin, "digital-human.error", {
- requestId: message.requestId,
- message: error instanceof Error ? error.message : String(error),
- });
- }
- }
-
- window.addEventListener("message", handleCommand);
- window.addEventListener("digital-human-runtime-ready", markReady);
- // The runtime can finish between the preceding script tag and bridge
- // installation. Preserve the ready state so fast local bundles cannot lose
- // the only handshake event.
- if (window.DH_RUNTIME_READY) queueMicrotask(markReady);
- window.addEventListener("error", (event) => {
- if (statusElement) statusElement.textContent = "数字人加载失败";
- notifyParent("digital-human.error", { message: event.message || "viewer error" });
- });
- })();
|