@absolutejs/voice 0.0.22-beta.656 → 0.0.22-beta.657
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/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/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();
|
package/dist/vue/index.js
CHANGED
|
@@ -10838,6 +10838,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
10838
10838
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
10839
10839
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
10840
10840
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
10841
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
10841
10842
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
10842
10843
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
10843
10844
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -10931,6 +10932,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
10931
10932
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
10932
10933
|
let lifecycleGeneration = 0;
|
|
10933
10934
|
let disposed = false;
|
|
10935
|
+
let reconnectPreparationAbort = null;
|
|
10934
10936
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
10935
10937
|
const state = {
|
|
10936
10938
|
isConnected: false,
|
|
@@ -11018,12 +11020,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11018
11020
|
return;
|
|
11019
11021
|
}
|
|
11020
11022
|
try {
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11023
|
+
if (options.prepareReconnect) {
|
|
11024
|
+
const preparationAbort = new AbortController;
|
|
11025
|
+
reconnectPreparationAbort = preparationAbort;
|
|
11026
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
11027
|
+
try {
|
|
11028
|
+
await Promise.race([
|
|
11029
|
+
options.prepareReconnect({
|
|
11030
|
+
attempt: state.reconnectAttempts,
|
|
11031
|
+
path,
|
|
11032
|
+
scenarioId: state.scenarioId,
|
|
11033
|
+
sessionId: state.sessionId,
|
|
11034
|
+
signal: preparationAbort.signal
|
|
11035
|
+
}),
|
|
11036
|
+
new Promise((_, reject) => {
|
|
11037
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
11038
|
+
})
|
|
11039
|
+
]);
|
|
11040
|
+
} finally {
|
|
11041
|
+
clearTimeout(timeout);
|
|
11042
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
11043
|
+
reconnectPreparationAbort = null;
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
11046
|
+
}
|
|
11027
11047
|
} catch (error) {
|
|
11028
11048
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
11029
11049
|
return;
|
|
@@ -11147,6 +11167,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11147
11167
|
state.ws = ws;
|
|
11148
11168
|
};
|
|
11149
11169
|
const sendSerialized = (value) => {
|
|
11170
|
+
if (disposed)
|
|
11171
|
+
return;
|
|
11150
11172
|
if (state.ws?.readyState === WS_OPEN) {
|
|
11151
11173
|
state.ws.send(value);
|
|
11152
11174
|
return;
|
|
@@ -11201,6 +11223,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11201
11223
|
};
|
|
11202
11224
|
const close = (reason = "client-close") => {
|
|
11203
11225
|
disposed = true;
|
|
11226
|
+
reconnectPreparationAbort?.abort();
|
|
11227
|
+
reconnectPreparationAbort = null;
|
|
11204
11228
|
lifecycleGeneration += 1;
|
|
11205
11229
|
clearTimers();
|
|
11206
11230
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -11218,6 +11242,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
11218
11242
|
};
|
|
11219
11243
|
const disconnect = () => {
|
|
11220
11244
|
disposed = true;
|
|
11245
|
+
reconnectPreparationAbort?.abort();
|
|
11246
|
+
reconnectPreparationAbort = null;
|
|
11221
11247
|
lifecycleGeneration += 1;
|
|
11222
11248
|
clearTimers();
|
|
11223
11249
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -11469,7 +11495,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
11469
11495
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
11470
11496
|
}) : null;
|
|
11471
11497
|
const subscribers = new Set;
|
|
11498
|
+
let disposed = false;
|
|
11472
11499
|
const start = (input) => Promise.resolve().then(() => {
|
|
11500
|
+
if (disposed)
|
|
11501
|
+
return;
|
|
11473
11502
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
11474
11503
|
return;
|
|
11475
11504
|
}
|
|
@@ -11528,6 +11557,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
11528
11557
|
return connection.callControl(message);
|
|
11529
11558
|
},
|
|
11530
11559
|
close(reason) {
|
|
11560
|
+
disposed = true;
|
|
11531
11561
|
unsubscribeConnection();
|
|
11532
11562
|
browserMediaReporter?.close();
|
|
11533
11563
|
connection.close(reason);
|
|
@@ -11535,6 +11565,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
11535
11565
|
notify();
|
|
11536
11566
|
},
|
|
11537
11567
|
disconnect() {
|
|
11568
|
+
disposed = true;
|
|
11538
11569
|
unsubscribeConnection();
|
|
11539
11570
|
browserMediaReporter?.close();
|
|
11540
11571
|
connection.disconnect();
|
|
@@ -11753,48 +11784,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
11753
11784
|
let sourceNode = null;
|
|
11754
11785
|
let processorNode = null;
|
|
11755
11786
|
let mediaStream = null;
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11787
|
+
let lifecycleGeneration = 0;
|
|
11788
|
+
let startPromise = null;
|
|
11789
|
+
const release = (resources) => {
|
|
11790
|
+
resources.processorNode?.disconnect();
|
|
11791
|
+
resources.sourceNode?.disconnect();
|
|
11792
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
11793
|
+
resources.audioContext?.close();
|
|
11794
|
+
};
|
|
11795
|
+
const start = () => {
|
|
11796
|
+
if (mediaStream)
|
|
11797
|
+
return Promise.resolve();
|
|
11798
|
+
if (startPromise)
|
|
11799
|
+
return startPromise;
|
|
11800
|
+
const generation = ++lifecycleGeneration;
|
|
11801
|
+
const operation = async () => {
|
|
11802
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
11803
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
11804
|
+
}
|
|
11805
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
11806
|
+
if (!AudioContextCtor) {
|
|
11807
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
11808
|
+
}
|
|
11809
|
+
let pendingAudioContext = null;
|
|
11810
|
+
let pendingMediaStream = null;
|
|
11811
|
+
let pendingProcessorNode = null;
|
|
11812
|
+
let pendingSourceNode = null;
|
|
11813
|
+
try {
|
|
11814
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
11815
|
+
audio: {
|
|
11816
|
+
autoGainControl: true,
|
|
11817
|
+
channelCount: options.channelCount ?? 1,
|
|
11818
|
+
echoCancellation: true,
|
|
11819
|
+
noiseSuppression: true
|
|
11820
|
+
}
|
|
11821
|
+
});
|
|
11822
|
+
if (generation !== lifecycleGeneration) {
|
|
11823
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
11771
11824
|
}
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11825
|
+
pendingAudioContext = new AudioContextCtor;
|
|
11826
|
+
if (pendingAudioContext.state === "suspended") {
|
|
11827
|
+
await pendingAudioContext.resume();
|
|
11828
|
+
}
|
|
11829
|
+
if (generation !== lifecycleGeneration) {
|
|
11830
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
11831
|
+
}
|
|
11832
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
11833
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
11834
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
11835
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
11836
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
11837
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
11838
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
11839
|
+
options.onAudio(pcm);
|
|
11840
|
+
};
|
|
11841
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
11842
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
11843
|
+
if (generation !== lifecycleGeneration) {
|
|
11844
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
11845
|
+
}
|
|
11846
|
+
audioContext = pendingAudioContext;
|
|
11847
|
+
mediaStream = pendingMediaStream;
|
|
11848
|
+
processorNode = pendingProcessorNode;
|
|
11849
|
+
sourceNode = pendingSourceNode;
|
|
11850
|
+
} catch (error) {
|
|
11851
|
+
release({
|
|
11852
|
+
audioContext: pendingAudioContext,
|
|
11853
|
+
mediaStream: pendingMediaStream,
|
|
11854
|
+
processorNode: pendingProcessorNode,
|
|
11855
|
+
sourceNode: pendingSourceNode
|
|
11856
|
+
});
|
|
11857
|
+
throw error;
|
|
11776
11858
|
}
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
options.onAudio(pcm);
|
|
11785
|
-
};
|
|
11786
|
-
sourceNode.connect(processorNode);
|
|
11787
|
-
processorNode.connect(audioContext.destination);
|
|
11788
|
-
} catch (error) {
|
|
11789
|
-
stop();
|
|
11790
|
-
throw error;
|
|
11791
|
-
}
|
|
11859
|
+
};
|
|
11860
|
+
const pending = operation().finally(() => {
|
|
11861
|
+
if (startPromise === pending)
|
|
11862
|
+
startPromise = null;
|
|
11863
|
+
});
|
|
11864
|
+
startPromise = pending;
|
|
11865
|
+
return pending;
|
|
11792
11866
|
};
|
|
11793
11867
|
const stop = () => {
|
|
11794
|
-
|
|
11795
|
-
sourceNode
|
|
11796
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
11797
|
-
audioContext?.close();
|
|
11868
|
+
lifecycleGeneration += 1;
|
|
11869
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
11798
11870
|
options.onLevel?.(0);
|
|
11799
11871
|
audioContext = null;
|
|
11800
11872
|
mediaStream = null;
|
|
@@ -12150,6 +12222,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12150
12222
|
...options.connection
|
|
12151
12223
|
});
|
|
12152
12224
|
let capture = null;
|
|
12225
|
+
let captureGeneration = 0;
|
|
12226
|
+
let disposed = false;
|
|
12227
|
+
let startRecordingPromise = null;
|
|
12153
12228
|
let state = createInitialState2(stream);
|
|
12154
12229
|
const subscribers = new Set;
|
|
12155
12230
|
const notify = () => {
|
|
@@ -12206,6 +12281,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12206
12281
|
return capture;
|
|
12207
12282
|
};
|
|
12208
12283
|
const stopRecording = () => {
|
|
12284
|
+
captureGeneration += 1;
|
|
12209
12285
|
capture?.stop();
|
|
12210
12286
|
capture = null;
|
|
12211
12287
|
state = {
|
|
@@ -12215,39 +12291,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
12215
12291
|
notify();
|
|
12216
12292
|
};
|
|
12217
12293
|
const startRecording = async () => {
|
|
12294
|
+
if (disposed) {
|
|
12295
|
+
throw new Error("Voice controller is closed.");
|
|
12296
|
+
}
|
|
12218
12297
|
if (state.isRecording) {
|
|
12219
12298
|
return;
|
|
12220
12299
|
}
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12300
|
+
if (startRecordingPromise)
|
|
12301
|
+
return startRecordingPromise;
|
|
12302
|
+
const generation = ++captureGeneration;
|
|
12303
|
+
const activeCapture = ensureCapture();
|
|
12304
|
+
const operation = (async () => {
|
|
12305
|
+
try {
|
|
12306
|
+
state = {
|
|
12307
|
+
...state,
|
|
12308
|
+
recordingError: null
|
|
12309
|
+
};
|
|
12310
|
+
notify();
|
|
12311
|
+
await activeCapture.start();
|
|
12312
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
12313
|
+
activeCapture.stop();
|
|
12314
|
+
return;
|
|
12315
|
+
}
|
|
12316
|
+
state = {
|
|
12317
|
+
...state,
|
|
12318
|
+
isRecording: true
|
|
12319
|
+
};
|
|
12320
|
+
notify();
|
|
12321
|
+
} catch (error) {
|
|
12322
|
+
activeCapture.stop();
|
|
12323
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
12324
|
+
throw error;
|
|
12325
|
+
}
|
|
12326
|
+
capture = null;
|
|
12327
|
+
state = {
|
|
12328
|
+
...state,
|
|
12329
|
+
isRecording: false,
|
|
12330
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
12331
|
+
};
|
|
12332
|
+
notify();
|
|
12333
|
+
throw error;
|
|
12334
|
+
}
|
|
12335
|
+
})();
|
|
12336
|
+
const pending = operation.finally(() => {
|
|
12337
|
+
if (startRecordingPromise === pending)
|
|
12338
|
+
startRecordingPromise = null;
|
|
12339
|
+
});
|
|
12340
|
+
startRecordingPromise = pending;
|
|
12341
|
+
return startRecordingPromise;
|
|
12244
12342
|
};
|
|
12245
12343
|
const close = (reason) => {
|
|
12344
|
+
disposed = true;
|
|
12246
12345
|
unsubscribeStream();
|
|
12247
12346
|
stopRecording();
|
|
12248
12347
|
stream.close(reason);
|
|
12249
12348
|
};
|
|
12250
12349
|
const disconnect = () => {
|
|
12350
|
+
disposed = true;
|
|
12251
12351
|
unsubscribeStream();
|
|
12252
12352
|
stopRecording();
|
|
12253
12353
|
stream.disconnect();
|