@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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
1826
|
-
*
|
|
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 = ["
|
|
1833
|
-
this._interactionHandler =
|
|
1834
|
-
this.
|
|
1835
|
-
|
|
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, {
|
|
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 (
|
|
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.
|
|
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.
|
|
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", () =>
|
|
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`;
|