@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/client/index.js
CHANGED
|
@@ -110,6 +110,7 @@ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 15;
|
|
|
110
110
|
var DEFAULT_PING_INTERVAL = 30000;
|
|
111
111
|
var DEFAULT_RECONNECT_RESET_AFTER_MS = 30000;
|
|
112
112
|
var DEFAULT_CALL_CONTROL_ACK_TIMEOUT_MS = 5000;
|
|
113
|
+
var DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS = 1e4;
|
|
113
114
|
var MAX_PENDING_CONTROL_MESSAGES = 100;
|
|
114
115
|
var RECONNECT_BASE_DELAY_MS = 500;
|
|
115
116
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 8000;
|
|
@@ -203,6 +204,7 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
203
204
|
const reconnectResetAfterMs = options.reconnectResetAfterMs ?? DEFAULT_RECONNECT_RESET_AFTER_MS;
|
|
204
205
|
let lifecycleGeneration = 0;
|
|
205
206
|
let disposed = false;
|
|
207
|
+
let reconnectPreparationAbort = null;
|
|
206
208
|
const computeReconnectDelayMs = (attempt) => computeVoiceReconnectDelayMs(attempt, RECONNECT_BASE_DELAY_MS, reconnectMaxDelayMs);
|
|
207
209
|
const state = {
|
|
208
210
|
isConnected: false,
|
|
@@ -290,12 +292,30 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
290
292
|
return;
|
|
291
293
|
}
|
|
292
294
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
if (options.prepareReconnect) {
|
|
296
|
+
const preparationAbort = new AbortController;
|
|
297
|
+
reconnectPreparationAbort = preparationAbort;
|
|
298
|
+
const timeout = setTimeout(() => preparationAbort.abort("Voice reconnect preparation timed out"), options.prepareReconnectTimeoutMs ?? DEFAULT_PREPARE_RECONNECT_TIMEOUT_MS);
|
|
299
|
+
try {
|
|
300
|
+
await Promise.race([
|
|
301
|
+
options.prepareReconnect({
|
|
302
|
+
attempt: state.reconnectAttempts,
|
|
303
|
+
path,
|
|
304
|
+
scenarioId: state.scenarioId,
|
|
305
|
+
sessionId: state.sessionId,
|
|
306
|
+
signal: preparationAbort.signal
|
|
307
|
+
}),
|
|
308
|
+
new Promise((_, reject) => {
|
|
309
|
+
preparationAbort.signal.addEventListener("abort", () => reject(new Error("Voice reconnect preparation timed out.")), { once: true });
|
|
310
|
+
})
|
|
311
|
+
]);
|
|
312
|
+
} finally {
|
|
313
|
+
clearTimeout(timeout);
|
|
314
|
+
if (reconnectPreparationAbort === preparationAbort) {
|
|
315
|
+
reconnectPreparationAbort = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
299
319
|
} catch (error) {
|
|
300
320
|
if (disposed || lifecycleGeneration !== scheduledGeneration)
|
|
301
321
|
return;
|
|
@@ -419,6 +439,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
419
439
|
state.ws = ws;
|
|
420
440
|
};
|
|
421
441
|
const sendSerialized = (value) => {
|
|
442
|
+
if (disposed)
|
|
443
|
+
return;
|
|
422
444
|
if (state.ws?.readyState === WS_OPEN) {
|
|
423
445
|
state.ws.send(value);
|
|
424
446
|
return;
|
|
@@ -473,6 +495,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
473
495
|
};
|
|
474
496
|
const close = (reason = "client-close") => {
|
|
475
497
|
disposed = true;
|
|
498
|
+
reconnectPreparationAbort?.abort();
|
|
499
|
+
reconnectPreparationAbort = null;
|
|
476
500
|
lifecycleGeneration += 1;
|
|
477
501
|
clearTimers();
|
|
478
502
|
rejectPendingCallControls("Voice connection closed");
|
|
@@ -490,6 +514,8 @@ var createVoiceConnection = (path, options = {}) => {
|
|
|
490
514
|
};
|
|
491
515
|
const disconnect = () => {
|
|
492
516
|
disposed = true;
|
|
517
|
+
reconnectPreparationAbort?.abort();
|
|
518
|
+
reconnectPreparationAbort = null;
|
|
493
519
|
lifecycleGeneration += 1;
|
|
494
520
|
clearTimers();
|
|
495
521
|
rejectPendingCallControls("Voice connection disconnected");
|
|
@@ -1663,7 +1689,10 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1663
1689
|
getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
|
|
1664
1690
|
}) : null;
|
|
1665
1691
|
const subscribers = new Set;
|
|
1692
|
+
let disposed = false;
|
|
1666
1693
|
const start = (input) => Promise.resolve().then(() => {
|
|
1694
|
+
if (disposed)
|
|
1695
|
+
return;
|
|
1667
1696
|
if (!input?.sessionId && !input?.scenarioId) {
|
|
1668
1697
|
return;
|
|
1669
1698
|
}
|
|
@@ -1722,6 +1751,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1722
1751
|
return connection.callControl(message);
|
|
1723
1752
|
},
|
|
1724
1753
|
close(reason) {
|
|
1754
|
+
disposed = true;
|
|
1725
1755
|
unsubscribeConnection();
|
|
1726
1756
|
browserMediaReporter?.close();
|
|
1727
1757
|
connection.close(reason);
|
|
@@ -1729,6 +1759,7 @@ var createVoiceStream = (path, options = {}) => {
|
|
|
1729
1759
|
notify();
|
|
1730
1760
|
},
|
|
1731
1761
|
disconnect() {
|
|
1762
|
+
disposed = true;
|
|
1732
1763
|
unsubscribeConnection();
|
|
1733
1764
|
browserMediaReporter?.close();
|
|
1734
1765
|
connection.disconnect();
|
|
@@ -1887,48 +1918,89 @@ var createMicrophoneCapture = (options) => {
|
|
|
1887
1918
|
let sourceNode = null;
|
|
1888
1919
|
let processorNode = null;
|
|
1889
1920
|
let mediaStream = null;
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1921
|
+
let lifecycleGeneration = 0;
|
|
1922
|
+
let startPromise = null;
|
|
1923
|
+
const release = (resources) => {
|
|
1924
|
+
resources.processorNode?.disconnect();
|
|
1925
|
+
resources.sourceNode?.disconnect();
|
|
1926
|
+
resources.mediaStream?.getTracks().forEach((track) => track.stop());
|
|
1927
|
+
resources.audioContext?.close();
|
|
1928
|
+
};
|
|
1929
|
+
const start = () => {
|
|
1930
|
+
if (mediaStream)
|
|
1931
|
+
return Promise.resolve();
|
|
1932
|
+
if (startPromise)
|
|
1933
|
+
return startPromise;
|
|
1934
|
+
const generation = ++lifecycleGeneration;
|
|
1935
|
+
const operation = async () => {
|
|
1936
|
+
if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
|
|
1937
|
+
throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
|
|
1938
|
+
}
|
|
1939
|
+
const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
|
|
1940
|
+
if (!AudioContextCtor) {
|
|
1941
|
+
throw new Error("Browser microphone capture requires AudioContext support.");
|
|
1942
|
+
}
|
|
1943
|
+
let pendingAudioContext = null;
|
|
1944
|
+
let pendingMediaStream = null;
|
|
1945
|
+
let pendingProcessorNode = null;
|
|
1946
|
+
let pendingSourceNode = null;
|
|
1947
|
+
try {
|
|
1948
|
+
pendingMediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
|
|
1949
|
+
audio: {
|
|
1950
|
+
autoGainControl: true,
|
|
1951
|
+
channelCount: options.channelCount ?? 1,
|
|
1952
|
+
echoCancellation: true,
|
|
1953
|
+
noiseSuppression: true
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
if (generation !== lifecycleGeneration) {
|
|
1957
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
1905
1958
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1959
|
+
pendingAudioContext = new AudioContextCtor;
|
|
1960
|
+
if (pendingAudioContext.state === "suspended") {
|
|
1961
|
+
await pendingAudioContext.resume();
|
|
1962
|
+
}
|
|
1963
|
+
if (generation !== lifecycleGeneration) {
|
|
1964
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
1965
|
+
}
|
|
1966
|
+
pendingSourceNode = pendingAudioContext.createMediaStreamSource(pendingMediaStream);
|
|
1967
|
+
pendingProcessorNode = pendingAudioContext.createScriptProcessor(4096, 1, 1);
|
|
1968
|
+
pendingProcessorNode.onaudioprocess = (event) => {
|
|
1969
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
1970
|
+
const downsampled = downsampleBuffer(channel, pendingAudioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
|
|
1971
|
+
const pcm = floatTo16BitPCM(downsampled);
|
|
1972
|
+
options.onLevel?.(getPcmLevel(pcm));
|
|
1973
|
+
options.onAudio(pcm);
|
|
1974
|
+
};
|
|
1975
|
+
pendingSourceNode.connect(pendingProcessorNode);
|
|
1976
|
+
pendingProcessorNode.connect(pendingAudioContext.destination);
|
|
1977
|
+
if (generation !== lifecycleGeneration) {
|
|
1978
|
+
throw new Error("Microphone capture stopped during startup.");
|
|
1979
|
+
}
|
|
1980
|
+
audioContext = pendingAudioContext;
|
|
1981
|
+
mediaStream = pendingMediaStream;
|
|
1982
|
+
processorNode = pendingProcessorNode;
|
|
1983
|
+
sourceNode = pendingSourceNode;
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
release({
|
|
1986
|
+
audioContext: pendingAudioContext,
|
|
1987
|
+
mediaStream: pendingMediaStream,
|
|
1988
|
+
processorNode: pendingProcessorNode,
|
|
1989
|
+
sourceNode: pendingSourceNode
|
|
1990
|
+
});
|
|
1991
|
+
throw error;
|
|
1910
1992
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
options.onAudio(pcm);
|
|
1919
|
-
};
|
|
1920
|
-
sourceNode.connect(processorNode);
|
|
1921
|
-
processorNode.connect(audioContext.destination);
|
|
1922
|
-
} catch (error) {
|
|
1923
|
-
stop();
|
|
1924
|
-
throw error;
|
|
1925
|
-
}
|
|
1993
|
+
};
|
|
1994
|
+
const pending = operation().finally(() => {
|
|
1995
|
+
if (startPromise === pending)
|
|
1996
|
+
startPromise = null;
|
|
1997
|
+
});
|
|
1998
|
+
startPromise = pending;
|
|
1999
|
+
return pending;
|
|
1926
2000
|
};
|
|
1927
2001
|
const stop = () => {
|
|
1928
|
-
|
|
1929
|
-
sourceNode
|
|
1930
|
-
mediaStream?.getTracks().forEach((track) => track.stop());
|
|
1931
|
-
audioContext?.close();
|
|
2002
|
+
lifecycleGeneration += 1;
|
|
2003
|
+
release({ audioContext, mediaStream, processorNode, sourceNode });
|
|
1932
2004
|
options.onLevel?.(0);
|
|
1933
2005
|
audioContext = null;
|
|
1934
2006
|
mediaStream = null;
|
|
@@ -2284,6 +2356,9 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2284
2356
|
...options.connection
|
|
2285
2357
|
});
|
|
2286
2358
|
let capture = null;
|
|
2359
|
+
let captureGeneration = 0;
|
|
2360
|
+
let disposed = false;
|
|
2361
|
+
let startRecordingPromise = null;
|
|
2287
2362
|
let state = createInitialState3(stream);
|
|
2288
2363
|
const subscribers = new Set;
|
|
2289
2364
|
const notify = () => {
|
|
@@ -2340,6 +2415,7 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2340
2415
|
return capture;
|
|
2341
2416
|
};
|
|
2342
2417
|
const stopRecording = () => {
|
|
2418
|
+
captureGeneration += 1;
|
|
2343
2419
|
capture?.stop();
|
|
2344
2420
|
capture = null;
|
|
2345
2421
|
state = {
|
|
@@ -2349,39 +2425,63 @@ var createVoiceController = (path, options = {}) => {
|
|
|
2349
2425
|
notify();
|
|
2350
2426
|
};
|
|
2351
2427
|
const startRecording = async () => {
|
|
2428
|
+
if (disposed) {
|
|
2429
|
+
throw new Error("Voice controller is closed.");
|
|
2430
|
+
}
|
|
2352
2431
|
if (state.isRecording) {
|
|
2353
2432
|
return;
|
|
2354
2433
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2434
|
+
if (startRecordingPromise)
|
|
2435
|
+
return startRecordingPromise;
|
|
2436
|
+
const generation = ++captureGeneration;
|
|
2437
|
+
const activeCapture = ensureCapture();
|
|
2438
|
+
const operation = (async () => {
|
|
2439
|
+
try {
|
|
2440
|
+
state = {
|
|
2441
|
+
...state,
|
|
2442
|
+
recordingError: null
|
|
2443
|
+
};
|
|
2444
|
+
notify();
|
|
2445
|
+
await activeCapture.start();
|
|
2446
|
+
if (disposed || generation !== captureGeneration || capture !== activeCapture) {
|
|
2447
|
+
activeCapture.stop();
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
state = {
|
|
2451
|
+
...state,
|
|
2452
|
+
isRecording: true
|
|
2453
|
+
};
|
|
2454
|
+
notify();
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
activeCapture.stop();
|
|
2457
|
+
if (generation !== captureGeneration || capture !== activeCapture) {
|
|
2458
|
+
throw error;
|
|
2459
|
+
}
|
|
2460
|
+
capture = null;
|
|
2461
|
+
state = {
|
|
2462
|
+
...state,
|
|
2463
|
+
isRecording: false,
|
|
2464
|
+
recordingError: error instanceof Error ? error.message : String(error)
|
|
2465
|
+
};
|
|
2466
|
+
notify();
|
|
2467
|
+
throw error;
|
|
2468
|
+
}
|
|
2469
|
+
})();
|
|
2470
|
+
const pending = operation.finally(() => {
|
|
2471
|
+
if (startRecordingPromise === pending)
|
|
2472
|
+
startRecordingPromise = null;
|
|
2473
|
+
});
|
|
2474
|
+
startRecordingPromise = pending;
|
|
2475
|
+
return startRecordingPromise;
|
|
2378
2476
|
};
|
|
2379
2477
|
const close = (reason) => {
|
|
2478
|
+
disposed = true;
|
|
2380
2479
|
unsubscribeStream();
|
|
2381
2480
|
stopRecording();
|
|
2382
2481
|
stream.close(reason);
|
|
2383
2482
|
};
|
|
2384
2483
|
const disconnect = () => {
|
|
2484
|
+
disposed = true;
|
|
2385
2485
|
unsubscribeStream();
|
|
2386
2486
|
stopRecording();
|
|
2387
2487
|
stream.disconnect();
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1367,7 +1367,11 @@ export type VoiceConnectionOptions = {
|
|
|
1367
1367
|
path: string;
|
|
1368
1368
|
scenarioId: string | null;
|
|
1369
1369
|
sessionId: string;
|
|
1370
|
+
signal: AbortSignal;
|
|
1370
1371
|
}) => Promise<void> | void;
|
|
1372
|
+
/** Maximum time allowed for one prepareReconnect hook. A hook that exceeds
|
|
1373
|
+
* this deadline is aborted and consumes the attempt. Default 10000. */
|
|
1374
|
+
prepareReconnectTimeoutMs?: number;
|
|
1371
1375
|
pingInterval?: number;
|
|
1372
1376
|
/** Additional query values sent on every initial or reconnecting socket. */
|
|
1373
1377
|
query?: Record<string, string>;
|