25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

207 satır
7.9 KiB

  1. (function installControlBridge() {
  2. const allowedOrigins = window.DH_ALLOWED_PARENT_ORIGINS || ["*"];
  3. const statusElement = document.getElementById("status");
  4. let audioContext = null;
  5. let currentSource = null;
  6. let currentRequestId = null;
  7. let ready = false;
  8. let volume = 1;
  9. function isAllowedOrigin(origin) {
  10. return allowedOrigins.includes("*") || allowedOrigins.includes(origin);
  11. }
  12. function send(target, targetOrigin, type, payload = {}) {
  13. if (!target || typeof target.postMessage !== "function") return;
  14. target.postMessage({ type, avatarId: window.DH_BOOTSTRAP.avatarId, ...payload }, targetOrigin);
  15. }
  16. function notifyParent(type, payload = {}) {
  17. if (window.parent === window) return;
  18. if (allowedOrigins.includes("*")) {
  19. send(window.parent, "*", type, payload);
  20. return;
  21. }
  22. for (const origin of allowedOrigins) {
  23. send(window.parent, origin, type, payload);
  24. }
  25. }
  26. function requireReady() {
  27. if (!ready || !window.Module || !window.DigitalHumanRuntime) {
  28. throw new Error("digital human runtime is not ready");
  29. }
  30. }
  31. function markReady() {
  32. ready = true;
  33. if (statusElement) statusElement.style.display = "none";
  34. notifyParent("digital-human.ready", {});
  35. }
  36. function clearWasmAudio() {
  37. if (window.Module && typeof window.Module._clearAudio === "function") {
  38. window.Module._clearAudio();
  39. }
  40. }
  41. function stopAudio(emitEvent = true) {
  42. if (currentSource) {
  43. currentSource.onended = null;
  44. try { currentSource.stop(); } catch (_) { /* already stopped */ }
  45. currentSource.disconnect();
  46. currentSource = null;
  47. }
  48. clearWasmAudio();
  49. if (emitEvent && currentRequestId) {
  50. notifyParent("digital-human.speaking-end", {
  51. requestId: currentRequestId,
  52. reason: "stopped",
  53. });
  54. }
  55. currentRequestId = null;
  56. }
  57. async function resolveAudioBuffer(message) {
  58. if (message.audioBuffer instanceof ArrayBuffer) {
  59. return message.audioBuffer;
  60. }
  61. if (ArrayBuffer.isView(message.audioBuffer)) {
  62. return message.audioBuffer.buffer.slice(
  63. message.audioBuffer.byteOffset,
  64. message.audioBuffer.byteOffset + message.audioBuffer.byteLength
  65. );
  66. }
  67. if (message.audioBase64) {
  68. const binary = atob(message.audioBase64);
  69. const bytes = new Uint8Array(binary.length);
  70. for (let index = 0; index < binary.length; index++) {
  71. bytes[index] = binary.charCodeAt(index);
  72. }
  73. return bytes.buffer;
  74. }
  75. if (message.audioUrl) {
  76. const response = await fetch(message.audioUrl);
  77. if (!response.ok) {
  78. throw new Error(`audio download failed with HTTP ${response.status}`);
  79. }
  80. return await response.arrayBuffer();
  81. }
  82. throw new Error("audioBuffer, audioBase64, or audioUrl is required");
  83. }
  84. async function playAudio(message) {
  85. requireReady();
  86. const compressedAudio = await resolveAudioBuffer(message);
  87. stopAudio(false);
  88. const wasmBytes = new Uint8Array(compressedAudio);
  89. const pointer = window.Module._malloc(wasmBytes.byteLength);
  90. try {
  91. window.Module.HEAPU8.set(wasmBytes, pointer);
  92. window.Module._setAudioBuffer(pointer, wasmBytes.byteLength);
  93. } finally {
  94. window.Module._free(pointer);
  95. }
  96. audioContext = audioContext || new (window.AudioContext || window.webkitAudioContext)();
  97. if (audioContext.state === "suspended") {
  98. await audioContext.resume();
  99. }
  100. const decoded = await audioContext.decodeAudioData(compressedAudio.slice(0));
  101. const gain = audioContext.createGain();
  102. gain.gain.value = volume;
  103. gain.connect(audioContext.destination);
  104. currentSource = audioContext.createBufferSource();
  105. currentSource.buffer = decoded;
  106. currentSource.connect(gain);
  107. currentRequestId = message.requestId || crypto.randomUUID();
  108. currentSource.onended = () => {
  109. const requestId = currentRequestId;
  110. currentSource = null;
  111. currentRequestId = null;
  112. // Natural playback completion must release the same WASM buffer as
  113. // an explicit stop. Otherwise the browser audio is silent while the
  114. // lip-sync runtime keeps consuming stale samples.
  115. clearWasmAudio();
  116. notifyParent("digital-human.speaking-end", {
  117. requestId,
  118. reason: "ended",
  119. });
  120. };
  121. currentSource.start(0);
  122. notifyParent("digital-human.speaking-start", {
  123. requestId: currentRequestId,
  124. duration: decoded.duration,
  125. });
  126. }
  127. async function handleCommand(event) {
  128. if (!isAllowedOrigin(event.origin)) return;
  129. const message = event.data || {};
  130. if (typeof message.type !== "string" || !message.type.startsWith("digital-human.")) {
  131. return;
  132. }
  133. try {
  134. switch (message.type) {
  135. case "digital-human.play-audio":
  136. await playAudio(message);
  137. break;
  138. case "digital-human.stop":
  139. stopAudio(true);
  140. break;
  141. case "digital-human.pause":
  142. requireReady();
  143. window.DigitalHumanRuntime.pause();
  144. if (audioContext) await audioContext.suspend();
  145. break;
  146. case "digital-human.resume":
  147. requireReady();
  148. window.DigitalHumanRuntime.resume();
  149. if (audioContext) await audioContext.resume();
  150. break;
  151. case "digital-human.set-background":
  152. requireReady();
  153. window.DigitalHumanRuntime.setBackground(
  154. message.backgroundUrl || null,
  155. message.backgroundType || "video"
  156. );
  157. break;
  158. case "digital-human.set-volume":
  159. volume = Math.max(0, Math.min(1, Number(message.volume)));
  160. break;
  161. case "digital-human.get-state":
  162. send(event.source, event.origin, "digital-human.state", {
  163. requestId: message.requestId,
  164. ready,
  165. speaking: Boolean(currentSource),
  166. paused: window.DigitalHumanRuntime?.isPaused() ?? false,
  167. });
  168. return;
  169. default:
  170. return;
  171. }
  172. send(event.source, event.origin, "digital-human.command-accepted", {
  173. requestId: message.requestId,
  174. command: message.type,
  175. });
  176. } catch (error) {
  177. send(event.source, event.origin, "digital-human.error", {
  178. requestId: message.requestId,
  179. message: error instanceof Error ? error.message : String(error),
  180. });
  181. }
  182. }
  183. window.addEventListener("message", handleCommand);
  184. window.addEventListener("digital-human-runtime-ready", markReady);
  185. // The runtime can finish between the preceding script tag and bridge
  186. // installation. Preserve the ready state so fast local bundles cannot lose
  187. // the only handshake event.
  188. if (window.DH_RUNTIME_READY) queueMicrotask(markReady);
  189. window.addEventListener("error", (event) => {
  190. if (statusElement) statusElement.textContent = "数字人加载失败";
  191. notifyParent("digital-human.error", { message: event.message || "viewer error" });
  192. });
  193. })();