@myned-ai/avatar-chat-widget 0.11.0 → 0.12.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,27 @@ 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() {
1819
+ if (!this._context) {
1820
+ this.createContext();
1821
+ }
1822
+ if (this._context && this._context.state !== "running") {
1823
+ this.resume().catch((error2) => {
1824
+ log$c.warn("ensureAudioReady resume failed (will retry on next gesture):", error2);
1825
+ });
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Internal: create the AudioContext and wire up fallback document listeners.
1830
+ */
1831
+ createContext() {
1803
1832
  try {
1804
1833
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1805
1834
  if (!AudioContextClass) {
@@ -1808,12 +1837,14 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1808
1837
  this._context = new AudioContextClass({
1809
1838
  sampleRate: this._sampleRate,
1810
1839
  latencyHint: "interactive"
1811
- // Optimize for real-time
1812
1840
  });
1813
1841
  log$c.info(`AudioContext created: sampleRate=${this._context.sampleRate}, state=${this._context.state}`);
1814
1842
  this.setupResumeListener();
1815
1843
  this._context.onstatechange = () => {
1816
1844
  log$c.debug(`AudioContext state changed: ${this._context?.state}`);
1845
+ if (this._context?.state === "running") {
1846
+ this.removeResumeListeners();
1847
+ }
1817
1848
  };
1818
1849
  } catch (error2) {
1819
1850
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1822,26 +1853,29 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1822
1853
  return this._context;
1823
1854
  }
1824
1855
  /**
1825
- * Setup a single click listener to resume AudioContext (browser policy)
1826
- * This replaces multiple listeners scattered across services
1856
+ * Fallback document-level listeners for audio unlock.
1857
+ * These are a safety net primary unlock happens via ensureAudioReady()
1858
+ * called from widget gesture handlers.
1827
1859
  */
1828
1860
  setupResumeListener() {
1829
1861
  if (this._isResumeListenerAdded) {
1830
1862
  return;
1831
1863
  }
1832
- this._listenerEvents = ["click", "touchstart", "keydown"];
1833
- this._interactionHandler = async () => {
1834
- this.removeResumeListeners();
1835
- await this.resume();
1864
+ this._listenerEvents = ["pointerdown", "touchend", "keydown"];
1865
+ this._interactionHandler = () => {
1866
+ if (this._context && this._context.state !== "running") {
1867
+ this._context.resume().catch(() => {
1868
+ });
1869
+ }
1836
1870
  };
1837
1871
  this._listenerEvents.forEach((event) => {
1838
- document.addEventListener(event, this._interactionHandler, { once: true, passive: true });
1872
+ document.addEventListener(event, this._interactionHandler, { passive: true });
1839
1873
  });
1840
1874
  this._isResumeListenerAdded = true;
1841
- log$c.debug("Audio resume listeners added");
1875
+ log$c.debug("Audio resume fallback listeners added");
1842
1876
  }
1843
1877
  /**
1844
- * Remove resume listeners (called after first interaction or on cleanup)
1878
+ * Remove resume listeners (only after context is running, or on cleanup)
1845
1879
  */
1846
1880
  removeResumeListeners() {
1847
1881
  if (this._interactionHandler) {
@@ -1850,6 +1884,8 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1850
1884
  });
1851
1885
  this._interactionHandler = null;
1852
1886
  this._listenerEvents = [];
1887
+ this._isResumeListenerAdded = false;
1888
+ log$c.debug("Audio resume fallback listeners removed");
1853
1889
  }
1854
1890
  }
1855
1891
  /**
@@ -1870,6 +1906,7 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1870
1906
  try {
1871
1907
  await this._context.resume();
1872
1908
  log$c.info("AudioContext resumed successfully");
1909
+ this.removeResumeListeners();
1873
1910
  } catch (error2) {
1874
1911
  log$c.error("Failed to resume AudioContext:", error2);
1875
1912
  errorBoundary.handleError(error2, "audio-context-manager");
@@ -1998,7 +2035,7 @@ class AudioOutput {
1998
2035
  this.chunkDurationMs = 100;
1999
2036
  this.audioBuffer = new CircularBuffer(CONFIG.audio.output.maxBufferFrames);
2000
2037
  this.adaptiveBuffer = new AdaptiveBuffer(100, 50, 500);
2001
- AudioContextManager.getContext(this.sampleRate);
2038
+ AudioContextManager.setSampleRate(this.sampleRate);
2002
2039
  }
2003
2040
  /**
2004
2041
  * Get the shared AudioContext
@@ -2011,6 +2048,7 @@ class AudioOutput {
2011
2048
  */
2012
2049
  setDefaultSampleRate(sampleRate) {
2013
2050
  this.sampleRate = sampleRate;
2051
+ AudioContextManager.setSampleRate(sampleRate);
2014
2052
  log$b.info(`Output sample rate set to ${sampleRate}Hz`);
2015
2053
  }
2016
2054
  startSession(sessionId, sampleRate) {
@@ -2024,6 +2062,7 @@ class AudioOutput {
2024
2062
  this.isPlaying = false;
2025
2063
  this.nextPlayTime = 0;
2026
2064
  AudioContextManager.resume().catch(() => {
2065
+ log$b.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2027
2066
  });
2028
2067
  }
2029
2068
  addAudioChunk(data, timestamp) {
@@ -2535,7 +2574,7 @@ class SyncPlayback {
2535
2574
  this.floatBuffer = new Float32Array(this.maxSamplesPerFrame);
2536
2575
  this.visibilityHandler = this.handleVisibilityChange.bind(this);
2537
2576
  document.addEventListener("visibilitychange", this.visibilityHandler);
2538
- AudioContextManager.getContext(this.sampleRate);
2577
+ AudioContextManager.setSampleRate(this.sampleRate);
2539
2578
  }
2540
2579
  /**
2541
2580
  * Get the shared AudioContext
@@ -2572,6 +2611,7 @@ class SyncPlayback {
2572
2611
  */
2573
2612
  setDefaultSampleRate(sampleRate) {
2574
2613
  this.sampleRate = sampleRate;
2614
+ AudioContextManager.setSampleRate(sampleRate);
2575
2615
  log$8.info(`SyncPlayback sample rate set to ${sampleRate}Hz`);
2576
2616
  }
2577
2617
  startSession(sessionId, sampleRate) {
@@ -2585,6 +2625,7 @@ class SyncPlayback {
2585
2625
  this.sampleRate = sampleRate;
2586
2626
  }
2587
2627
  AudioContextManager.resume().catch(() => {
2628
+ log$8.warn("AudioContext resume failed in startSession — will retry on next user gesture");
2588
2629
  });
2589
2630
  try {
2590
2631
  const ctx = this.audioContext;
@@ -5884,6 +5925,7 @@ class AvatarChatElement extends HTMLElement {
5884
5925
  }
5885
5926
  });
5886
5927
  chatInput.addEventListener("focus", () => {
5928
+ AudioContextManager.ensureAudioReady();
5887
5929
  const root = this.shadow.querySelector(".widget-root");
5888
5930
  if (root) root.classList.add("input-focused");
5889
5931
  });
@@ -5914,6 +5956,7 @@ class AvatarChatElement extends HTMLElement {
5914
5956
  const handleChipClick = (e) => {
5915
5957
  const target = e.target;
5916
5958
  if (target.classList.contains("suggestion-chip")) {
5959
+ AudioContextManager.ensureAudioReady();
5917
5960
  this.markHasMessages();
5918
5961
  hideSuggestions();
5919
5962
  const text = target.textContent;
@@ -5927,11 +5970,13 @@ class AvatarChatElement extends HTMLElement {
5927
5970
  quickReplies?.addEventListener("click", handleChipClick);
5928
5971
  avatarSuggestions?.addEventListener("click", handleChipClick);
5929
5972
  micBtn?.addEventListener("click", () => {
5973
+ AudioContextManager.ensureAudioReady();
5930
5974
  this.markHasMessages();
5931
5975
  hideSuggestions();
5932
5976
  });
5933
5977
  chatInput.addEventListener("keydown", (e) => {
5934
5978
  if (e.key === "Enter") {
5979
+ AudioContextManager.ensureAudioReady();
5935
5980
  this.markHasMessages();
5936
5981
  hideSuggestions();
5937
5982
  setTimeout(() => {
@@ -6133,6 +6178,7 @@ class AvatarChatElement extends HTMLElement {
6133
6178
  */
6134
6179
  async expand() {
6135
6180
  if (!this._isCollapsed) return;
6181
+ AudioContextManager.ensureAudioReady();
6136
6182
  this._isCollapsed = false;
6137
6183
  this.classList.remove("collapsed");
6138
6184
  this.style.width = `${this.config.width}px`;