@layercode/js-sdk 2.8.2 → 2.8.4

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.
@@ -5088,11 +5088,7 @@ registerProcessor('audio_processor', AudioProcessor);
5088
5088
  * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
5089
5089
  * @returns {WavRecorder}
5090
5090
  */
5091
- constructor({
5092
- sampleRate = 24000,
5093
- outputToSpeakers = false,
5094
- debug = false,
5095
- } = {}) {
5091
+ constructor({ sampleRate = 24000, outputToSpeakers = false, debug = false } = {}) {
5096
5092
  // Script source
5097
5093
  this.scriptSrc = AudioProcessorSrc;
5098
5094
  // Config
@@ -5110,6 +5106,11 @@ registerProcessor('audio_processor', AudioProcessor);
5110
5106
  this.analyser = null;
5111
5107
  this.recording = false;
5112
5108
  this.contextSampleRate = sampleRate;
5109
+ // Track whether we've already obtained microphone permission
5110
+ // This avoids redundant getUserMedia calls which are expensive on iOS Safari
5111
+ this._hasPermission = false;
5112
+ // Promise used to dedupe concurrent requestPermission() calls
5113
+ this._permissionPromise = null;
5113
5114
  // Event handling with AudioWorklet
5114
5115
  this._lastEventId = 0;
5115
5116
  this.eventReceipts = {};
@@ -5137,17 +5138,13 @@ registerProcessor('audio_processor', AudioProcessor);
5137
5138
  let blob;
5138
5139
  if (audioData instanceof Blob) {
5139
5140
  if (fromSampleRate !== -1) {
5140
- throw new Error(
5141
- `Can not specify "fromSampleRate" when reading from Blob`,
5142
- );
5141
+ throw new Error(`Can not specify "fromSampleRate" when reading from Blob`);
5143
5142
  }
5144
5143
  blob = audioData;
5145
5144
  arrayBuffer = await blob.arrayBuffer();
5146
5145
  } else if (audioData instanceof ArrayBuffer) {
5147
5146
  if (fromSampleRate !== -1) {
5148
- throw new Error(
5149
- `Can not specify "fromSampleRate" when reading from ArrayBuffer`,
5150
- );
5147
+ throw new Error(`Can not specify "fromSampleRate" when reading from ArrayBuffer`);
5151
5148
  }
5152
5149
  arrayBuffer = audioData;
5153
5150
  blob = new Blob([arrayBuffer], { type: 'audio/wav' });
@@ -5165,14 +5162,10 @@ registerProcessor('audio_processor', AudioProcessor);
5165
5162
  } else if (audioData instanceof Array) {
5166
5163
  float32Array = new Float32Array(audioData);
5167
5164
  } else {
5168
- throw new Error(
5169
- `"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`,
5170
- );
5165
+ throw new Error(`"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`);
5171
5166
  }
5172
5167
  if (fromSampleRate === -1) {
5173
- throw new Error(
5174
- `Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`,
5175
- );
5168
+ throw new Error(`Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`);
5176
5169
  } else if (fromSampleRate < 3000) {
5177
5170
  throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`);
5178
5171
  }
@@ -5202,12 +5195,13 @@ registerProcessor('audio_processor', AudioProcessor);
5202
5195
 
5203
5196
  /**
5204
5197
  * Logs data in debug mode
5205
- * @param {...any} arguments
5198
+ * @param {...any} args
5206
5199
  * @returns {true}
5207
5200
  */
5208
- log() {
5201
+ log(...args) {
5209
5202
  if (this.debug) {
5210
- this.log(...arguments);
5203
+ // eslint-disable-next-line no-console
5204
+ console.log(...args);
5211
5205
  }
5212
5206
  return true;
5213
5207
  }
@@ -5280,10 +5274,7 @@ registerProcessor('audio_processor', AudioProcessor);
5280
5274
  */
5281
5275
  listenForDeviceChange(callback) {
5282
5276
  if (callback === null && this._deviceChangeCallback) {
5283
- navigator.mediaDevices.removeEventListener(
5284
- 'devicechange',
5285
- this._deviceChangeCallback,
5286
- );
5277
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeCallback);
5287
5278
  this._deviceChangeCallback = null;
5288
5279
  } else if (callback !== null) {
5289
5280
  // Basically a debounce; we only want this called once when devices change
@@ -5315,19 +5306,39 @@ registerProcessor('audio_processor', AudioProcessor);
5315
5306
 
5316
5307
  /**
5317
5308
  * Manually request permission to use the microphone
5309
+ * Skips if permission has already been granted to avoid expensive redundant getUserMedia calls.
5310
+ * Dedupes concurrent calls to prevent multiple getUserMedia requests.
5318
5311
  * @returns {Promise<true>}
5319
5312
  */
5320
5313
  async requestPermission() {
5321
- try {
5322
- console.log('ensureUserMediaAccess');
5323
- await navigator.mediaDevices.getUserMedia({
5324
- audio: true,
5325
- });
5326
- } catch (fallbackError) {
5327
- window.alert('You must grant microphone access to use this feature.');
5328
- throw fallbackError;
5314
+ // Skip if we already have permission - each getUserMedia is expensive on iOS Safari
5315
+ if (this._hasPermission) {
5316
+ return true;
5329
5317
  }
5330
- return true;
5318
+ // Dedupe concurrent calls: if a permission request is already in flight, wait for it
5319
+ if (this._permissionPromise) {
5320
+ return this._permissionPromise;
5321
+ }
5322
+
5323
+ console.log('ensureUserMediaAccess');
5324
+ this._permissionPromise = (async () => {
5325
+ try {
5326
+ const stream = await navigator.mediaDevices.getUserMedia({
5327
+ audio: true,
5328
+ });
5329
+ // Stop the tracks immediately after getting permission
5330
+ stream.getTracks().forEach((track) => track.stop());
5331
+ this._hasPermission = true;
5332
+ return true;
5333
+ } catch (fallbackError) {
5334
+ console.error('getUserMedia failed:', fallbackError.name, fallbackError.message);
5335
+ throw fallbackError;
5336
+ } finally {
5337
+ this._permissionPromise = null;
5338
+ }
5339
+ })();
5340
+
5341
+ return this._permissionPromise;
5331
5342
  }
5332
5343
 
5333
5344
  /**
@@ -5335,25 +5346,18 @@ registerProcessor('audio_processor', AudioProcessor);
5335
5346
  * @returns {Promise<Array<MediaDeviceInfo & {default: boolean}>>}
5336
5347
  */
5337
5348
  async listDevices() {
5338
- if (
5339
- !navigator.mediaDevices ||
5340
- !('enumerateDevices' in navigator.mediaDevices)
5341
- ) {
5349
+ if (!navigator.mediaDevices || !('enumerateDevices' in navigator.mediaDevices)) {
5342
5350
  throw new Error('Could not request user devices');
5343
5351
  }
5344
5352
  await this.requestPermission();
5345
5353
 
5346
5354
  const devices = await navigator.mediaDevices.enumerateDevices();
5347
5355
  const audioDevices = devices.filter((device) => device.kind === 'audioinput');
5348
- const defaultDeviceIndex = audioDevices.findIndex(
5349
- (device) => device.deviceId === 'default',
5350
- );
5356
+ const defaultDeviceIndex = audioDevices.findIndex((device) => device.deviceId === 'default');
5351
5357
  const deviceList = [];
5352
5358
  if (defaultDeviceIndex !== -1) {
5353
5359
  let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0];
5354
- let existingIndex = audioDevices.findIndex(
5355
- (device) => device.groupId === defaultDevice.groupId,
5356
- );
5360
+ let existingIndex = audioDevices.findIndex((device) => device.groupId === defaultDevice.groupId);
5357
5361
  if (existingIndex !== -1) {
5358
5362
  defaultDevice = audioDevices.splice(existingIndex, 1)[0];
5359
5363
  }
@@ -5375,15 +5379,10 @@ registerProcessor('audio_processor', AudioProcessor);
5375
5379
  */
5376
5380
  async begin(deviceId) {
5377
5381
  if (this.processor) {
5378
- throw new Error(
5379
- `Already connected: please call .end() to start a new session`,
5380
- );
5382
+ throw new Error(`Already connected: please call .end() to start a new session`);
5381
5383
  }
5382
5384
 
5383
- if (
5384
- !navigator.mediaDevices ||
5385
- !('getUserMedia' in navigator.mediaDevices)
5386
- ) {
5385
+ if (!navigator.mediaDevices || !('getUserMedia' in navigator.mediaDevices)) {
5387
5386
  throw new Error('Could not request user media');
5388
5387
  }
5389
5388
  try {
@@ -5394,14 +5393,16 @@ registerProcessor('audio_processor', AudioProcessor);
5394
5393
  echoCancellation: true,
5395
5394
  autoGainControl: true,
5396
5395
  noiseSuppression: true,
5397
- }
5396
+ },
5398
5397
  };
5399
5398
  if (deviceId) {
5400
5399
  config.audio.deviceId = { exact: deviceId };
5401
5400
  }
5402
5401
  this.stream = await navigator.mediaDevices.getUserMedia(config);
5402
+ // Mark permission as granted so listDevices() won't call requestPermission() again
5403
+ this._hasPermission = true;
5403
5404
  } catch (err) {
5404
- throw new Error('Could not start media stream');
5405
+ throw err;
5405
5406
  }
5406
5407
 
5407
5408
  const createContext = (rate) => {
@@ -5453,10 +5454,7 @@ registerProcessor('audio_processor', AudioProcessor);
5453
5454
  raw: WavPacker.mergeBuffers(buffer.raw, data.raw),
5454
5455
  mono: WavPacker.mergeBuffers(buffer.mono, data.mono),
5455
5456
  };
5456
- if (
5457
- this._chunkProcessorBuffer.mono.byteLength >=
5458
- this._chunkProcessorSize
5459
- ) {
5457
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
5460
5458
  this._chunkProcessor(this._chunkProcessorBuffer);
5461
5459
  this._chunkProcessorBuffer = {
5462
5460
  raw: new ArrayBuffer(0),
@@ -5484,11 +5482,7 @@ registerProcessor('audio_processor', AudioProcessor);
5484
5482
  node.connect(analyser);
5485
5483
  if (this.outputToSpeakers) {
5486
5484
  // eslint-disable-next-line no-console
5487
- console.warn(
5488
- 'Warning: Output to speakers may affect sound quality,\n' +
5489
- 'especially due to system audio feedback preventative measures.\n' +
5490
- 'use only for debugging',
5491
- );
5485
+ console.warn('Warning: Output to speakers may affect sound quality,\n' + 'especially due to system audio feedback preventative measures.\n' + 'use only for debugging');
5492
5486
  analyser.connect(context.destination);
5493
5487
  }
5494
5488
 
@@ -5515,26 +5509,14 @@ registerProcessor('audio_processor', AudioProcessor);
5515
5509
  * @param {number} [maxDecibels] default -30
5516
5510
  * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
5517
5511
  */
5518
- getFrequencies(
5519
- analysisType = 'frequency',
5520
- minDecibels = -100,
5521
- maxDecibels = -30,
5522
- ) {
5512
+ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
5523
5513
  if (!this.processor) {
5524
5514
  throw new Error('Session ended: please call .begin() first');
5525
5515
  }
5526
- return AudioAnalysis.getFrequencies(
5527
- this.analyser,
5528
- this.sampleRate,
5529
- null,
5530
- analysisType,
5531
- minDecibels,
5532
- maxDecibels,
5533
- );
5516
+ return AudioAnalysis.getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
5534
5517
  }
5535
5518
 
5536
-
5537
- /**
5519
+ /**
5538
5520
  * Gets the real-time amplitude of the audio signal
5539
5521
  * @returns {number} Amplitude value between 0 and 1
5540
5522
  */
@@ -5659,9 +5641,7 @@ registerProcessor('audio_processor', AudioProcessor);
5659
5641
  throw new Error('Session ended: please call .begin() first');
5660
5642
  }
5661
5643
  if (!force && this.recording) {
5662
- throw new Error(
5663
- 'Currently recording: please call .pause() first, or call .save(true) to force',
5664
- );
5644
+ throw new Error('Currently recording: please call .pause() first, or call .save(true) to force');
5665
5645
  }
5666
5646
  this.log('Exporting ...');
5667
5647
  const exportData = await this._event('export');
@@ -5768,6 +5748,7 @@ registerProcessor('audio_processor', AudioProcessor);
5768
5748
  return btoa(binary);
5769
5749
  }
5770
5750
 
5751
+ //// src/index.ts
5771
5752
  /* eslint-env browser */
5772
5753
  // import { env as ortEnv } from 'onnxruntime-web';
5773
5754
  // @ts-ignore - VAD package does not provide TypeScript types
@@ -5775,137 +5756,40 @@ registerProcessor('audio_processor', AudioProcessor);
5775
5756
  const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
5776
5757
  // SDK version - updated when publishing
5777
5758
  const SDK_VERSION = '2.7.0';
5778
- const MEDIA_DEVICE_CHANGE_EVENT = 'devicechange';
5779
- const MEDIA_DEVICE_KIND_AUDIO = 'audioinput';
5759
+ const DEFAULT_RECORDER_SAMPLE_RATE = 8000;
5780
5760
  const hasMediaDevicesSupport = () => typeof navigator !== 'undefined' && !!navigator.mediaDevices;
5781
- let microphonePermissionPromise = null;
5782
- let microphonePermissionGranted = false;
5783
- const stopStreamTracks = (stream) => {
5784
- if (!stream) {
5785
- return;
5786
- }
5787
- stream.getTracks().forEach((track) => {
5788
- try {
5789
- track.stop();
5790
- }
5791
- catch (_a) {
5792
- /* noop */
5793
- }
5794
- });
5795
- };
5796
- const ensureMicrophonePermissions = async () => {
5797
- if (!hasMediaDevicesSupport()) {
5798
- throw new Error('Media devices are not available in this environment');
5799
- }
5800
- if (microphonePermissionGranted) {
5801
- return;
5802
- }
5803
- if (!microphonePermissionPromise) {
5804
- microphonePermissionPromise = navigator.mediaDevices
5805
- .getUserMedia({ audio: true })
5806
- .then((stream) => {
5807
- microphonePermissionGranted = true;
5808
- stopStreamTracks(stream);
5809
- })
5810
- .finally(() => {
5811
- microphonePermissionPromise = null;
5812
- });
5813
- }
5814
- return microphonePermissionPromise;
5815
- };
5816
- const cloneAudioDevice = (device, isDefault) => {
5761
+ const toLayercodeAudioInputDevice = (device) => {
5817
5762
  const cloned = {
5818
- deviceId: device.deviceId,
5819
- groupId: device.groupId,
5820
- kind: device.kind,
5763
+ ...device,
5821
5764
  label: device.label,
5822
- default: isDefault,
5765
+ default: Boolean(device.default),
5823
5766
  };
5824
5767
  if (typeof device.toJSON === 'function') {
5825
5768
  cloned.toJSON = device.toJSON.bind(device);
5826
5769
  }
5827
5770
  return cloned;
5828
5771
  };
5829
- const normalizeAudioInputDevices = (devices) => {
5830
- const audioDevices = devices.filter((device) => device.kind === MEDIA_DEVICE_KIND_AUDIO);
5831
- if (!audioDevices.length) {
5832
- return [];
5833
- }
5834
- const remaining = [...audioDevices];
5835
- const normalized = [];
5836
- const defaultIndex = remaining.findIndex((device) => device.deviceId === 'default');
5837
- if (defaultIndex !== -1) {
5838
- let defaultDevice = remaining.splice(defaultIndex, 1)[0];
5839
- const groupMatchIndex = remaining.findIndex((device) => device.groupId && defaultDevice.groupId && device.groupId === defaultDevice.groupId);
5840
- if (groupMatchIndex !== -1) {
5841
- defaultDevice = remaining.splice(groupMatchIndex, 1)[0];
5842
- }
5843
- normalized.push(cloneAudioDevice(defaultDevice, true));
5844
- }
5845
- else if (remaining.length) {
5846
- const fallbackDefault = remaining.shift();
5847
- normalized.push(cloneAudioDevice(fallbackDefault, true));
5848
- }
5849
- return normalized.concat(remaining.map((device) => cloneAudioDevice(device, false)));
5850
- };
5851
5772
  const listAudioInputDevices = async () => {
5852
5773
  if (!hasMediaDevicesSupport()) {
5853
5774
  throw new Error('Media devices are not available in this environment');
5854
5775
  }
5855
- await ensureMicrophonePermissions();
5856
- const devices = await navigator.mediaDevices.enumerateDevices();
5857
- return normalizeAudioInputDevices(devices);
5776
+ const recorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE });
5777
+ const devices = (await recorder.listDevices());
5778
+ return devices.map(toLayercodeAudioInputDevice);
5858
5779
  };
5859
5780
  const watchAudioInputDevices = (callback) => {
5860
5781
  if (!hasMediaDevicesSupport()) {
5861
5782
  return () => { };
5862
5783
  }
5863
- let disposed = false;
5864
- let lastSignature = null;
5865
- let requestId = 0;
5866
- const emitDevices = async () => {
5867
- requestId += 1;
5868
- const currentRequest = requestId;
5869
- try {
5870
- const devices = await listAudioInputDevices();
5871
- if (disposed || currentRequest !== requestId) {
5872
- return;
5873
- }
5874
- const signature = devices.map((device) => `${device.deviceId}:${device.label}:${device.groupId}:${device.default ? '1' : '0'}`).join('|');
5875
- if (signature !== lastSignature) {
5876
- lastSignature = signature;
5877
- callback(devices);
5878
- }
5879
- }
5880
- catch (error) {
5881
- if (!disposed) {
5882
- console.warn('Failed to refresh audio devices', error);
5883
- }
5884
- }
5885
- };
5886
- const handler = () => {
5887
- void emitDevices();
5784
+ const recorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE });
5785
+ const handleDevicesChange = (devices) => {
5786
+ callback(devices.map(toLayercodeAudioInputDevice));
5888
5787
  };
5889
- const mediaDevices = navigator.mediaDevices;
5890
- let teardown = null;
5891
- if (typeof mediaDevices.addEventListener === 'function') {
5892
- mediaDevices.addEventListener(MEDIA_DEVICE_CHANGE_EVENT, handler);
5893
- teardown = () => mediaDevices.removeEventListener(MEDIA_DEVICE_CHANGE_EVENT, handler);
5894
- }
5895
- else if ('ondevicechange' in mediaDevices) {
5896
- const previousHandler = mediaDevices.ondevicechange;
5897
- mediaDevices.ondevicechange = handler;
5898
- teardown = () => {
5899
- if (mediaDevices.ondevicechange === handler) {
5900
- mediaDevices.ondevicechange = previousHandler || null;
5901
- }
5902
- };
5903
- }
5904
- // Always emit once on subscribe
5905
- void emitDevices();
5788
+ // WavRecorder handles initial emit + deduping devicechange events
5789
+ recorder.listenForDeviceChange(handleDevicesChange);
5906
5790
  return () => {
5907
- disposed = true;
5908
- teardown === null || teardown === void 0 ? void 0 : teardown();
5791
+ recorder.listenForDeviceChange(null);
5792
+ recorder.quit().catch(() => { });
5909
5793
  };
5910
5794
  };
5911
5795
  /**
@@ -5952,7 +5836,7 @@ registerProcessor('audio_processor', AudioProcessor);
5952
5836
  this.AMPLITUDE_MONITORING_SAMPLE_RATE = 2;
5953
5837
  this._websocketUrl = DEFAULT_WS_URL;
5954
5838
  this.audioOutputReady = null;
5955
- this.wavRecorder = new WavRecorder({ sampleRate: 8000 }); // TODO should be set my fetched agent config
5839
+ this.wavRecorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE }); // TODO should be set by fetched agent config
5956
5840
  this.wavPlayer = new WavStreamPlayer({
5957
5841
  finishedPlayingCallback: this._clientResponseAudioReplayFinished.bind(this),
5958
5842
  sampleRate: 16000, // TODO should be set my fetched agent config
@@ -5972,6 +5856,7 @@ registerProcessor('audio_processor', AudioProcessor);
5972
5856
  this.recorderStarted = false;
5973
5857
  this.readySent = false;
5974
5858
  this.currentTurnId = null;
5859
+ this.sentReplayFinishedForDisabledOutput = false;
5975
5860
  this.audioBuffer = [];
5976
5861
  this.vadConfig = null;
5977
5862
  this.activeDeviceId = null;
@@ -5983,6 +5868,7 @@ registerProcessor('audio_processor', AudioProcessor);
5983
5868
  this.stopRecorderAmplitude = undefined;
5984
5869
  this.deviceChangeListener = null;
5985
5870
  this.recorderRestartChain = Promise.resolve();
5871
+ this._skipFirstDeviceCallback = false;
5986
5872
  this.deviceListenerReady = null;
5987
5873
  this.resolveDeviceListenerReady = null;
5988
5874
  // this.audioPauseTime = null;
@@ -6002,7 +5888,7 @@ registerProcessor('audio_processor', AudioProcessor);
6002
5888
  set onDevicesChanged(callback) {
6003
5889
  this.options.onDevicesChanged = callback !== null && callback !== void 0 ? callback : NOOP;
6004
5890
  }
6005
- _initializeVAD() {
5891
+ async _initializeVAD() {
6006
5892
  var _a;
6007
5893
  console.log('initializing VAD', { pushToTalkEnabled: this.pushToTalkEnabled, canInterrupt: this.canInterrupt, vadConfig: this.vadConfig });
6008
5894
  // If we're in push to talk mode or mute mode, we don't need to use the VAD model
@@ -6086,13 +5972,13 @@ registerProcessor('audio_processor', AudioProcessor);
6086
5972
  vadOptions.frameSamples = 512; // Required for v5
6087
5973
  }
6088
5974
  console.log('Creating VAD with options:', vadOptions);
6089
- dist.MicVAD.new(vadOptions)
6090
- .then((vad) => {
5975
+ try {
5976
+ const vad = await dist.MicVAD.new(vadOptions);
6091
5977
  this.vad = vad;
6092
5978
  this.vad.start();
6093
5979
  console.log('VAD started successfully');
6094
- })
6095
- .catch((error) => {
5980
+ }
5981
+ catch (error) {
6096
5982
  console.warn('Error initializing VAD:', error);
6097
5983
  // Send a message to server indicating VAD failure
6098
5984
  const vadFailureMessage = {
@@ -6104,7 +5990,7 @@ registerProcessor('audio_processor', AudioProcessor);
6104
5990
  ...vadFailureMessage,
6105
5991
  userSpeaking: this.userIsSpeaking,
6106
5992
  });
6107
- });
5993
+ }
6108
5994
  }
6109
5995
  /**
6110
5996
  * Updates the connection status and triggers the callback
@@ -6131,11 +6017,14 @@ registerProcessor('audio_processor', AudioProcessor);
6131
6017
  this.options.onAgentSpeakingChange(shouldReportSpeaking);
6132
6018
  }
6133
6019
  _setUserSpeaking(isSpeaking) {
6134
- const shouldReportSpeaking = this._shouldCaptureUserAudio() && isSpeaking;
6020
+ const shouldCapture = this._shouldCaptureUserAudio();
6021
+ const shouldReportSpeaking = shouldCapture && isSpeaking;
6022
+ console.log('_setUserSpeaking called:', isSpeaking, 'shouldCapture:', shouldCapture, 'shouldReportSpeaking:', shouldReportSpeaking, 'current userIsSpeaking:', this.userIsSpeaking);
6135
6023
  if (this.userIsSpeaking === shouldReportSpeaking) {
6136
6024
  return;
6137
6025
  }
6138
6026
  this.userIsSpeaking = shouldReportSpeaking;
6027
+ console.log('_setUserSpeaking: updated userIsSpeaking to:', this.userIsSpeaking);
6139
6028
  this.options.onUserIsSpeakingChange(shouldReportSpeaking);
6140
6029
  }
6141
6030
  /**
@@ -6185,6 +6074,7 @@ registerProcessor('audio_processor', AudioProcessor);
6185
6074
  * @param {MessageEvent} event - The WebSocket message event
6186
6075
  */
6187
6076
  async _handleWebSocketMessage(event) {
6077
+ var _a, _b;
6188
6078
  try {
6189
6079
  const message = JSON.parse(event.data);
6190
6080
  if (message.type !== 'response.audio') {
@@ -6197,6 +6087,20 @@ registerProcessor('audio_processor', AudioProcessor);
6197
6087
  // Start tracking new agent turn
6198
6088
  console.debug('Agent turn started, will track new turn ID from audio/text');
6199
6089
  this._setUserSpeaking(false);
6090
+ // Reset the flag for the new assistant turn
6091
+ this.sentReplayFinishedForDisabledOutput = false;
6092
+ // When assistant's turn starts but we're not playing audio,
6093
+ // we need to tell the server we're "done" with playback so it can
6094
+ // transition the turn back to user. Use a small delay to let any
6095
+ // response.audio/response.end messages arrive first.
6096
+ if (!this.audioOutput) {
6097
+ setTimeout(() => {
6098
+ if (!this.audioOutput && !this.sentReplayFinishedForDisabledOutput) {
6099
+ this.sentReplayFinishedForDisabledOutput = true;
6100
+ this._clientResponseAudioReplayFinished();
6101
+ }
6102
+ }, 1000);
6103
+ }
6200
6104
  }
6201
6105
  else if (message.role === 'user' && !this.pushToTalkEnabled) {
6202
6106
  // Interrupt any playing agent audio if this is a turn triggered by the server (and not push to talk, which will have already called interrupt)
@@ -6216,7 +6120,25 @@ registerProcessor('audio_processor', AudioProcessor);
6216
6120
  });
6217
6121
  break;
6218
6122
  }
6123
+ case 'response.end': {
6124
+ // When audioOutput is disabled, notify server that "playback" is complete
6125
+ if (!this.audioOutput && !this.sentReplayFinishedForDisabledOutput) {
6126
+ this.sentReplayFinishedForDisabledOutput = true;
6127
+ this._clientResponseAudioReplayFinished();
6128
+ }
6129
+ (_b = (_a = this.options).onMessage) === null || _b === void 0 ? void 0 : _b.call(_a, message);
6130
+ break;
6131
+ }
6219
6132
  case 'response.audio': {
6133
+ // Skip audio playback if audioOutput is disabled
6134
+ if (!this.audioOutput) {
6135
+ // Send replay_finished so server knows we're "done" with playback (only once per turn)
6136
+ if (!this.sentReplayFinishedForDisabledOutput) {
6137
+ this.sentReplayFinishedForDisabledOutput = true;
6138
+ this._clientResponseAudioReplayFinished();
6139
+ }
6140
+ break;
6141
+ }
6220
6142
  await this._waitForAudioOutputReady();
6221
6143
  const audioBuffer = base64ToArrayBuffer(message.content);
6222
6144
  const hasAudioSamples = audioBuffer.byteLength > 0;
@@ -6351,6 +6273,9 @@ registerProcessor('audio_processor', AudioProcessor);
6351
6273
  }
6352
6274
  _sendReadyIfNeeded() {
6353
6275
  var _a;
6276
+ // Send client.ready when either:
6277
+ // 1. Recorder is started (audio mode active)
6278
+ // 2. audioInput is false (text-only mode, but server should still be ready)
6354
6279
  const audioReady = this.recorderStarted || !this.audioInput;
6355
6280
  if (audioReady && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.readySent) {
6356
6281
  this._wsSend({ type: 'client.ready' });
@@ -6416,14 +6341,99 @@ registerProcessor('audio_processor', AudioProcessor);
6416
6341
  }
6417
6342
  async audioInputConnect() {
6418
6343
  // Turn mic ON
6419
- await this.wavRecorder.requestPermission();
6344
+ // NOTE: On iOS Safari, each getUserMedia call is expensive (~2-3 seconds).
6345
+ // We optimize by:
6346
+ // 1. Starting the recorder FIRST with begin() (single getUserMedia)
6347
+ // 2. THEN setting up device change listeners (which will skip getUserMedia since permission is cached)
6348
+ console.log('audioInputConnect: recorderStarted =', this.recorderStarted);
6349
+ // If the recorder hasn't spun up yet, start it first with the preferred or default device
6350
+ // This ensures we only make ONE getUserMedia call instead of multiple sequential ones
6351
+ if (!this.recorderStarted) {
6352
+ // Use preferred device if set, otherwise use system default
6353
+ const targetDeviceId = this.useSystemDefaultDevice ? undefined : this.deviceId || undefined;
6354
+ // Mark as using system default if no specific device is set
6355
+ if (!targetDeviceId) {
6356
+ this.useSystemDefaultDevice = true;
6357
+ }
6358
+ console.log('audioInputConnect: starting recorder with device:', targetDeviceId !== null && targetDeviceId !== void 0 ? targetDeviceId : 'system default');
6359
+ await this._startRecorderWithDevice(targetDeviceId);
6360
+ }
6361
+ // Now set up device change listeners - permission is already granted so listDevices() won't call getUserMedia
6362
+ // Skip the first callback since we've already started with the correct device
6363
+ this._skipFirstDeviceCallback = true;
6364
+ console.log('audioInputConnect: setting up device change listener');
6420
6365
  await this._setupDeviceChangeListener();
6421
- // If the recorder hasn't spun up yet, proactively select a device.
6422
- if (!this.recorderStarted && this.deviceChangeListener) {
6423
- await this._initializeRecorderWithDefaultDevice();
6366
+ console.log('audioInputConnect: done, recorderStarted =', this.recorderStarted);
6367
+ }
6368
+ /**
6369
+ * Starts the recorder with a specific device (or default if undefined)
6370
+ * This is the single point where getUserMedia is called during initial setup.
6371
+ * Idempotent: returns early if recorder is already started or has a live stream.
6372
+ */
6373
+ async _startRecorderWithDevice(deviceId) {
6374
+ var _a, _b;
6375
+ // Idempotency guard: don't start again if already running
6376
+ if (this.recorderStarted || this._hasLiveRecorderStream()) {
6377
+ console.debug('_startRecorderWithDevice: already started, skipping');
6378
+ return;
6379
+ }
6380
+ try {
6381
+ this._stopRecorderAmplitudeMonitoring();
6382
+ try {
6383
+ await this.wavRecorder.end();
6384
+ }
6385
+ catch (_c) {
6386
+ // Ignore cleanup errors
6387
+ }
6388
+ await this.wavRecorder.begin(deviceId);
6389
+ await this.wavRecorder.record(this._handleDataAvailable, 1638);
6390
+ // Re-setup amplitude monitoring with the new stream
6391
+ this._setupAmplitudeMonitoring(this.wavRecorder, this.options.onUserAmplitudeChange, (amp) => (this.userAudioAmplitude = amp));
6392
+ if (!this.options.enableAmplitudeMonitoring) {
6393
+ this.userAudioAmplitude = 0;
6394
+ }
6395
+ const stream = this.wavRecorder.getStream();
6396
+ const activeTrack = (stream === null || stream === void 0 ? void 0 : stream.getAudioTracks()[0]) || null;
6397
+ const trackSettings = activeTrack && typeof activeTrack.getSettings === 'function' ? activeTrack.getSettings() : null;
6398
+ const trackDeviceId = trackSettings && typeof trackSettings.deviceId === 'string' ? trackSettings.deviceId : null;
6399
+ this.activeDeviceId = trackDeviceId !== null && trackDeviceId !== void 0 ? trackDeviceId : (this.useSystemDefaultDevice ? null : this.deviceId);
6400
+ if (!this.recorderStarted) {
6401
+ this.recorderStarted = true;
6402
+ this._sendReadyIfNeeded();
6403
+ }
6404
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
6405
+ if (reportedDeviceId !== this.lastReportedDeviceId) {
6406
+ this.lastReportedDeviceId = reportedDeviceId;
6407
+ if (this.options.onDeviceSwitched) {
6408
+ this.options.onDeviceSwitched(reportedDeviceId);
6409
+ }
6410
+ }
6411
+ console.debug('Recorder started successfully with device:', reportedDeviceId);
6412
+ }
6413
+ catch (error) {
6414
+ const permissionDeniedError = await this._microphonePermissionDeniedError(error);
6415
+ if (permissionDeniedError) {
6416
+ console.error(permissionDeniedError.message);
6417
+ this.options.onError(permissionDeniedError);
6418
+ throw permissionDeniedError;
6419
+ }
6420
+ if (await this._shouldWarnAudioDevicesRequireUserGesture(error)) {
6421
+ console.error('Cannot load audio devices before user has interacted with the page. Please move connect() to be triggered by a button, or load the SDK with "audioInput: false" to connection() on page load');
6422
+ }
6423
+ console.error('Error starting recorder:', error);
6424
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
6425
+ throw error;
6424
6426
  }
6425
6427
  }
6426
6428
  async audioInputDisconnect() {
6429
+ // If we never started the recorder, avoid touching audio APIs at all.
6430
+ if (!this.recorderStarted && !this._hasLiveRecorderStream()) {
6431
+ this._stopRecorderAmplitudeMonitoring();
6432
+ this.stopVad();
6433
+ this._teardownDeviceListeners();
6434
+ this.recorderStarted = false;
6435
+ return;
6436
+ }
6427
6437
  try {
6428
6438
  // stop amplitude monitoring tied to the recorder
6429
6439
  this._stopRecorderAmplitudeMonitoring();
@@ -6445,7 +6455,9 @@ registerProcessor('audio_processor', AudioProcessor);
6445
6455
  this.audioInput = state;
6446
6456
  this._emitAudioInput();
6447
6457
  if (state) {
6458
+ this._setStatus('connecting');
6448
6459
  await this.audioInputConnect();
6460
+ this._setStatus('connected');
6449
6461
  }
6450
6462
  else {
6451
6463
  await this.audioInputDisconnect();
@@ -6457,7 +6469,20 @@ registerProcessor('audio_processor', AudioProcessor);
6457
6469
  this.audioOutput = state;
6458
6470
  this._emitAudioOutput();
6459
6471
  if (state) {
6460
- this.wavPlayer.unmute();
6472
+ // Initialize audio output if not already connected
6473
+ // This happens when audioOutput was initially false and is now being enabled
6474
+ if (!this.wavPlayer.context) {
6475
+ this._setStatus('connecting');
6476
+ // Store the promise so _waitForAudioOutputReady() can await it
6477
+ // This prevents response.audio from running before AudioContext is ready
6478
+ const setupPromise = this.setupAudioOutput();
6479
+ this.audioOutputReady = setupPromise;
6480
+ await setupPromise;
6481
+ this._setStatus('connected');
6482
+ }
6483
+ else {
6484
+ this.wavPlayer.unmute();
6485
+ }
6461
6486
  // Sync agentSpeaking state with actual playback state when enabling audio output
6462
6487
  this._syncAgentSpeakingState();
6463
6488
  }
@@ -6538,7 +6563,19 @@ registerProcessor('audio_processor', AudioProcessor);
6538
6563
  await audioOutputReady;
6539
6564
  }
6540
6565
  catch (error) {
6541
- console.error('Error connecting to Layercode agent:', error);
6566
+ const permissionDeniedError = await this._microphonePermissionDeniedError(error);
6567
+ if (permissionDeniedError) {
6568
+ console.error(permissionDeniedError.message);
6569
+ this._setStatus('error');
6570
+ this.options.onError(permissionDeniedError);
6571
+ return;
6572
+ }
6573
+ if (await this._shouldWarnAudioDevicesRequireUserGesture(error)) {
6574
+ console.error('Cannot load audio devices before user has interacted with the page. Please move connect() to be triggered by a button, or load the SDK with "audioInput: false" to connection() on page load');
6575
+ }
6576
+ else {
6577
+ console.error('Error connecting to Layercode agent:', error);
6578
+ }
6542
6579
  this._setStatus('error');
6543
6580
  this.options.onError(error instanceof Error ? error : new Error(String(error)));
6544
6581
  }
@@ -6614,6 +6651,11 @@ registerProcessor('audio_processor', AudioProcessor);
6614
6651
  return authorizeSessionResponseBody;
6615
6652
  }
6616
6653
  async setupAudioOutput() {
6654
+ // Only initialize audio player if audioOutput is enabled
6655
+ // This prevents AudioContext creation before user gesture when audio is disabled
6656
+ if (!this.audioOutput) {
6657
+ return;
6658
+ }
6617
6659
  // Initialize audio player
6618
6660
  // wavRecorder will be started from the onDeviceSwitched callback,
6619
6661
  // which is called when the device is first initialized and also when the device is switched
@@ -6624,12 +6666,7 @@ registerProcessor('audio_processor', AudioProcessor);
6624
6666
  if (!this.options.enableAmplitudeMonitoring) {
6625
6667
  this.agentAudioAmplitude = 0;
6626
6668
  }
6627
- if (this.audioOutput) {
6628
- this.wavPlayer.unmute();
6629
- }
6630
- else {
6631
- this.wavPlayer.mute();
6632
- }
6669
+ this.wavPlayer.unmute();
6633
6670
  }
6634
6671
  async connectToAudioInput() {
6635
6672
  if (!this.audioInput) {
@@ -6678,6 +6715,7 @@ registerProcessor('audio_processor', AudioProcessor);
6678
6715
  */
6679
6716
  async setInputDevice(deviceId) {
6680
6717
  var _a, _b, _c;
6718
+ console.log('setInputDevice called with:', deviceId, 'audioInput:', this.audioInput);
6681
6719
  const normalizedDeviceId = !deviceId || deviceId === 'default' ? null : deviceId;
6682
6720
  this.useSystemDefaultDevice = normalizedDeviceId === null;
6683
6721
  this.deviceId = normalizedDeviceId;
@@ -6686,6 +6724,7 @@ registerProcessor('audio_processor', AudioProcessor);
6686
6724
  return;
6687
6725
  }
6688
6726
  try {
6727
+ console.log('setInputDevice: calling _queueRecorderRestart');
6689
6728
  // Restart recording with the new device
6690
6729
  await this._queueRecorderRestart();
6691
6730
  // Reinitialize VAD with the new audio stream if VAD is enabled
@@ -6695,7 +6734,7 @@ registerProcessor('audio_processor', AudioProcessor);
6695
6734
  const newStream = this.wavRecorder.getStream();
6696
6735
  await this._reinitializeVAD(newStream);
6697
6736
  }
6698
- const reportedDeviceId = (_c = (_b = this.lastReportedDeviceId) !== null && _b !== void 0 ? _b : this.activeDeviceId) !== null && _c !== void 0 ? _c : (this.useSystemDefaultDevice ? 'default' : normalizedDeviceId !== null && normalizedDeviceId !== void 0 ? normalizedDeviceId : 'default');
6737
+ const reportedDeviceId = (_c = (_b = this.lastReportedDeviceId) !== null && _b !== void 0 ? _b : this.activeDeviceId) !== null && _c !== void 0 ? _c : (this.useSystemDefaultDevice ? 'default' : (normalizedDeviceId !== null && normalizedDeviceId !== void 0 ? normalizedDeviceId : 'default'));
6699
6738
  console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
6700
6739
  }
6701
6740
  catch (error) {
@@ -6749,7 +6788,7 @@ registerProcessor('audio_processor', AudioProcessor);
6749
6788
  this.recorderStarted = true;
6750
6789
  this._sendReadyIfNeeded();
6751
6790
  }
6752
- const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
6791
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
6753
6792
  if (reportedDeviceId !== previousReportedDeviceId) {
6754
6793
  this.lastReportedDeviceId = reportedDeviceId;
6755
6794
  if (this.options.onDeviceSwitched) {
@@ -6768,29 +6807,6 @@ registerProcessor('audio_processor', AudioProcessor);
6768
6807
  this.recorderRestartChain = run.catch(() => { });
6769
6808
  return run;
6770
6809
  }
6771
- async _initializeRecorderWithDefaultDevice() {
6772
- if (!this.deviceChangeListener) {
6773
- return;
6774
- }
6775
- try {
6776
- const devices = await this.wavRecorder.listDevices();
6777
- if (devices.length) {
6778
- await this.deviceChangeListener(devices);
6779
- return;
6780
- }
6781
- console.warn('No audio input devices available when enabling microphone');
6782
- }
6783
- catch (error) {
6784
- console.warn('Unable to prime audio devices from listDevices()', error);
6785
- }
6786
- try {
6787
- await this.setInputDevice('default');
6788
- }
6789
- catch (error) {
6790
- console.error('Failed to start recording with the system default device:', error);
6791
- throw error;
6792
- }
6793
- }
6794
6810
  /**
6795
6811
  * Disconnect VAD
6796
6812
  */
@@ -6809,7 +6825,7 @@ registerProcessor('audio_processor', AudioProcessor);
6809
6825
  this.stopVad();
6810
6826
  // Reinitialize with new stream only if we're actually capturing audio
6811
6827
  if (stream && this._shouldCaptureUserAudio()) {
6812
- this._initializeVAD();
6828
+ await this._initializeVAD();
6813
6829
  }
6814
6830
  }
6815
6831
  /**
@@ -6831,7 +6847,8 @@ registerProcessor('audio_processor', AudioProcessor);
6831
6847
  };
6832
6848
  });
6833
6849
  this.deviceChangeListener = async (devices) => {
6834
- var _a;
6850
+ var _a, _b;
6851
+ console.log('deviceChangeListener called, devices:', devices.length, 'recorderStarted:', this.recorderStarted, '_skipFirstDeviceCallback:', this._skipFirstDeviceCallback);
6835
6852
  try {
6836
6853
  // Notify user that devices have changed
6837
6854
  this.options.onDevicesChanged(devices);
@@ -6839,7 +6856,17 @@ registerProcessor('audio_processor', AudioProcessor);
6839
6856
  const usingDefaultDevice = this.useSystemDefaultDevice;
6840
6857
  const previousDefaultDeviceKey = this.lastKnownSystemDefaultDeviceKey;
6841
6858
  const currentDefaultDeviceKey = this._getDeviceComparisonKey(defaultDevice);
6859
+ // Skip switching on the first callback after starting the recorder to avoid redundant begin() calls
6860
+ // This is set by audioInputConnect() after _startRecorderWithDevice() completes
6861
+ if (this._skipFirstDeviceCallback) {
6862
+ console.log('deviceChangeListener: skipping first callback after recorder start');
6863
+ this._skipFirstDeviceCallback = false;
6864
+ this.lastKnownSystemDefaultDeviceKey = currentDefaultDeviceKey;
6865
+ (_a = this.resolveDeviceListenerReady) === null || _a === void 0 ? void 0 : _a.call(this);
6866
+ return;
6867
+ }
6842
6868
  let shouldSwitch = !this.recorderStarted;
6869
+ console.log('deviceChangeListener: shouldSwitch initial:', shouldSwitch);
6843
6870
  if (!shouldSwitch) {
6844
6871
  if (usingDefaultDevice) {
6845
6872
  if (!defaultDevice) {
@@ -6848,8 +6875,7 @@ registerProcessor('audio_processor', AudioProcessor);
6848
6875
  else if (this.activeDeviceId && defaultDevice.deviceId !== 'default' && defaultDevice.deviceId !== this.activeDeviceId) {
6849
6876
  shouldSwitch = true;
6850
6877
  }
6851
- else if ((previousDefaultDeviceKey && previousDefaultDeviceKey !== currentDefaultDeviceKey) ||
6852
- (!previousDefaultDeviceKey && !currentDefaultDeviceKey && this.recorderStarted)) {
6878
+ else if ((previousDefaultDeviceKey && previousDefaultDeviceKey !== currentDefaultDeviceKey) || (!previousDefaultDeviceKey && !currentDefaultDeviceKey && this.recorderStarted)) {
6853
6879
  shouldSwitch = true;
6854
6880
  }
6855
6881
  }
@@ -6859,6 +6885,7 @@ registerProcessor('audio_processor', AudioProcessor);
6859
6885
  }
6860
6886
  }
6861
6887
  this.lastKnownSystemDefaultDeviceKey = currentDefaultDeviceKey;
6888
+ console.log('deviceChangeListener: final shouldSwitch:', shouldSwitch);
6862
6889
  if (shouldSwitch) {
6863
6890
  console.debug('Selecting audio input device after change');
6864
6891
  let targetDeviceId = null;
@@ -6888,7 +6915,7 @@ registerProcessor('audio_processor', AudioProcessor);
6888
6915
  this.options.onError(error instanceof Error ? error : new Error(String(error)));
6889
6916
  }
6890
6917
  finally {
6891
- (_a = this.resolveDeviceListenerReady) === null || _a === void 0 ? void 0 : _a.call(this);
6918
+ (_b = this.resolveDeviceListenerReady) === null || _b === void 0 ? void 0 : _b.call(this);
6892
6919
  }
6893
6920
  };
6894
6921
  this.wavRecorder.listenForDeviceChange(this.deviceChangeListener);
@@ -6912,6 +6939,7 @@ registerProcessor('audio_processor', AudioProcessor);
6912
6939
  this.lastKnownSystemDefaultDeviceKey = null;
6913
6940
  this.recorderStarted = false;
6914
6941
  this.readySent = false;
6942
+ this._skipFirstDeviceCallback = false;
6915
6943
  this._stopAmplitudeMonitoring();
6916
6944
  this._teardownDeviceListeners();
6917
6945
  if (this.vad) {
@@ -6947,6 +6975,81 @@ registerProcessor('audio_processor', AudioProcessor);
6947
6975
  }
6948
6976
  return null;
6949
6977
  }
6978
+ _getUserActivationState() {
6979
+ try {
6980
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
6981
+ const act = nav === null || nav === void 0 ? void 0 : nav.userActivation;
6982
+ if (act && typeof act === 'object') {
6983
+ if (typeof act.hasBeenActive === 'boolean')
6984
+ return act.hasBeenActive;
6985
+ if (typeof act.isActive === 'boolean')
6986
+ return act.isActive ? true : null;
6987
+ }
6988
+ const doc = typeof document !== 'undefined' ? document : null;
6989
+ const dact = doc === null || doc === void 0 ? void 0 : doc.userActivation;
6990
+ if (dact && typeof dact === 'object') {
6991
+ if (typeof dact.hasBeenActive === 'boolean')
6992
+ return dact.hasBeenActive;
6993
+ if (typeof dact.isActive === 'boolean')
6994
+ return dact.isActive ? true : null;
6995
+ }
6996
+ }
6997
+ catch (_a) { }
6998
+ return null;
6999
+ }
7000
+ async _isMicrophonePermissionDenied() {
7001
+ try {
7002
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
7003
+ const permissions = nav === null || nav === void 0 ? void 0 : nav.permissions;
7004
+ if (!(permissions === null || permissions === void 0 ? void 0 : permissions.query))
7005
+ return null;
7006
+ const status = await permissions.query({ name: 'microphone' });
7007
+ const state = status === null || status === void 0 ? void 0 : status.state;
7008
+ if (state === 'denied')
7009
+ return true;
7010
+ if (state === 'granted' || state === 'prompt')
7011
+ return false;
7012
+ }
7013
+ catch (_a) { }
7014
+ return null;
7015
+ }
7016
+ async _microphonePermissionDeniedError(error) {
7017
+ const err = error;
7018
+ const message = typeof (err === null || err === void 0 ? void 0 : err.message) === 'string' ? err.message : typeof error === 'string' ? error : '';
7019
+ if (message === 'User has denined audio device permissions') {
7020
+ return err instanceof Error ? err : new Error(message);
7021
+ }
7022
+ const name = typeof (err === null || err === void 0 ? void 0 : err.name) === 'string' ? err.name : '';
7023
+ const isPermissionLike = name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError';
7024
+ if (!isPermissionLike) {
7025
+ return null;
7026
+ }
7027
+ const micDenied = await this._isMicrophonePermissionDenied();
7028
+ if (micDenied === true || /permission denied/i.test(message)) {
7029
+ return new Error('User has denined audio device permissions');
7030
+ }
7031
+ return null;
7032
+ }
7033
+ async _shouldWarnAudioDevicesRequireUserGesture(error) {
7034
+ const e = error;
7035
+ const name = typeof (e === null || e === void 0 ? void 0 : e.name) === 'string' ? e.name : '';
7036
+ const msg = typeof (e === null || e === void 0 ? void 0 : e.message) === 'string'
7037
+ ? e.message
7038
+ : typeof error === 'string'
7039
+ ? error
7040
+ : '';
7041
+ const isPermissionLike = name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError';
7042
+ if (!isPermissionLike)
7043
+ return false;
7044
+ // If the browser can tell us mic permission is explicitly denied, don't show the "user gesture" guidance.
7045
+ const micDenied = await this._isMicrophonePermissionDenied();
7046
+ if (micDenied === true)
7047
+ return false;
7048
+ if (/user activation|user gesture|interacte?d? with( the)? (page|document)|before user has interacted/i.test(msg)) {
7049
+ return true;
7050
+ }
7051
+ return this._getUserActivationState() === false;
7052
+ }
6950
7053
  /**
6951
7054
  * Mutes the microphone to stop sending audio to the server
6952
7055
  * The connection and recording remain active for quick unmute
@@ -6963,13 +7066,13 @@ registerProcessor('audio_processor', AudioProcessor);
6963
7066
  /**
6964
7067
  * Unmutes the microphone to resume sending audio to the server
6965
7068
  */
6966
- unmute() {
7069
+ async unmute() {
6967
7070
  if (this.isMuted) {
6968
7071
  this.isMuted = false;
6969
7072
  console.log('Microphone unmuted');
6970
7073
  this.options.onMuteStateChange(false);
6971
7074
  if (this.audioInput && this.recorderStarted) {
6972
- this._initializeVAD();
7075
+ await this._initializeVAD();
6973
7076
  if (this.stopRecorderAmplitude === undefined) {
6974
7077
  this._setupAmplitudeMonitoring(this.wavRecorder, this.options.onUserAmplitudeChange, (amp) => (this.userAudioAmplitude = amp));
6975
7078
  }