@absolutejs/voice 0.0.22-beta.656 → 0.0.22-beta.658
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.
- package/dist/angular/index.js +167 -67
- package/dist/client/htmxBootstrap.js +167 -67
- package/dist/client/index.js +167 -67
- package/dist/core/types.d.ts +4 -0
- package/dist/embed/index.js +167 -67
- package/dist/embed/voice-widget.js +8 -8
- package/dist/index.js +52 -31
- package/dist/react/index.js +167 -67
- package/dist/svelte/index.js +167 -67
- package/dist/testing/index.js +168 -68
- package/dist/vue/index.js +167 -67
- package/package.json +1 -1
package/dist/angular/index.js
CHANGED
|
@@ -335,48 +335,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
335
335
|
let sourceNode = null;
|
|
336
336
|
let processorNode = null;
|
|
337
337
|
let mediaStream = null;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
338
|
+
let lifecycleGeneration = 0;
|
|
339
|
+
let startPromise = null;
|
|
340
|
+
const release = (resources) => {
|
|
341
|
+
resources.processorNode?.disconnect();
|
|
342
|
+
resources.sourceNode?.disconnect();
|
|
343
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
344
|
+
resources.audioContext?.close();
|
|
345
|
+
};
|
|
346
|
+
const start = () => {
|
|
347
|
+
if (mediaStream)
|
|
348
|
+
return Promise.resolve();
|
|
349
|
+
if (startPromise)
|
|
350
|
+
return startPromise;
|
|
351
|
+
const generation = ++lifecycleGeneration;
|
|
352
|
+
const operation = async () => {
|
|
353
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
354
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
355
|
+
}
|
|
356
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
357
|
+
if (!AudioContextCtor) {
|
|
358
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
359
|
+
}
|
|
360
|
+
let pendingAudioContext = null;
|
|
361
|
+
let pendingMediaStream = null;
|
|
362
|
+
let pendingProcessorNode = null;
|
|
363
|
+
let pendingSourceNode = null;
|
|
364
|
+
try {
|
|
365
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
366
|
+
audio: {
|
|
367
|
+
autoGainControl: true,
|
|
368
|
+
channelCount: options.channelCount ?? 1,
|
|
369
|
+
echoCancellation: true,
|
|
370
|
+
noiseSuppression: true
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
if (generation !== lifecycleGeneration) {
|
|
374
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
353
375
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
376
|
+
pendingAudioContext = new AudioContextCtor;
|
|
377
|
+
if (pendingAudioContext.state === "suspended") {
|
|
378
|
+
await pendingAudioContext.resume();
|
|
379
|
+
}
|
|
380
|
+
if (generation !== lifecycleGeneration) {
|
|
381
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
382
|
+
}
|
|
383
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
384
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
385
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
386
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
387
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
388
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
389
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
390
|
+
options.onAudio(pcm);
|
|
391
|
+
};
|
|
392
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
393
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
394
|
+
if (generation !== lifecycleGeneration) {
|
|
395
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
396
|
+
}
|
|
397
|
+
audioContext = pendingAudioContext;
|
|
398
|
+
mediaStream = pendingMediaStream;
|
|
399
|
+
processorNode = pendingProcessorNode;
|
|
400
|
+
sourceNode = pendingSourceNode;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
release({
|
|
403
|
+
audioContext: pendingAudioContext,
|
|
404
|
+
mediaStream: pendingMediaStream,
|
|
405
|
+
processorNode: pendingProcessorNode,
|
|
406
|
+
sourceNode: pendingSourceNode
|
|
407
|
+
});
|
|
408
|
+
throw error;
|
|
358
409
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
options.onAudio(pcm);
|
|
367
|
-
};
|
|
368
|
-
sourceNode.connect(processorNode);
|
|
369
|
-
processorNode.connect(audioContext.destination);
|
|
370
|
-
} catch (error) {
|
|
371
|
-
stop();
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
410
|
+
};
|
|
411
|
+
const pending = operation().finally(() => {
|
|
412
|
+
if (startPromise === pending)
|
|
413
|
+
startPromise = null;
|
|
414
|
+
});
|
|
415
|
+
startPromise = pending;
|
|
416
|
+
return pending;
|
|
374
417
|
};
|
|
375
418
|
const stop = () => {
|
|
376
|
-
|
|
377
|
-
sourceNode
|
|
378
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
379
|
-
audioContext?.close();
|
|
419
|
+
lifecycleGeneration += 1;
|
|
420
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
380
421
|
options.onLevel?.(0);
|
|
381
422
|
audioContext = null;
|
|
382
423
|
mediaStream = null;
|
|
@@ -588,6 +629,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
588
629
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
589
630
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
590
631
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
632
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
591
633
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
592
634
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
593
635
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -681,6 +723,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
681
723
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
682
724
|
let lifecycleGeneration = 0;
|
|
683
725
|
let disposed = false;
|
|
726
|
+
let reconnectPreparationAbort = null;
|
|
684
727
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
685
728
|
const state = {
|
|
686
729
|
isConnected: false,
|
|
@@ -768,12 +811,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
768
811
|
return;
|
|
769
812
|
}
|
|
770
813
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
814
|
+
if (options.prepareReconnect) {
|
|
815
|
+
const preparationAbort = new AbortController;
|
|
816
|
+
reconnectPreparationAbort = preparationAbort;
|
|
817
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
818
|
+
try {
|
|
819
|
+
await Promise.race([
|
|
820
|
+
options.prepareReconnect({
|
|
821
|
+
attempt: state.reconnectAttempts,
|
|
822
|
+
path,
|
|
823
|
+
scenarioId: state.scenarioId,
|
|
824
|
+
sessionId: state.sessionId,
|
|
825
|
+
signal: preparationAbort.signal
|
|
826
|
+
}),
|
|
827
|
+
new Promise((_, reject) => {
|
|
828
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
829
|
+
})
|
|
830
|
+
]);
|
|
831
|
+
} finally {
|
|
832
|
+
clearTimeout(timeout);
|
|
833
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
834
|
+
reconnectPreparationAbort = null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
777
838
|
} catch (error) {
|
|
778
839
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
779
840
|
return;
|
|
@@ -897,6 +958,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
897
958
|
state.ws = ws;
|
|
898
959
|
};
|
|
899
960
|
const sendSerialized = (value) => {
|
|
961
|
+
if (disposed)
|
|
962
|
+
return;
|
|
900
963
|
if (state.ws?.readyState === WS_OPEN) {
|
|
901
964
|
state.ws.send(value);
|
|
902
965
|
return;
|
|
@@ -951,6 +1014,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
951
1014
|
};
|
|
952
1015
|
const close = (reason = "client-close") => {
|
|
953
1016
|
disposed = true;
|
|
1017
|
+
reconnectPreparationAbort?.abort();
|
|
1018
|
+
reconnectPreparationAbort = null;
|
|
954
1019
|
lifecycleGeneration += 1;
|
|
955
1020
|
clearTimers();
|
|
956
1021
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -968,6 +1033,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
968
1033
|
};
|
|
969
1034
|
const disconnect = () => {
|
|
970
1035
|
disposed = true;
|
|
1036
|
+
reconnectPreparationAbort?.abort();
|
|
1037
|
+
reconnectPreparationAbort = null;
|
|
971
1038
|
lifecycleGeneration += 1;
|
|
972
1039
|
clearTimers();
|
|
973
1040
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -1219,7 +1286,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1219
1286
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1220
1287
|
}) : null;
|
|
1221
1288
|
const subscribers = new Set;
|
|
1289
|
+
let disposed = false;
|
|
1222
1290
|
const start = (input) => Promise.resolve().then(() => {
|
|
1291
|
+
if (disposed)
|
|
1292
|
+
return;
|
|
1223
1293
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
1224
1294
|
return;
|
|
1225
1295
|
}
|
|
@@ -1278,6 +1348,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1278
1348
|
return connection.callControl(message);
|
|
1279
1349
|
},
|
|
1280
1350
|
close(reason) {
|
|
1351
|
+
disposed = true;
|
|
1281
1352
|
unsubscribeConnection();
|
|
1282
1353
|
browserMediaReporter?.close();
|
|
1283
1354
|
connection.close(reason);
|
|
@@ -1285,6 +1356,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1285
1356
|
notify();
|
|
1286
1357
|
},
|
|
1287
1358
|
disconnect() {
|
|
1359
|
+
disposed = true;
|
|
1288
1360
|
unsubscribeConnection();
|
|
1289
1361
|
browserMediaReporter?.close();
|
|
1290
1362
|
connection.disconnect();
|
|
@@ -1688,6 +1760,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1688
1760
|
...options.connection
|
|
1689
1761
|
});
|
|
1690
1762
|
let capture = null;
|
|
1763
|
+
let captureGeneration = 0;
|
|
1764
|
+
let disposed = false;
|
|
1765
|
+
let startRecordingPromise = null;
|
|
1691
1766
|
let state = createInitialState2(stream);
|
|
1692
1767
|
const subscribers = new Set;
|
|
1693
1768
|
const notify = () => {
|
|
@@ -1744,6 +1819,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1744
1819
|
return capture;
|
|
1745
1820
|
};
|
|
1746
1821
|
const stopRecording = () => {
|
|
1822
|
+
captureGeneration += 1;
|
|
1747
1823
|
capture?.stop();
|
|
1748
1824
|
capture = null;
|
|
1749
1825
|
state = {
|
|
@@ -1753,39 +1829,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1753
1829
|
notify();
|
|
1754
1830
|
};
|
|
1755
1831
|
const startRecording = async () => {
|
|
1832
|
+
if (disposed) {
|
|
1833
|
+
throw new Error("Voice controller is closed.");
|
|
1834
|
+
}
|
|
1756
1835
|
if (state.isRecording) {
|
|
1757
1836
|
return;
|
|
1758
1837
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1838
|
+
if (startRecordingPromise)
|
|
1839
|
+
return startRecordingPromise;
|
|
1840
|
+
const generation = ++captureGeneration;
|
|
1841
|
+
const activeCapture = ensureCapture();
|
|
1842
|
+
const operation = (async () => {
|
|
1843
|
+
try {
|
|
1844
|
+
state = {
|
|
1845
|
+
...state,
|
|
1846
|
+
recordingError: null
|
|
1847
|
+
};
|
|
1848
|
+
notify();
|
|
1849
|
+
await activeCapture.start();
|
|
1850
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
1851
|
+
activeCapture.stop();
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
state = {
|
|
1855
|
+
...state,
|
|
1856
|
+
isRecording: true
|
|
1857
|
+
};
|
|
1858
|
+
notify();
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
activeCapture.stop();
|
|
1861
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
capture = null;
|
|
1865
|
+
state = {
|
|
1866
|
+
...state,
|
|
1867
|
+
isRecording: false,
|
|
1868
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
1869
|
+
};
|
|
1870
|
+
notify();
|
|
1871
|
+
throw error;
|
|
1872
|
+
}
|
|
1873
|
+
})();
|
|
1874
|
+
const pending = operation.finally(() => {
|
|
1875
|
+
if (startRecordingPromise === pending)
|
|
1876
|
+
startRecordingPromise = null;
|
|
1877
|
+
});
|
|
1878
|
+
startRecordingPromise = pending;
|
|
1879
|
+
return startRecordingPromise;
|
|
1782
1880
|
};
|
|
1783
1881
|
const close = (reason) => {
|
|
1882
|
+
disposed = true;
|
|
1784
1883
|
unsubscribeStream();
|
|
1785
1884
|
stopRecording();
|
|
1786
1885
|
stream.close(reason);
|
|
1787
1886
|
};
|
|
1788
1887
|
const disconnect = () => {
|
|
1888
|
+
disposed = true;
|
|
1789
1889
|
unsubscribeStream();
|
|
1790
1890
|
stopRecording();
|
|
1791
1891
|
stream.disconnect();
|
|
@@ -103,48 +103,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
103
103
|
let sourceNode = null;
|
|
104
104
|
let processorNode = null;
|
|
105
105
|
let mediaStream = null;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
let lifecycleGeneration = 0;
|
|
107
|
+
let startPromise = null;
|
|
108
|
+
const release = (resources) => {
|
|
109
|
+
resources.processorNode?.disconnect();
|
|
110
|
+
resources.sourceNode?.disconnect();
|
|
111
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
112
|
+
resources.audioContext?.close();
|
|
113
|
+
};
|
|
114
|
+
const start = () => {
|
|
115
|
+
if (mediaStream)
|
|
116
|
+
return Promise.resolve();
|
|
117
|
+
if (startPromise)
|
|
118
|
+
return startPromise;
|
|
119
|
+
const generation = ++lifecycleGeneration;
|
|
120
|
+
const operation = async () => {
|
|
121
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
122
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
123
|
+
}
|
|
124
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
125
|
+
if (!AudioContextCtor) {
|
|
126
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
127
|
+
}
|
|
128
|
+
let pendingAudioContext = null;
|
|
129
|
+
let pendingMediaStream = null;
|
|
130
|
+
let pendingProcessorNode = null;
|
|
131
|
+
let pendingSourceNode = null;
|
|
132
|
+
try {
|
|
133
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
134
|
+
audio: {
|
|
135
|
+
autoGainControl: true,
|
|
136
|
+
channelCount: options.channelCount ?? 1,
|
|
137
|
+
echoCancellation: true,
|
|
138
|
+
noiseSuppression: true
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (generation !== lifecycleGeneration) {
|
|
142
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
121
143
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
pendingAudioContext = new AudioContextCtor;
|
|
145
|
+
if (pendingAudioContext.state === "suspended") {
|
|
146
|
+
await pendingAudioContext.resume();
|
|
147
|
+
}
|
|
148
|
+
if (generation !== lifecycleGeneration) {
|
|
149
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
150
|
+
}
|
|
151
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
152
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
153
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
154
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
155
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
156
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
157
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
158
|
+
options.onAudio(pcm);
|
|
159
|
+
};
|
|
160
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
161
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
162
|
+
if (generation !== lifecycleGeneration) {
|
|
163
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
164
|
+
}
|
|
165
|
+
audioContext = pendingAudioContext;
|
|
166
|
+
mediaStream = pendingMediaStream;
|
|
167
|
+
processorNode = pendingProcessorNode;
|
|
168
|
+
sourceNode = pendingSourceNode;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
release({
|
|
171
|
+
audioContext: pendingAudioContext,
|
|
172
|
+
mediaStream: pendingMediaStream,
|
|
173
|
+
processorNode: pendingProcessorNode,
|
|
174
|
+
sourceNode: pendingSourceNode
|
|
175
|
+
});
|
|
176
|
+
throw error;
|
|
126
177
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
options.onAudio(pcm);
|
|
135
|
-
};
|
|
136
|
-
sourceNode.connect(processorNode);
|
|
137
|
-
processorNode.connect(audioContext.destination);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
stop();
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
178
|
+
};
|
|
179
|
+
const pending = operation().finally(() => {
|
|
180
|
+
if (startPromise === pending)
|
|
181
|
+
startPromise = null;
|
|
182
|
+
});
|
|
183
|
+
startPromise = pending;
|
|
184
|
+
return pending;
|
|
142
185
|
};
|
|
143
186
|
const stop = () => {
|
|
144
|
-
|
|
145
|
-
sourceNode
|
|
146
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
147
|
-
audioContext?.close();
|
|
187
|
+
lifecycleGeneration += 1;
|
|
188
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
148
189
|
options.onLevel?.(0);
|
|
149
190
|
audioContext = null;
|
|
150
191
|
mediaStream = null;
|
|
@@ -505,6 +546,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
505
546
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
506
547
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
507
548
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
549
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
508
550
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
509
551
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
510
552
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -598,6 +640,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
598
640
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
599
641
|
let lifecycleGeneration = 0;
|
|
600
642
|
let disposed = false;
|
|
643
|
+
let reconnectPreparationAbort = null;
|
|
601
644
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
602
645
|
const state = {
|
|
603
646
|
isConnected: false,
|
|
@@ -685,12 +728,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
685
728
|
return;
|
|
686
729
|
}
|
|
687
730
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
731
|
+
if (options.prepareReconnect) {
|
|
732
|
+
const preparationAbort = new AbortController;
|
|
733
|
+
reconnectPreparationAbort = preparationAbort;
|
|
734
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
735
|
+
try {
|
|
736
|
+
await Promise.race([
|
|
737
|
+
options.prepareReconnect({
|
|
738
|
+
attempt: state.reconnectAttempts,
|
|
739
|
+
path,
|
|
740
|
+
scenarioId: state.scenarioId,
|
|
741
|
+
sessionId: state.sessionId,
|
|
742
|
+
signal: preparationAbort.signal
|
|
743
|
+
}),
|
|
744
|
+
new Promise((_, reject) => {
|
|
745
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
746
|
+
})
|
|
747
|
+
]);
|
|
748
|
+
} finally {
|
|
749
|
+
clearTimeout(timeout);
|
|
750
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
751
|
+
reconnectPreparationAbort = null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
694
755
|
} catch (error) {
|
|
695
756
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
696
757
|
return;
|
|
@@ -814,6 +875,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
814
875
|
state.ws = ws;
|
|
815
876
|
};
|
|
816
877
|
const sendSerialized = (value) => {
|
|
878
|
+
if (disposed)
|
|
879
|
+
return;
|
|
817
880
|
if (state.ws?.readyState === WS_OPEN) {
|
|
818
881
|
state.ws.send(value);
|
|
819
882
|
return;
|
|
@@ -868,6 +931,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
868
931
|
};
|
|
869
932
|
const close = (reason = "client-close") => {
|
|
870
933
|
disposed = true;
|
|
934
|
+
reconnectPreparationAbort?.abort();
|
|
935
|
+
reconnectPreparationAbort = null;
|
|
871
936
|
lifecycleGeneration += 1;
|
|
872
937
|
clearTimers();
|
|
873
938
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -885,6 +950,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
885
950
|
};
|
|
886
951
|
const disconnect = () => {
|
|
887
952
|
disposed = true;
|
|
953
|
+
reconnectPreparationAbort?.abort();
|
|
954
|
+
reconnectPreparationAbort = null;
|
|
888
955
|
lifecycleGeneration += 1;
|
|
889
956
|
clearTimers();
|
|
890
957
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -1136,7 +1203,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1136
1203
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1137
1204
|
}) : null;
|
|
1138
1205
|
const subscribers = new Set;
|
|
1206
|
+
let disposed = false;
|
|
1139
1207
|
const start = (input) => Promise.resolve().then(() => {
|
|
1208
|
+
if (disposed)
|
|
1209
|
+
return;
|
|
1140
1210
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
1141
1211
|
return;
|
|
1142
1212
|
}
|
|
@@ -1195,6 +1265,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1195
1265
|
return connection.callControl(message);
|
|
1196
1266
|
},
|
|
1197
1267
|
close(reason) {
|
|
1268
|
+
disposed = true;
|
|
1198
1269
|
unsubscribeConnection();
|
|
1199
1270
|
browserMediaReporter?.close();
|
|
1200
1271
|
connection.close(reason);
|
|
@@ -1202,6 +1273,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1202
1273
|
notify();
|
|
1203
1274
|
},
|
|
1204
1275
|
disconnect() {
|
|
1276
|
+
disposed = true;
|
|
1205
1277
|
unsubscribeConnection();
|
|
1206
1278
|
browserMediaReporter?.close();
|
|
1207
1279
|
connection.disconnect();
|
|
@@ -1568,6 +1640,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1568
1640
|
...options.connection
|
|
1569
1641
|
});
|
|
1570
1642
|
let capture = null;
|
|
1643
|
+
let captureGeneration = 0;
|
|
1644
|
+
let disposed = false;
|
|
1645
|
+
let startRecordingPromise = null;
|
|
1571
1646
|
let state = createInitialState2(stream);
|
|
1572
1647
|
const subscribers = new Set;
|
|
1573
1648
|
const notify = () => {
|
|
@@ -1624,6 +1699,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1624
1699
|
return capture;
|
|
1625
1700
|
};
|
|
1626
1701
|
const stopRecording = () => {
|
|
1702
|
+
captureGeneration += 1;
|
|
1627
1703
|
capture?.stop();
|
|
1628
1704
|
capture = null;
|
|
1629
1705
|
state = {
|
|
@@ -1633,39 +1709,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1633
1709
|
notify();
|
|
1634
1710
|
};
|
|
1635
1711
|
const startRecording = async () => {
|
|
1712
|
+
if (disposed) {
|
|
1713
|
+
throw new Error("Voice controller is closed.");
|
|
1714
|
+
}
|
|
1636
1715
|
if (state.isRecording) {
|
|
1637
1716
|
return;
|
|
1638
1717
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1718
|
+
if (startRecordingPromise)
|
|
1719
|
+
return startRecordingPromise;
|
|
1720
|
+
const generation = ++captureGeneration;
|
|
1721
|
+
const activeCapture = ensureCapture();
|
|
1722
|
+
const operation = (async () => {
|
|
1723
|
+
try {
|
|
1724
|
+
state = {
|
|
1725
|
+
...state,
|
|
1726
|
+
recordingError: null
|
|
1727
|
+
};
|
|
1728
|
+
notify();
|
|
1729
|
+
await activeCapture.start();
|
|
1730
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
1731
|
+
activeCapture.stop();
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
state = {
|
|
1735
|
+
...state,
|
|
1736
|
+
isRecording: true
|
|
1737
|
+
};
|
|
1738
|
+
notify();
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
activeCapture.stop();
|
|
1741
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
capture = null;
|
|
1745
|
+
state = {
|
|
1746
|
+
...state,
|
|
1747
|
+
isRecording: false,
|
|
1748
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
1749
|
+
};
|
|
1750
|
+
notify();
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
})();
|
|
1754
|
+
const pending = operation.finally(() => {
|
|
1755
|
+
if (startRecordingPromise === pending)
|
|
1756
|
+
startRecordingPromise = null;
|
|
1757
|
+
});
|
|
1758
|
+
startRecordingPromise = pending;
|
|
1759
|
+
return startRecordingPromise;
|
|
1662
1760
|
};
|
|
1663
1761
|
const close = (reason) => {
|
|
1762
|
+
disposed = true;
|
|
1664
1763
|
unsubscribeStream();
|
|
1665
1764
|
stopRecording();
|
|
1666
1765
|
stream.close(reason);
|
|
1667
1766
|
};
|
|
1668
1767
|
const disconnect = () => {
|
|
1768
|
+
disposed = true;
|
|
1669
1769
|
unsubscribeStream();
|
|
1670
1770
|
stopRecording();
|
|
1671
1771
|
stream.disconnect();
|