@myned-ai/avatar-chat-widget 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1790,7 +1790,15 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1790
1790
  return _AudioContextManagerImpl._instance;
1791
1791
  }
1792
1792
  /**
1793
- * Get or create the shared AudioContext
1793
+ * Set the sample rate for when the AudioContext is eventually created.
1794
+ * Call this early (e.g., from server config) before any audio session starts.
1795
+ */
1796
+ setSampleRate(sampleRate) {
1797
+ this._sampleRate = sampleRate;
1798
+ }
1799
+ /**
1800
+ * Get the shared AudioContext. Creates lazily if needed.
1801
+ * Prefer ensureAudioReady() from user-gesture handlers to guarantee iOS unlock.
1794
1802
  * @param sampleRate Optional sample rate (only used on first creation)
1795
1803
  */
1796
1804
  getContext(sampleRate) {
@@ -1800,6 +1808,33 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1800
1808
  if (sampleRate) {
1801
1809
  this._sampleRate = sampleRate;
1802
1810
  }
1811
+ return this.createContext();
1812
+ }
1813
+ /**
1814
+ * Single idempotent entry point: create (if needed) + resume AudioContext.
1815
+ * Call this synchronously from any user-gesture handler (pointerdown, touchend,
1816
+ * click, keydown) to guarantee iOS audio unlock.
1817
+ */
1818
+ ensureAudioReady(source) {
1819
+ const tag = source ? `[${source}]` : "";
1820
+ if (!this._context) {
1821
+ this.createContext();
1822
+ }
1823
+ const ctx = this._context;
1824
+ if (!ctx) return;
1825
+ if (ctx.state === "running") return;
1826
+ log$c.debug(`${tag} Attempting AudioContext unlock, current state=${ctx.state}`);
1827
+ ctx.resume().then(() => {
1828
+ log$c.info(`${tag} AudioContext unlocked via ensureAudioReady`);
1829
+ this.removeResumeListeners();
1830
+ }).catch((error2) => {
1831
+ log$c.warn(`${tag} ensureAudioReady resume failed (will retry on next gesture):`, error2);
1832
+ });
1833
+ }
1834
+ /**
1835
+ * Internal: create the AudioContext and wire up fallback document listeners.
1836
+ */
1837
+ createContext() {
1803
1838
  try {
1804
1839
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1805
1840
  if (!AudioContextClass) {
@@ -1808,12 +1843,16 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1808
1843
  this._context = new AudioContextClass({
1809
1844
  sampleRate: this._sampleRate,
1810
1845
  latencyHint: "interactive"
1811
- // Optimize for real-time
1812
1846
  });
1813
1847
  log$c.info(`AudioContext created: sampleRate=${this._context.sampleRate}, state=${this._context.state}`);
1814
- this.setupResumeListener();
1848
+ if (this._context.state !== "running") {
1849
+ this.setupResumeListener();
1850
+ }
1815
1851
  this._context.onstatechange = () => {
1816
1852
  log$c.debug(`AudioContext state changed: ${this._context?.state}`);
1853
+ if (this._context?.state === "running") {
1854
+ this.removeResumeListeners();
1855
+ }
1817
1856
  };
1818
1857
  } catch (error2) {
1819
1858
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1822,26 +1861,29 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1822
1861
  return this._context;
1823
1862
  }
1824
1863
  /**
1825
- * Setup a single click listener to resume AudioContext (browser policy)
1826
- * This replaces multiple listeners scattered across services
1864
+ * Fallback document-level listeners for audio unlock.
1865
+ * These are a safety net primary unlock happens via ensureAudioReady()
1866
+ * called from widget gesture handlers.
1827
1867
  */
1828
1868
  setupResumeListener() {
1829
1869
  if (this._isResumeListenerAdded) {
1830
1870
  return;
1831
1871
  }
1832
- this._listenerEvents = ["click", "touchstart", "keydown"];
1833
- this._interactionHandler = async () => {
1834
- this.removeResumeListeners();
1835
- await this.resume();
1872
+ this._listenerEvents = ["pointerdown", "touchend", "keydown"];
1873
+ this._interactionHandler = () => {
1874
+ if (this._context && this._context.state !== "running") {
1875
+ this._context.resume().catch(() => {
1876
+ });
1877
+ }
1836
1878
  };
1837
1879
  this._listenerEvents.forEach((event) => {
1838
- document.addEventListener(event, this._interactionHandler, { once: true, passive: true });
1880
+ document.addEventListener(event, this._interactionHandler, { passive: true });
1839
1881
  });
1840
1882
  this._isResumeListenerAdded = true;
1841
- log$c.debug("Audio resume listeners added");
1883
+ log$c.debug("Audio resume fallback listeners added");
1842
1884
  }
1843
1885
  /**
1844
- * Remove resume listeners (called after first interaction or on cleanup)
1886
+ * Remove resume listeners (only after context is running, or on cleanup)
1845
1887
  */
1846
1888
  removeResumeListeners() {
1847
1889
  if (this._interactionHandler) {
@@ -1850,6 +1892,8 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1850
1892
  });
1851
1893
  this._interactionHandler = null;
1852
1894
  this._listenerEvents = [];
1895
+ this._isResumeListenerAdded = false;
1896
+ log$c.debug("Audio resume fallback listeners removed");
1853
1897
  }
1854
1898
  }
1855
1899
  /**
@@ -1870,6 +1914,7 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1870
1914
  try {
1871
1915
  await this._context.resume();
1872
1916
  log$c.info("AudioContext resumed successfully");
1917
+ this.removeResumeListeners();
1873
1918
  } catch (error2) {
1874
1919
  log$c.error("Failed to resume AudioContext:", error2);
1875
1920
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1998,7 +2043,7 @@ class AudioOutput {
1998
2043
  this.chunkDurationMs = 100;
1999
2044
  this.audioBuffer = new CircularBuffer(CONFIG.audio.output.maxBufferFrames);
2000
2045
  this.adaptiveBuffer = new AdaptiveBuffer(100, 50, 500);
2001
- AudioContextManager.getContext(this.sampleRate);
2046
+ AudioContextManager.setSampleRate(this.sampleRate);
2002
2047
  }
2003
2048
  /**
2004
2049
  * Get the shared AudioContext
@@ -2011,6 +2056,7 @@ class AudioOutput {
2011
2056
  */
2012
2057
  setDefaultSampleRate(sampleRate) {
2013
2058
  this.sampleRate = sampleRate;
2059
+ AudioContextManager.setSampleRate(sampleRate);
2014
2060
  log$b.info(`Output sample rate set to ${sampleRate}Hz`);
2015
2061
  }
2016
2062
  startSession(sessionId, sampleRate) {
@@ -2024,6 +2070,7 @@ class AudioOutput {
2024
2070
  this.isPlaying = false;
2025
2071
  this.nextPlayTime = 0;
2026
2072
  AudioContextManager.resume().catch(() => {
2073
+ log$b.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2027
2074
  });
2028
2075
  }
2029
2076
  addAudioChunk(data, timestamp) {
@@ -2535,7 +2582,7 @@ class SyncPlayback {
2535
2582
  this.floatBuffer = new Float32Array(this.maxSamplesPerFrame);
2536
2583
  this.visibilityHandler = this.handleVisibilityChange.bind(this);
2537
2584
  document.addEventListener("visibilitychange", this.visibilityHandler);
2538
- AudioContextManager.getContext(this.sampleRate);
2585
+ AudioContextManager.setSampleRate(this.sampleRate);
2539
2586
  }
2540
2587
  /**
2541
2588
  * Get the shared AudioContext
@@ -2572,6 +2619,7 @@ class SyncPlayback {
2572
2619
  */
2573
2620
  setDefaultSampleRate(sampleRate) {
2574
2621
  this.sampleRate = sampleRate;
2622
+ AudioContextManager.setSampleRate(sampleRate);
2575
2623
  log$8.info(`SyncPlayback sample rate set to ${sampleRate}Hz`);
2576
2624
  }
2577
2625
  startSession(sessionId, sampleRate) {
@@ -2585,6 +2633,7 @@ class SyncPlayback {
2585
2633
  this.sampleRate = sampleRate;
2586
2634
  }
2587
2635
  AudioContextManager.resume().catch(() => {
2636
+ log$8.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2588
2637
  });
2589
2638
  try {
2590
2639
  const ctx = this.audioContext;
@@ -5871,8 +5920,18 @@ class AvatarChatElement extends HTMLElement {
5871
5920
  * Setup UI event listeners
5872
5921
  */
5873
5922
  setupUIEvents() {
5923
+ const unlockAudio = (e) => {
5924
+ AudioContextManager.ensureAudioReady(`widget:${e.type}`);
5925
+ };
5926
+ this.shadow.addEventListener("pointerdown", unlockAudio, { capture: true, passive: true });
5927
+ this.shadow.addEventListener("touchend", unlockAudio, { capture: true, passive: true });
5928
+ this.shadow.addEventListener("click", unlockAudio, { capture: true, passive: true });
5929
+ this.shadow.addEventListener("focusin", unlockAudio, { capture: true });
5874
5930
  const minimizeBtn = this.shadow.getElementById("minimizeBtn");
5875
- minimizeBtn?.addEventListener("click", () => this.collapse());
5931
+ minimizeBtn?.addEventListener("click", () => {
5932
+ AudioContextManager.ensureAudioReady();
5933
+ this.collapse();
5934
+ });
5876
5935
  const chatInput = this.shadow.getElementById("chatInput");
5877
5936
  const inputControls = this.shadow.querySelector(".chat-input-controls");
5878
5937
  if (chatInput && inputControls) {
@@ -5884,6 +5943,7 @@ class AvatarChatElement extends HTMLElement {
5884
5943
  }
5885
5944
  });
5886
5945
  chatInput.addEventListener("focus", () => {
5946
+ AudioContextManager.ensureAudioReady();
5887
5947
  const root = this.shadow.querySelector(".widget-root");
5888
5948
  if (root) root.classList.add("input-focused");
5889
5949
  });
@@ -5914,6 +5974,7 @@ class AvatarChatElement extends HTMLElement {
5914
5974
  const handleChipClick = (e) => {
5915
5975
  const target = e.target;
5916
5976
  if (target.classList.contains("suggestion-chip")) {
5977
+ AudioContextManager.ensureAudioReady();
5917
5978
  this.markHasMessages();
5918
5979
  hideSuggestions();
5919
5980
  const text = target.textContent;
@@ -5927,11 +5988,13 @@ class AvatarChatElement extends HTMLElement {
5927
5988
  quickReplies?.addEventListener("click", handleChipClick);
5928
5989
  avatarSuggestions?.addEventListener("click", handleChipClick);
5929
5990
  micBtn?.addEventListener("click", () => {
5991
+ AudioContextManager.ensureAudioReady();
5930
5992
  this.markHasMessages();
5931
5993
  hideSuggestions();
5932
5994
  });
5933
5995
  chatInput.addEventListener("keydown", (e) => {
5934
5996
  if (e.key === "Enter") {
5997
+ AudioContextManager.ensureAudioReady();
5935
5998
  this.markHasMessages();
5936
5999
  hideSuggestions();
5937
6000
  setTimeout(() => {
@@ -6065,6 +6128,7 @@ class AvatarChatElement extends HTMLElement {
6065
6128
  return;
6066
6129
  }
6067
6130
  expandBtn.addEventListener("click", () => {
6131
+ AudioContextManager.ensureAudioReady();
6068
6132
  const isExpanded = widgetRoot.classList.toggle("expanded");
6069
6133
  expandBtn.setAttribute("aria-label", isExpanded ? "Collapse chat" : "Expand chat");
6070
6134
  expandBtn.setAttribute("title", isExpanded ? "Collapse" : "Expand");
@@ -6082,6 +6146,7 @@ class AvatarChatElement extends HTMLElement {
6082
6146
  return;
6083
6147
  }
6084
6148
  viewModeBtn.addEventListener("click", (e) => {
6149
+ AudioContextManager.ensureAudioReady();
6085
6150
  e.stopPropagation();
6086
6151
  if (this.drawerController) {
6087
6152
  const currentState = this.drawerController.getState();
@@ -6133,6 +6198,7 @@ class AvatarChatElement extends HTMLElement {
6133
6198
  */
6134
6199
  async expand() {
6135
6200
  if (!this._isCollapsed) return;
6201
+ AudioContextManager.ensureAudioReady("widget:expand");
6136
6202
  this._isCollapsed = false;
6137
6203
  this.classList.remove("collapsed");
6138
6204
  this.style.width = `${this.config.width}px`;