@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/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
- await options.prepareReconnect?.({
11022
- attempt: state.reconnectAttempts,
11023
- path,
11024
- scenarioId: state.scenarioId,
11025
- sessionId: state.sessionId
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
- const start = async () => {
11757
- if (!options.stream && (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)) {
11758
- throw new Error("Browser microphone capture requires navigator.mediaDevices.getUserMedia.");
11759
- }
11760
- const AudioContextCtor = (typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : undefined) ?? AudioContext;
11761
- if (!AudioContextCtor) {
11762
- throw new Error("Browser microphone capture requires AudioContext support.");
11763
- }
11764
- try {
11765
- mediaStream = options.stream ?? await navigator.mediaDevices.getUserMedia({
11766
- audio: {
11767
- autoGainControl: true,
11768
- channelCount: options.channelCount ?? 1,
11769
- echoCancellation: true,
11770
- noiseSuppression: true
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
- audioContext = new AudioContextCtor;
11774
- if (audioContext.state === "suspended") {
11775
- await audioContext.resume();
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
- sourceNode = audioContext.createMediaStreamSource(mediaStream);
11778
- processorNode = audioContext.createScriptProcessor(4096, 1, 1);
11779
- processorNode.onaudioprocess = (event) => {
11780
- const channel = event.inputBuffer.getChannelData(0);
11781
- const downsampled = downsampleBuffer(channel, audioContext?.sampleRate ?? 48000, options.sampleRateHz ?? 16000);
11782
- const pcm = floatTo16BitPCM(downsampled);
11783
- options.onLevel?.(getPcmLevel(pcm));
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
- processorNode?.disconnect();
11795
- sourceNode?.disconnect();
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
- try {
12222
- state = {
12223
- ...state,
12224
- recordingError: null
12225
- };
12226
- notify();
12227
- await ensureCapture().start();
12228
- state = {
12229
- ...state,
12230
- isRecording: true
12231
- };
12232
- notify();
12233
- } catch (error) {
12234
- capture?.stop();
12235
- capture = null;
12236
- state = {
12237
- ...state,
12238
- isRecording: false,
12239
- recordingError: error instanceof Error ? error.message : String(error)
12240
- };
12241
- notify();
12242
- throw error;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.656",
3
+ "version": "0.0.22-beta.658",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",