@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/svelte/index.js
CHANGED
|
@@ -517,48 +517,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
517
517
|
let sourceNode = null;
|
|
518
518
|
let processorNode = null;
|
|
519
519
|
let mediaStream = null;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
520
|
+
let lifecycleGeneration = 0;
|
|
521
|
+
let startPromise = null;
|
|
522
|
+
const release = (resources) => {
|
|
523
|
+
resources.processorNode?.disconnect();
|
|
524
|
+
resources.sourceNode?.disconnect();
|
|
525
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
526
|
+
resources.audioContext?.close();
|
|
527
|
+
};
|
|
528
|
+
const start = () => {
|
|
529
|
+
if (mediaStream)
|
|
530
|
+
return Promise.resolve();
|
|
531
|
+
if (startPromise)
|
|
532
|
+
return startPromise;
|
|
533
|
+
const generation = ++lifecycleGeneration;
|
|
534
|
+
const operation = async () => {
|
|
535
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
536
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
537
|
+
}
|
|
538
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
539
|
+
if (!AudioContextCtor) {
|
|
540
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
541
|
+
}
|
|
542
|
+
let pendingAudioContext = null;
|
|
543
|
+
let pendingMediaStream = null;
|
|
544
|
+
let pendingProcessorNode = null;
|
|
545
|
+
let pendingSourceNode = null;
|
|
546
|
+
try {
|
|
547
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
548
|
+
audio: {
|
|
549
|
+
autoGainControl: true,
|
|
550
|
+
channelCount: options.channelCount ?? 1,
|
|
551
|
+
echoCancellation: true,
|
|
552
|
+
noiseSuppression: true
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
if (generation !== lifecycleGeneration) {
|
|
556
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
535
557
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
558
|
+
pendingAudioContext = new AudioContextCtor;
|
|
559
|
+
if (pendingAudioContext.state === "suspended") {
|
|
560
|
+
await pendingAudioContext.resume();
|
|
561
|
+
}
|
|
562
|
+
if (generation !== lifecycleGeneration) {
|
|
563
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
564
|
+
}
|
|
565
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
566
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
567
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
568
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
569
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
570
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
571
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
572
|
+
options.onAudio(pcm);
|
|
573
|
+
};
|
|
574
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
575
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
576
|
+
if (generation !== lifecycleGeneration) {
|
|
577
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
578
|
+
}
|
|
579
|
+
audioContext = pendingAudioContext;
|
|
580
|
+
mediaStream = pendingMediaStream;
|
|
581
|
+
processorNode = pendingProcessorNode;
|
|
582
|
+
sourceNode = pendingSourceNode;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
release({
|
|
585
|
+
audioContext: pendingAudioContext,
|
|
586
|
+
mediaStream: pendingMediaStream,
|
|
587
|
+
processorNode: pendingProcessorNode,
|
|
588
|
+
sourceNode: pendingSourceNode
|
|
589
|
+
});
|
|
590
|
+
throw error;
|
|
540
591
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
options.onAudio(pcm);
|
|
549
|
-
};
|
|
550
|
-
sourceNode.connect(processorNode);
|
|
551
|
-
processorNode.connect(audioContext.destination);
|
|
552
|
-
} catch (error) {
|
|
553
|
-
stop();
|
|
554
|
-
throw error;
|
|
555
|
-
}
|
|
592
|
+
};
|
|
593
|
+
const pending = operation().finally(() => {
|
|
594
|
+
if (startPromise === pending)
|
|
595
|
+
startPromise = null;
|
|
596
|
+
});
|
|
597
|
+
startPromise = pending;
|
|
598
|
+
return pending;
|
|
556
599
|
};
|
|
557
600
|
const stop = () => {
|
|
558
|
-
|
|
559
|
-
sourceNode
|
|
560
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
561
|
-
audioContext?.close();
|
|
601
|
+
lifecycleGeneration += 1;
|
|
602
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
562
603
|
options.onLevel?.(0);
|
|
563
604
|
audioContext = null;
|
|
564
605
|
mediaStream = null;
|
|
@@ -770,6 +811,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
770
811
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
771
812
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
772
813
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
814
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
773
815
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
774
816
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
775
817
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -863,6 +905,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
863
905
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
864
906
|
let lifecycleGeneration = 0;
|
|
865
907
|
let disposed = false;
|
|
908
|
+
let reconnectPreparationAbort = null;
|
|
866
909
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
867
910
|
const state = {
|
|
868
911
|
isConnected: false,
|
|
@@ -950,12 +993,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
950
993
|
return;
|
|
951
994
|
}
|
|
952
995
|
try {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
996
|
+
if (options.prepareReconnect) {
|
|
997
|
+
const preparationAbort = new AbortController;
|
|
998
|
+
reconnectPreparationAbort = preparationAbort;
|
|
999
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
1000
|
+
try {
|
|
1001
|
+
await Promise.race([
|
|
1002
|
+
options.prepareReconnect({
|
|
1003
|
+
attempt: state.reconnectAttempts,
|
|
1004
|
+
path,
|
|
1005
|
+
scenarioId: state.scenarioId,
|
|
1006
|
+
sessionId: state.sessionId,
|
|
1007
|
+
signal: preparationAbort.signal
|
|
1008
|
+
}),
|
|
1009
|
+
new Promise((_, reject) => {
|
|
1010
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
1011
|
+
})
|
|
1012
|
+
]);
|
|
1013
|
+
} finally {
|
|
1014
|
+
clearTimeout(timeout);
|
|
1015
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
1016
|
+
reconnectPreparationAbort = null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
959
1020
|
} catch (error) {
|
|
960
1021
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
961
1022
|
return;
|
|
@@ -1079,6 +1140,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1079
1140
|
state.ws = ws;
|
|
1080
1141
|
};
|
|
1081
1142
|
const sendSerialized = (value) => {
|
|
1143
|
+
if (disposed)
|
|
1144
|
+
return;
|
|
1082
1145
|
if (state.ws?.readyState === WS_OPEN) {
|
|
1083
1146
|
state.ws.send(value);
|
|
1084
1147
|
return;
|
|
@@ -1133,6 +1196,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1133
1196
|
};
|
|
1134
1197
|
const close = (reason = "client-close") => {
|
|
1135
1198
|
disposed = true;
|
|
1199
|
+
reconnectPreparationAbort?.abort();
|
|
1200
|
+
reconnectPreparationAbort = null;
|
|
1136
1201
|
lifecycleGeneration += 1;
|
|
1137
1202
|
clearTimers();
|
|
1138
1203
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -1150,6 +1215,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
1150
1215
|
};
|
|
1151
1216
|
const disconnect = () => {
|
|
1152
1217
|
disposed = true;
|
|
1218
|
+
reconnectPreparationAbort?.abort();
|
|
1219
|
+
reconnectPreparationAbort = null;
|
|
1153
1220
|
lifecycleGeneration += 1;
|
|
1154
1221
|
clearTimers();
|
|
1155
1222
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -1401,7 +1468,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1401
1468
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1402
1469
|
}) : null;
|
|
1403
1470
|
const subscribers = new Set;
|
|
1471
|
+
let disposed = false;
|
|
1404
1472
|
const start = (input) => Promise.resolve().then(() => {
|
|
1473
|
+
if (disposed)
|
|
1474
|
+
return;
|
|
1405
1475
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
1406
1476
|
return;
|
|
1407
1477
|
}
|
|
@@ -1460,6 +1530,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1460
1530
|
return connection.callControl(message);
|
|
1461
1531
|
},
|
|
1462
1532
|
close(reason) {
|
|
1533
|
+
disposed = true;
|
|
1463
1534
|
unsubscribeConnection();
|
|
1464
1535
|
browserMediaReporter?.close();
|
|
1465
1536
|
connection.close(reason);
|
|
@@ -1467,6 +1538,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1467
1538
|
notify();
|
|
1468
1539
|
},
|
|
1469
1540
|
disconnect() {
|
|
1541
|
+
disposed = true;
|
|
1470
1542
|
unsubscribeConnection();
|
|
1471
1543
|
browserMediaReporter?.close();
|
|
1472
1544
|
connection.disconnect();
|
|
@@ -1870,6 +1942,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1870
1942
|
...options.connection
|
|
1871
1943
|
});
|
|
1872
1944
|
let capture = null;
|
|
1945
|
+
let captureGeneration = 0;
|
|
1946
|
+
let disposed = false;
|
|
1947
|
+
let startRecordingPromise = null;
|
|
1873
1948
|
let state = createInitialState2(stream);
|
|
1874
1949
|
const subscribers = new Set;
|
|
1875
1950
|
const notify = () => {
|
|
@@ -1926,6 +2001,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1926
2001
|
return capture;
|
|
1927
2002
|
};
|
|
1928
2003
|
const stopRecording = () => {
|
|
2004
|
+
captureGeneration += 1;
|
|
1929
2005
|
capture?.stop();
|
|
1930
2006
|
capture = null;
|
|
1931
2007
|
state = {
|
|
@@ -1935,39 +2011,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
1935
2011
|
notify();
|
|
1936
2012
|
};
|
|
1937
2013
|
const startRecording = async () => {
|
|
2014
|
+
if (disposed) {
|
|
2015
|
+
throw new Error("Voice controller is closed.");
|
|
2016
|
+
}
|
|
1938
2017
|
if (state.isRecording) {
|
|
1939
2018
|
return;
|
|
1940
2019
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2020
|
+
if (startRecordingPromise)
|
|
2021
|
+
return startRecordingPromise;
|
|
2022
|
+
const generation = ++captureGeneration;
|
|
2023
|
+
const activeCapture = ensureCapture();
|
|
2024
|
+
const operation = (async () => {
|
|
2025
|
+
try {
|
|
2026
|
+
state = {
|
|
2027
|
+
...state,
|
|
2028
|
+
recordingError: null
|
|
2029
|
+
};
|
|
2030
|
+
notify();
|
|
2031
|
+
await activeCapture.start();
|
|
2032
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
2033
|
+
activeCapture.stop();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
state = {
|
|
2037
|
+
...state,
|
|
2038
|
+
isRecording: true
|
|
2039
|
+
};
|
|
2040
|
+
notify();
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
activeCapture.stop();
|
|
2043
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
2044
|
+
throw error;
|
|
2045
|
+
}
|
|
2046
|
+
capture = null;
|
|
2047
|
+
state = {
|
|
2048
|
+
...state,
|
|
2049
|
+
isRecording: false,
|
|
2050
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
2051
|
+
};
|
|
2052
|
+
notify();
|
|
2053
|
+
throw error;
|
|
2054
|
+
}
|
|
2055
|
+
})();
|
|
2056
|
+
const pending = operation.finally(() => {
|
|
2057
|
+
if (startRecordingPromise === pending)
|
|
2058
|
+
startRecordingPromise = null;
|
|
2059
|
+
});
|
|
2060
|
+
startRecordingPromise = pending;
|
|
2061
|
+
return startRecordingPromise;
|
|
1964
2062
|
};
|
|
1965
2063
|
const close = (reason) => {
|
|
2064
|
+
disposed = true;
|
|
1966
2065
|
unsubscribeStream();
|
|
1967
2066
|
stopRecording();
|
|
1968
2067
|
stream.close(reason);
|
|
1969
2068
|
};
|
|
1970
2069
|
const disconnect = () => {
|
|
2070
|
+
disposed = true;
|
|
1971
2071
|
unsubscribeStream();
|
|
1972
2072
|
stopRecording();
|
|
1973
2073
|
stream.disconnect();
|
package/dist/testing/index.js
CHANGED
|
@@ -2738,48 +2738,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
2738
2738
|
let sourceNode = null;
|
|
2739
2739
|
let processorNode = null;
|
|
2740
2740
|
let mediaStream = null;
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2741
|
+
let lifecycleGeneration = 0;
|
|
2742
|
+
let startPromise = null;
|
|
2743
|
+
const release = (resources) => {
|
|
2744
|
+
resources.processorNode?.disconnect();
|
|
2745
|
+
resources.sourceNode?.disconnect();
|
|
2746
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
2747
|
+
resources.audioContext?.close();
|
|
2748
|
+
};
|
|
2749
|
+
const start = () => {
|
|
2750
|
+
if (mediaStream)
|
|
2751
|
+
return Promise.resolve();
|
|
2752
|
+
if (startPromise)
|
|
2753
|
+
return startPromise;
|
|
2754
|
+
const generation = ++lifecycleGeneration;
|
|
2755
|
+
const operation = async () => {
|
|
2756
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
2757
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
2758
|
+
}
|
|
2759
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
2760
|
+
if (!AudioContextCtor) {
|
|
2761
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
2762
|
+
}
|
|
2763
|
+
let pendingAudioContext = null;
|
|
2764
|
+
let pendingMediaStream = null;
|
|
2765
|
+
let pendingProcessorNode = null;
|
|
2766
|
+
let pendingSourceNode = null;
|
|
2767
|
+
try {
|
|
2768
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
2769
|
+
audio: {
|
|
2770
|
+
autoGainControl: true,
|
|
2771
|
+
channelCount: options.channelCount ?? 1,
|
|
2772
|
+
echoCancellation: true,
|
|
2773
|
+
noiseSuppression: true
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
if (generation !== lifecycleGeneration) {
|
|
2777
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2756
2778
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2779
|
+
pendingAudioContext = new AudioContextCtor;
|
|
2780
|
+
if (pendingAudioContext.state === "suspended") {
|
|
2781
|
+
await pendingAudioContext.resume();
|
|
2782
|
+
}
|
|
2783
|
+
if (generation !== lifecycleGeneration) {
|
|
2784
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2785
|
+
}
|
|
2786
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
2787
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
2788
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
2789
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
2790
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
2791
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
2792
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
2793
|
+
options.onAudio(pcm);
|
|
2794
|
+
};
|
|
2795
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
2796
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
2797
|
+
if (generation !== lifecycleGeneration) {
|
|
2798
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
2799
|
+
}
|
|
2800
|
+
audioContext = pendingAudioContext;
|
|
2801
|
+
mediaStream = pendingMediaStream;
|
|
2802
|
+
processorNode = pendingProcessorNode;
|
|
2803
|
+
sourceNode = pendingSourceNode;
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
release({
|
|
2806
|
+
audioContext: pendingAudioContext,
|
|
2807
|
+
mediaStream: pendingMediaStream,
|
|
2808
|
+
processorNode: pendingProcessorNode,
|
|
2809
|
+
sourceNode: pendingSourceNode
|
|
2810
|
+
});
|
|
2811
|
+
throw error;
|
|
2812
|
+
}
|
|
2813
|
+
};
|
|
2814
|
+
const pending = operation().finally(() => {
|
|
2815
|
+
if (startPromise === pending)
|
|
2816
|
+
startPromise = null;
|
|
2817
|
+
});
|
|
2818
|
+
startPromise = pending;
|
|
2819
|
+
return pending;
|
|
2777
2820
|
};
|
|
2778
2821
|
const stop = () => {
|
|
2779
|
-
|
|
2780
|
-
sourceNode
|
|
2781
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
2782
|
-
audioContext?.close();
|
|
2822
|
+
lifecycleGeneration += 1;
|
|
2823
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
2783
2824
|
options.onLevel?.(0);
|
|
2784
2825
|
audioContext = null;
|
|
2785
2826
|
mediaStream = null;
|
|
@@ -2991,6 +3032,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
2991
3032
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
2992
3033
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
2993
3034
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
3035
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
2994
3036
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
2995
3037
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
2996
3038
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -3084,6 +3126,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3084
3126
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
3085
3127
|
let lifecycleGeneration = 0;
|
|
3086
3128
|
let disposed = false;
|
|
3129
|
+
let reconnectPreparationAbort = null;
|
|
3087
3130
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
3088
3131
|
const state = {
|
|
3089
3132
|
isConnected: false,
|
|
@@ -3171,12 +3214,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3171
3214
|
return;
|
|
3172
3215
|
}
|
|
3173
3216
|
try {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3217
|
+
if (options.prepareReconnect) {
|
|
3218
|
+
const preparationAbort = new AbortController;
|
|
3219
|
+
reconnectPreparationAbort = preparationAbort;
|
|
3220
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
3221
|
+
try {
|
|
3222
|
+
await Promise.race([
|
|
3223
|
+
options.prepareReconnect({
|
|
3224
|
+
attempt: state.reconnectAttempts,
|
|
3225
|
+
path,
|
|
3226
|
+
scenarioId: state.scenarioId,
|
|
3227
|
+
sessionId: state.sessionId,
|
|
3228
|
+
signal: preparationAbort.signal
|
|
3229
|
+
}),
|
|
3230
|
+
new Promise((_, reject) => {
|
|
3231
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
3232
|
+
})
|
|
3233
|
+
]);
|
|
3234
|
+
} finally {
|
|
3235
|
+
clearTimeout(timeout);
|
|
3236
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
3237
|
+
reconnectPreparationAbort = null;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3180
3241
|
} catch (error) {
|
|
3181
3242
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
3182
3243
|
return;
|
|
@@ -3300,6 +3361,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3300
3361
|
state.ws = ws;
|
|
3301
3362
|
};
|
|
3302
3363
|
const sendSerialized = (value) => {
|
|
3364
|
+
if (disposed)
|
|
3365
|
+
return;
|
|
3303
3366
|
if (state.ws?.readyState === WS_OPEN) {
|
|
3304
3367
|
state.ws.send(value);
|
|
3305
3368
|
return;
|
|
@@ -3354,6 +3417,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3354
3417
|
};
|
|
3355
3418
|
const close = (reason = "client-close") => {
|
|
3356
3419
|
disposed = true;
|
|
3420
|
+
reconnectPreparationAbort?.abort();
|
|
3421
|
+
reconnectPreparationAbort = null;
|
|
3357
3422
|
lifecycleGeneration += 1;
|
|
3358
3423
|
clearTimers();
|
|
3359
3424
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -3371,6 +3436,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
3371
3436
|
};
|
|
3372
3437
|
const disconnect = () => {
|
|
3373
3438
|
disposed = true;
|
|
3439
|
+
reconnectPreparationAbort?.abort();
|
|
3440
|
+
reconnectPreparationAbort = null;
|
|
3374
3441
|
lifecycleGeneration += 1;
|
|
3375
3442
|
clearTimers();
|
|
3376
3443
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -3622,7 +3689,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3622
3689
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
3623
3690
|
}) : null;
|
|
3624
3691
|
const subscribers = new Set;
|
|
3692
|
+
let disposed = false;
|
|
3625
3693
|
const start = (input) => Promise.resolve().then(() => {
|
|
3694
|
+
if (disposed)
|
|
3695
|
+
return;
|
|
3626
3696
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
3627
3697
|
return;
|
|
3628
3698
|
}
|
|
@@ -3681,6 +3751,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3681
3751
|
return connection.callControl(message);
|
|
3682
3752
|
},
|
|
3683
3753
|
close(reason) {
|
|
3754
|
+
disposed = true;
|
|
3684
3755
|
unsubscribeConnection();
|
|
3685
3756
|
browserMediaReporter?.close();
|
|
3686
3757
|
connection.close(reason);
|
|
@@ -3688,6 +3759,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
3688
3759
|
notify();
|
|
3689
3760
|
},
|
|
3690
3761
|
disconnect() {
|
|
3762
|
+
disposed = true;
|
|
3691
3763
|
unsubscribeConnection();
|
|
3692
3764
|
browserMediaReporter?.close();
|
|
3693
3765
|
connection.disconnect();
|
|
@@ -4091,6 +4163,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4091
4163
|
...options.connection
|
|
4092
4164
|
});
|
|
4093
4165
|
let capture = null;
|
|
4166
|
+
let captureGeneration = 0;
|
|
4167
|
+
let disposed = false;
|
|
4168
|
+
let startRecordingPromise = null;
|
|
4094
4169
|
let state = createInitialState3(stream);
|
|
4095
4170
|
const subscribers = new Set;
|
|
4096
4171
|
const notify = () => {
|
|
@@ -4147,6 +4222,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4147
4222
|
return capture;
|
|
4148
4223
|
};
|
|
4149
4224
|
const stopRecording = () => {
|
|
4225
|
+
captureGeneration += 1;
|
|
4150
4226
|
capture?.stop();
|
|
4151
4227
|
capture = null;
|
|
4152
4228
|
state = {
|
|
@@ -4156,39 +4232,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
4156
4232
|
notify();
|
|
4157
4233
|
};
|
|
4158
4234
|
const startRecording = async () => {
|
|
4235
|
+
if (disposed) {
|
|
4236
|
+
throw new Error("Voice controller is closed.");
|
|
4237
|
+
}
|
|
4159
4238
|
if (state.isRecording) {
|
|
4160
4239
|
return;
|
|
4161
4240
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4241
|
+
if (startRecordingPromise)
|
|
4242
|
+
return startRecordingPromise;
|
|
4243
|
+
const generation = ++captureGeneration;
|
|
4244
|
+
const activeCapture = ensureCapture();
|
|
4245
|
+
const operation = (async () => {
|
|
4246
|
+
try {
|
|
4247
|
+
state = {
|
|
4248
|
+
...state,
|
|
4249
|
+
recordingError: null
|
|
4250
|
+
};
|
|
4251
|
+
notify();
|
|
4252
|
+
await activeCapture.start();
|
|
4253
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
4254
|
+
activeCapture.stop();
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
state = {
|
|
4258
|
+
...state,
|
|
4259
|
+
isRecording: true
|
|
4260
|
+
};
|
|
4261
|
+
notify();
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
activeCapture.stop();
|
|
4264
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
4265
|
+
throw error;
|
|
4266
|
+
}
|
|
4267
|
+
capture = null;
|
|
4268
|
+
state = {
|
|
4269
|
+
...state,
|
|
4270
|
+
isRecording: false,
|
|
4271
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
4272
|
+
};
|
|
4273
|
+
notify();
|
|
4274
|
+
throw error;
|
|
4275
|
+
}
|
|
4276
|
+
})();
|
|
4277
|
+
const pending = operation.finally(() => {
|
|
4278
|
+
if (startRecordingPromise === pending)
|
|
4279
|
+
startRecordingPromise = null;
|
|
4280
|
+
});
|
|
4281
|
+
startRecordingPromise = pending;
|
|
4282
|
+
return startRecordingPromise;
|
|
4185
4283
|
};
|
|
4186
4284
|
const close = (reason) => {
|
|
4285
|
+
disposed = true;
|
|
4187
4286
|
unsubscribeStream();
|
|
4188
4287
|
stopRecording();
|
|
4189
4288
|
stream.close(reason);
|
|
4190
4289
|
};
|
|
4191
4290
|
const disconnect = () => {
|
|
4291
|
+
disposed = true;
|
|
4192
4292
|
unsubscribeStream();
|
|
4193
4293
|
stopRecording();
|
|
4194
4294
|
stream.disconnect();
|