@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.
@@ -5082,11 +5082,7 @@ class WavRecorder {
5082
5082
  * @param {{sampleRate?: number, outputToSpeakers?: boolean, debug?: boolean}} [options]
5083
5083
  * @returns {WavRecorder}
5084
5084
  */
5085
- constructor({
5086
- sampleRate = 24000,
5087
- outputToSpeakers = false,
5088
- debug = false,
5089
- } = {}) {
5085
+ constructor({ sampleRate = 24000, outputToSpeakers = false, debug = false } = {}) {
5090
5086
  // Script source
5091
5087
  this.scriptSrc = AudioProcessorSrc;
5092
5088
  // Config
@@ -5104,6 +5100,11 @@ class WavRecorder {
5104
5100
  this.analyser = null;
5105
5101
  this.recording = false;
5106
5102
  this.contextSampleRate = sampleRate;
5103
+ // Track whether we've already obtained microphone permission
5104
+ // This avoids redundant getUserMedia calls which are expensive on iOS Safari
5105
+ this._hasPermission = false;
5106
+ // Promise used to dedupe concurrent requestPermission() calls
5107
+ this._permissionPromise = null;
5107
5108
  // Event handling with AudioWorklet
5108
5109
  this._lastEventId = 0;
5109
5110
  this.eventReceipts = {};
@@ -5131,17 +5132,13 @@ class WavRecorder {
5131
5132
  let blob;
5132
5133
  if (audioData instanceof Blob) {
5133
5134
  if (fromSampleRate !== -1) {
5134
- throw new Error(
5135
- `Can not specify "fromSampleRate" when reading from Blob`,
5136
- );
5135
+ throw new Error(`Can not specify "fromSampleRate" when reading from Blob`);
5137
5136
  }
5138
5137
  blob = audioData;
5139
5138
  arrayBuffer = await blob.arrayBuffer();
5140
5139
  } else if (audioData instanceof ArrayBuffer) {
5141
5140
  if (fromSampleRate !== -1) {
5142
- throw new Error(
5143
- `Can not specify "fromSampleRate" when reading from ArrayBuffer`,
5144
- );
5141
+ throw new Error(`Can not specify "fromSampleRate" when reading from ArrayBuffer`);
5145
5142
  }
5146
5143
  arrayBuffer = audioData;
5147
5144
  blob = new Blob([arrayBuffer], { type: 'audio/wav' });
@@ -5159,14 +5156,10 @@ class WavRecorder {
5159
5156
  } else if (audioData instanceof Array) {
5160
5157
  float32Array = new Float32Array(audioData);
5161
5158
  } else {
5162
- throw new Error(
5163
- `"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`,
5164
- );
5159
+ throw new Error(`"audioData" must be one of: Blob, Float32Arrray, Int16Array, ArrayBuffer, Array<number>`);
5165
5160
  }
5166
5161
  if (fromSampleRate === -1) {
5167
- throw new Error(
5168
- `Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`,
5169
- );
5162
+ throw new Error(`Must specify "fromSampleRate" when reading from Float32Array, In16Array or Array`);
5170
5163
  } else if (fromSampleRate < 3000) {
5171
5164
  throw new Error(`Minimum "fromSampleRate" is 3000 (3kHz)`);
5172
5165
  }
@@ -5196,12 +5189,13 @@ class WavRecorder {
5196
5189
 
5197
5190
  /**
5198
5191
  * Logs data in debug mode
5199
- * @param {...any} arguments
5192
+ * @param {...any} args
5200
5193
  * @returns {true}
5201
5194
  */
5202
- log() {
5195
+ log(...args) {
5203
5196
  if (this.debug) {
5204
- this.log(...arguments);
5197
+ // eslint-disable-next-line no-console
5198
+ console.log(...args);
5205
5199
  }
5206
5200
  return true;
5207
5201
  }
@@ -5274,10 +5268,7 @@ class WavRecorder {
5274
5268
  */
5275
5269
  listenForDeviceChange(callback) {
5276
5270
  if (callback === null && this._deviceChangeCallback) {
5277
- navigator.mediaDevices.removeEventListener(
5278
- 'devicechange',
5279
- this._deviceChangeCallback,
5280
- );
5271
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeCallback);
5281
5272
  this._deviceChangeCallback = null;
5282
5273
  } else if (callback !== null) {
5283
5274
  // Basically a debounce; we only want this called once when devices change
@@ -5309,19 +5300,39 @@ class WavRecorder {
5309
5300
 
5310
5301
  /**
5311
5302
  * Manually request permission to use the microphone
5303
+ * Skips if permission has already been granted to avoid expensive redundant getUserMedia calls.
5304
+ * Dedupes concurrent calls to prevent multiple getUserMedia requests.
5312
5305
  * @returns {Promise<true>}
5313
5306
  */
5314
5307
  async requestPermission() {
5315
- try {
5316
- console.log('ensureUserMediaAccess');
5317
- await navigator.mediaDevices.getUserMedia({
5318
- audio: true,
5319
- });
5320
- } catch (fallbackError) {
5321
- window.alert('You must grant microphone access to use this feature.');
5322
- throw fallbackError;
5308
+ // Skip if we already have permission - each getUserMedia is expensive on iOS Safari
5309
+ if (this._hasPermission) {
5310
+ return true;
5323
5311
  }
5324
- return true;
5312
+ // Dedupe concurrent calls: if a permission request is already in flight, wait for it
5313
+ if (this._permissionPromise) {
5314
+ return this._permissionPromise;
5315
+ }
5316
+
5317
+ console.log('ensureUserMediaAccess');
5318
+ this._permissionPromise = (async () => {
5319
+ try {
5320
+ const stream = await navigator.mediaDevices.getUserMedia({
5321
+ audio: true,
5322
+ });
5323
+ // Stop the tracks immediately after getting permission
5324
+ stream.getTracks().forEach((track) => track.stop());
5325
+ this._hasPermission = true;
5326
+ return true;
5327
+ } catch (fallbackError) {
5328
+ console.error('getUserMedia failed:', fallbackError.name, fallbackError.message);
5329
+ throw fallbackError;
5330
+ } finally {
5331
+ this._permissionPromise = null;
5332
+ }
5333
+ })();
5334
+
5335
+ return this._permissionPromise;
5325
5336
  }
5326
5337
 
5327
5338
  /**
@@ -5329,25 +5340,18 @@ class WavRecorder {
5329
5340
  * @returns {Promise<Array<MediaDeviceInfo & {default: boolean}>>}
5330
5341
  */
5331
5342
  async listDevices() {
5332
- if (
5333
- !navigator.mediaDevices ||
5334
- !('enumerateDevices' in navigator.mediaDevices)
5335
- ) {
5343
+ if (!navigator.mediaDevices || !('enumerateDevices' in navigator.mediaDevices)) {
5336
5344
  throw new Error('Could not request user devices');
5337
5345
  }
5338
5346
  await this.requestPermission();
5339
5347
 
5340
5348
  const devices = await navigator.mediaDevices.enumerateDevices();
5341
5349
  const audioDevices = devices.filter((device) => device.kind === 'audioinput');
5342
- const defaultDeviceIndex = audioDevices.findIndex(
5343
- (device) => device.deviceId === 'default',
5344
- );
5350
+ const defaultDeviceIndex = audioDevices.findIndex((device) => device.deviceId === 'default');
5345
5351
  const deviceList = [];
5346
5352
  if (defaultDeviceIndex !== -1) {
5347
5353
  let defaultDevice = audioDevices.splice(defaultDeviceIndex, 1)[0];
5348
- let existingIndex = audioDevices.findIndex(
5349
- (device) => device.groupId === defaultDevice.groupId,
5350
- );
5354
+ let existingIndex = audioDevices.findIndex((device) => device.groupId === defaultDevice.groupId);
5351
5355
  if (existingIndex !== -1) {
5352
5356
  defaultDevice = audioDevices.splice(existingIndex, 1)[0];
5353
5357
  }
@@ -5369,15 +5373,10 @@ class WavRecorder {
5369
5373
  */
5370
5374
  async begin(deviceId) {
5371
5375
  if (this.processor) {
5372
- throw new Error(
5373
- `Already connected: please call .end() to start a new session`,
5374
- );
5376
+ throw new Error(`Already connected: please call .end() to start a new session`);
5375
5377
  }
5376
5378
 
5377
- if (
5378
- !navigator.mediaDevices ||
5379
- !('getUserMedia' in navigator.mediaDevices)
5380
- ) {
5379
+ if (!navigator.mediaDevices || !('getUserMedia' in navigator.mediaDevices)) {
5381
5380
  throw new Error('Could not request user media');
5382
5381
  }
5383
5382
  try {
@@ -5388,14 +5387,16 @@ class WavRecorder {
5388
5387
  echoCancellation: true,
5389
5388
  autoGainControl: true,
5390
5389
  noiseSuppression: true,
5391
- }
5390
+ },
5392
5391
  };
5393
5392
  if (deviceId) {
5394
5393
  config.audio.deviceId = { exact: deviceId };
5395
5394
  }
5396
5395
  this.stream = await navigator.mediaDevices.getUserMedia(config);
5396
+ // Mark permission as granted so listDevices() won't call requestPermission() again
5397
+ this._hasPermission = true;
5397
5398
  } catch (err) {
5398
- throw new Error('Could not start media stream');
5399
+ throw err;
5399
5400
  }
5400
5401
 
5401
5402
  const createContext = (rate) => {
@@ -5447,10 +5448,7 @@ class WavRecorder {
5447
5448
  raw: WavPacker.mergeBuffers(buffer.raw, data.raw),
5448
5449
  mono: WavPacker.mergeBuffers(buffer.mono, data.mono),
5449
5450
  };
5450
- if (
5451
- this._chunkProcessorBuffer.mono.byteLength >=
5452
- this._chunkProcessorSize
5453
- ) {
5451
+ if (this._chunkProcessorBuffer.mono.byteLength >= this._chunkProcessorSize) {
5454
5452
  this._chunkProcessor(this._chunkProcessorBuffer);
5455
5453
  this._chunkProcessorBuffer = {
5456
5454
  raw: new ArrayBuffer(0),
@@ -5478,11 +5476,7 @@ class WavRecorder {
5478
5476
  node.connect(analyser);
5479
5477
  if (this.outputToSpeakers) {
5480
5478
  // eslint-disable-next-line no-console
5481
- console.warn(
5482
- 'Warning: Output to speakers may affect sound quality,\n' +
5483
- 'especially due to system audio feedback preventative measures.\n' +
5484
- 'use only for debugging',
5485
- );
5479
+ console.warn('Warning: Output to speakers may affect sound quality,\n' + 'especially due to system audio feedback preventative measures.\n' + 'use only for debugging');
5486
5480
  analyser.connect(context.destination);
5487
5481
  }
5488
5482
 
@@ -5509,26 +5503,14 @@ class WavRecorder {
5509
5503
  * @param {number} [maxDecibels] default -30
5510
5504
  * @returns {import('./analysis/audio_analysis.js').AudioAnalysisOutputType}
5511
5505
  */
5512
- getFrequencies(
5513
- analysisType = 'frequency',
5514
- minDecibels = -100,
5515
- maxDecibels = -30,
5516
- ) {
5506
+ getFrequencies(analysisType = 'frequency', minDecibels = -100, maxDecibels = -30) {
5517
5507
  if (!this.processor) {
5518
5508
  throw new Error('Session ended: please call .begin() first');
5519
5509
  }
5520
- return AudioAnalysis.getFrequencies(
5521
- this.analyser,
5522
- this.sampleRate,
5523
- null,
5524
- analysisType,
5525
- minDecibels,
5526
- maxDecibels,
5527
- );
5510
+ return AudioAnalysis.getFrequencies(this.analyser, this.sampleRate, null, analysisType, minDecibels, maxDecibels);
5528
5511
  }
5529
5512
 
5530
-
5531
- /**
5513
+ /**
5532
5514
  * Gets the real-time amplitude of the audio signal
5533
5515
  * @returns {number} Amplitude value between 0 and 1
5534
5516
  */
@@ -5653,9 +5635,7 @@ class WavRecorder {
5653
5635
  throw new Error('Session ended: please call .begin() first');
5654
5636
  }
5655
5637
  if (!force && this.recording) {
5656
- throw new Error(
5657
- 'Currently recording: please call .pause() first, or call .save(true) to force',
5658
- );
5638
+ throw new Error('Currently recording: please call .pause() first, or call .save(true) to force');
5659
5639
  }
5660
5640
  this.log('Exporting ...');
5661
5641
  const exportData = await this._event('export');
@@ -5762,6 +5742,7 @@ function arrayBufferToBase64(arrayBuffer) {
5762
5742
  return btoa(binary);
5763
5743
  }
5764
5744
 
5745
+ //// src/index.ts
5765
5746
  /* eslint-env browser */
5766
5747
  // import { env as ortEnv } from 'onnxruntime-web';
5767
5748
  // @ts-ignore - VAD package does not provide TypeScript types
@@ -5769,137 +5750,40 @@ const NOOP = () => { };
5769
5750
  const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
5770
5751
  // SDK version - updated when publishing
5771
5752
  const SDK_VERSION = '2.7.0';
5772
- const MEDIA_DEVICE_CHANGE_EVENT = 'devicechange';
5773
- const MEDIA_DEVICE_KIND_AUDIO = 'audioinput';
5753
+ const DEFAULT_RECORDER_SAMPLE_RATE = 8000;
5774
5754
  const hasMediaDevicesSupport = () => typeof navigator !== 'undefined' && !!navigator.mediaDevices;
5775
- let microphonePermissionPromise = null;
5776
- let microphonePermissionGranted = false;
5777
- const stopStreamTracks = (stream) => {
5778
- if (!stream) {
5779
- return;
5780
- }
5781
- stream.getTracks().forEach((track) => {
5782
- try {
5783
- track.stop();
5784
- }
5785
- catch (_a) {
5786
- /* noop */
5787
- }
5788
- });
5789
- };
5790
- const ensureMicrophonePermissions = async () => {
5791
- if (!hasMediaDevicesSupport()) {
5792
- throw new Error('Media devices are not available in this environment');
5793
- }
5794
- if (microphonePermissionGranted) {
5795
- return;
5796
- }
5797
- if (!microphonePermissionPromise) {
5798
- microphonePermissionPromise = navigator.mediaDevices
5799
- .getUserMedia({ audio: true })
5800
- .then((stream) => {
5801
- microphonePermissionGranted = true;
5802
- stopStreamTracks(stream);
5803
- })
5804
- .finally(() => {
5805
- microphonePermissionPromise = null;
5806
- });
5807
- }
5808
- return microphonePermissionPromise;
5809
- };
5810
- const cloneAudioDevice = (device, isDefault) => {
5755
+ const toLayercodeAudioInputDevice = (device) => {
5811
5756
  const cloned = {
5812
- deviceId: device.deviceId,
5813
- groupId: device.groupId,
5814
- kind: device.kind,
5757
+ ...device,
5815
5758
  label: device.label,
5816
- default: isDefault,
5759
+ default: Boolean(device.default),
5817
5760
  };
5818
5761
  if (typeof device.toJSON === 'function') {
5819
5762
  cloned.toJSON = device.toJSON.bind(device);
5820
5763
  }
5821
5764
  return cloned;
5822
5765
  };
5823
- const normalizeAudioInputDevices = (devices) => {
5824
- const audioDevices = devices.filter((device) => device.kind === MEDIA_DEVICE_KIND_AUDIO);
5825
- if (!audioDevices.length) {
5826
- return [];
5827
- }
5828
- const remaining = [...audioDevices];
5829
- const normalized = [];
5830
- const defaultIndex = remaining.findIndex((device) => device.deviceId === 'default');
5831
- if (defaultIndex !== -1) {
5832
- let defaultDevice = remaining.splice(defaultIndex, 1)[0];
5833
- const groupMatchIndex = remaining.findIndex((device) => device.groupId && defaultDevice.groupId && device.groupId === defaultDevice.groupId);
5834
- if (groupMatchIndex !== -1) {
5835
- defaultDevice = remaining.splice(groupMatchIndex, 1)[0];
5836
- }
5837
- normalized.push(cloneAudioDevice(defaultDevice, true));
5838
- }
5839
- else if (remaining.length) {
5840
- const fallbackDefault = remaining.shift();
5841
- normalized.push(cloneAudioDevice(fallbackDefault, true));
5842
- }
5843
- return normalized.concat(remaining.map((device) => cloneAudioDevice(device, false)));
5844
- };
5845
5766
  const listAudioInputDevices = async () => {
5846
5767
  if (!hasMediaDevicesSupport()) {
5847
5768
  throw new Error('Media devices are not available in this environment');
5848
5769
  }
5849
- await ensureMicrophonePermissions();
5850
- const devices = await navigator.mediaDevices.enumerateDevices();
5851
- return normalizeAudioInputDevices(devices);
5770
+ const recorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE });
5771
+ const devices = (await recorder.listDevices());
5772
+ return devices.map(toLayercodeAudioInputDevice);
5852
5773
  };
5853
5774
  const watchAudioInputDevices = (callback) => {
5854
5775
  if (!hasMediaDevicesSupport()) {
5855
5776
  return () => { };
5856
5777
  }
5857
- let disposed = false;
5858
- let lastSignature = null;
5859
- let requestId = 0;
5860
- const emitDevices = async () => {
5861
- requestId += 1;
5862
- const currentRequest = requestId;
5863
- try {
5864
- const devices = await listAudioInputDevices();
5865
- if (disposed || currentRequest !== requestId) {
5866
- return;
5867
- }
5868
- const signature = devices.map((device) => `${device.deviceId}:${device.label}:${device.groupId}:${device.default ? '1' : '0'}`).join('|');
5869
- if (signature !== lastSignature) {
5870
- lastSignature = signature;
5871
- callback(devices);
5872
- }
5873
- }
5874
- catch (error) {
5875
- if (!disposed) {
5876
- console.warn('Failed to refresh audio devices', error);
5877
- }
5878
- }
5778
+ const recorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE });
5779
+ const handleDevicesChange = (devices) => {
5780
+ callback(devices.map(toLayercodeAudioInputDevice));
5879
5781
  };
5880
- const handler = () => {
5881
- void emitDevices();
5882
- };
5883
- const mediaDevices = navigator.mediaDevices;
5884
- let teardown = null;
5885
- if (typeof mediaDevices.addEventListener === 'function') {
5886
- mediaDevices.addEventListener(MEDIA_DEVICE_CHANGE_EVENT, handler);
5887
- teardown = () => mediaDevices.removeEventListener(MEDIA_DEVICE_CHANGE_EVENT, handler);
5888
- }
5889
- else if ('ondevicechange' in mediaDevices) {
5890
- const previousHandler = mediaDevices.ondevicechange;
5891
- mediaDevices.ondevicechange = handler;
5892
- teardown = () => {
5893
- if (mediaDevices.ondevicechange === handler) {
5894
- mediaDevices.ondevicechange = previousHandler || null;
5895
- }
5896
- };
5897
- }
5898
- // Always emit once on subscribe
5899
- void emitDevices();
5782
+ // WavRecorder handles initial emit + deduping devicechange events
5783
+ recorder.listenForDeviceChange(handleDevicesChange);
5900
5784
  return () => {
5901
- disposed = true;
5902
- teardown === null || teardown === void 0 ? void 0 : teardown();
5785
+ recorder.listenForDeviceChange(null);
5786
+ recorder.quit().catch(() => { });
5903
5787
  };
5904
5788
  };
5905
5789
  /**
@@ -5946,7 +5830,7 @@ class LayercodeClient {
5946
5830
  this.AMPLITUDE_MONITORING_SAMPLE_RATE = 2;
5947
5831
  this._websocketUrl = DEFAULT_WS_URL;
5948
5832
  this.audioOutputReady = null;
5949
- this.wavRecorder = new WavRecorder({ sampleRate: 8000 }); // TODO should be set my fetched agent config
5833
+ this.wavRecorder = new WavRecorder({ sampleRate: DEFAULT_RECORDER_SAMPLE_RATE }); // TODO should be set by fetched agent config
5950
5834
  this.wavPlayer = new WavStreamPlayer({
5951
5835
  finishedPlayingCallback: this._clientResponseAudioReplayFinished.bind(this),
5952
5836
  sampleRate: 16000, // TODO should be set my fetched agent config
@@ -5966,6 +5850,7 @@ class LayercodeClient {
5966
5850
  this.recorderStarted = false;
5967
5851
  this.readySent = false;
5968
5852
  this.currentTurnId = null;
5853
+ this.sentReplayFinishedForDisabledOutput = false;
5969
5854
  this.audioBuffer = [];
5970
5855
  this.vadConfig = null;
5971
5856
  this.activeDeviceId = null;
@@ -5977,6 +5862,7 @@ class LayercodeClient {
5977
5862
  this.stopRecorderAmplitude = undefined;
5978
5863
  this.deviceChangeListener = null;
5979
5864
  this.recorderRestartChain = Promise.resolve();
5865
+ this._skipFirstDeviceCallback = false;
5980
5866
  this.deviceListenerReady = null;
5981
5867
  this.resolveDeviceListenerReady = null;
5982
5868
  // this.audioPauseTime = null;
@@ -5996,7 +5882,7 @@ class LayercodeClient {
5996
5882
  set onDevicesChanged(callback) {
5997
5883
  this.options.onDevicesChanged = callback !== null && callback !== void 0 ? callback : NOOP;
5998
5884
  }
5999
- _initializeVAD() {
5885
+ async _initializeVAD() {
6000
5886
  var _a;
6001
5887
  console.log('initializing VAD', { pushToTalkEnabled: this.pushToTalkEnabled, canInterrupt: this.canInterrupt, vadConfig: this.vadConfig });
6002
5888
  // If we're in push to talk mode or mute mode, we don't need to use the VAD model
@@ -6080,13 +5966,13 @@ class LayercodeClient {
6080
5966
  vadOptions.frameSamples = 512; // Required for v5
6081
5967
  }
6082
5968
  console.log('Creating VAD with options:', vadOptions);
6083
- dist.MicVAD.new(vadOptions)
6084
- .then((vad) => {
5969
+ try {
5970
+ const vad = await dist.MicVAD.new(vadOptions);
6085
5971
  this.vad = vad;
6086
5972
  this.vad.start();
6087
5973
  console.log('VAD started successfully');
6088
- })
6089
- .catch((error) => {
5974
+ }
5975
+ catch (error) {
6090
5976
  console.warn('Error initializing VAD:', error);
6091
5977
  // Send a message to server indicating VAD failure
6092
5978
  const vadFailureMessage = {
@@ -6098,7 +5984,7 @@ class LayercodeClient {
6098
5984
  ...vadFailureMessage,
6099
5985
  userSpeaking: this.userIsSpeaking,
6100
5986
  });
6101
- });
5987
+ }
6102
5988
  }
6103
5989
  /**
6104
5990
  * Updates the connection status and triggers the callback
@@ -6125,11 +6011,14 @@ class LayercodeClient {
6125
6011
  this.options.onAgentSpeakingChange(shouldReportSpeaking);
6126
6012
  }
6127
6013
  _setUserSpeaking(isSpeaking) {
6128
- const shouldReportSpeaking = this._shouldCaptureUserAudio() && isSpeaking;
6014
+ const shouldCapture = this._shouldCaptureUserAudio();
6015
+ const shouldReportSpeaking = shouldCapture && isSpeaking;
6016
+ console.log('_setUserSpeaking called:', isSpeaking, 'shouldCapture:', shouldCapture, 'shouldReportSpeaking:', shouldReportSpeaking, 'current userIsSpeaking:', this.userIsSpeaking);
6129
6017
  if (this.userIsSpeaking === shouldReportSpeaking) {
6130
6018
  return;
6131
6019
  }
6132
6020
  this.userIsSpeaking = shouldReportSpeaking;
6021
+ console.log('_setUserSpeaking: updated userIsSpeaking to:', this.userIsSpeaking);
6133
6022
  this.options.onUserIsSpeakingChange(shouldReportSpeaking);
6134
6023
  }
6135
6024
  /**
@@ -6179,6 +6068,7 @@ class LayercodeClient {
6179
6068
  * @param {MessageEvent} event - The WebSocket message event
6180
6069
  */
6181
6070
  async _handleWebSocketMessage(event) {
6071
+ var _a, _b;
6182
6072
  try {
6183
6073
  const message = JSON.parse(event.data);
6184
6074
  if (message.type !== 'response.audio') {
@@ -6191,6 +6081,20 @@ class LayercodeClient {
6191
6081
  // Start tracking new agent turn
6192
6082
  console.debug('Agent turn started, will track new turn ID from audio/text');
6193
6083
  this._setUserSpeaking(false);
6084
+ // Reset the flag for the new assistant turn
6085
+ this.sentReplayFinishedForDisabledOutput = false;
6086
+ // When assistant's turn starts but we're not playing audio,
6087
+ // we need to tell the server we're "done" with playback so it can
6088
+ // transition the turn back to user. Use a small delay to let any
6089
+ // response.audio/response.end messages arrive first.
6090
+ if (!this.audioOutput) {
6091
+ setTimeout(() => {
6092
+ if (!this.audioOutput && !this.sentReplayFinishedForDisabledOutput) {
6093
+ this.sentReplayFinishedForDisabledOutput = true;
6094
+ this._clientResponseAudioReplayFinished();
6095
+ }
6096
+ }, 1000);
6097
+ }
6194
6098
  }
6195
6099
  else if (message.role === 'user' && !this.pushToTalkEnabled) {
6196
6100
  // 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)
@@ -6210,7 +6114,25 @@ class LayercodeClient {
6210
6114
  });
6211
6115
  break;
6212
6116
  }
6117
+ case 'response.end': {
6118
+ // When audioOutput is disabled, notify server that "playback" is complete
6119
+ if (!this.audioOutput && !this.sentReplayFinishedForDisabledOutput) {
6120
+ this.sentReplayFinishedForDisabledOutput = true;
6121
+ this._clientResponseAudioReplayFinished();
6122
+ }
6123
+ (_b = (_a = this.options).onMessage) === null || _b === void 0 ? void 0 : _b.call(_a, message);
6124
+ break;
6125
+ }
6213
6126
  case 'response.audio': {
6127
+ // Skip audio playback if audioOutput is disabled
6128
+ if (!this.audioOutput) {
6129
+ // Send replay_finished so server knows we're "done" with playback (only once per turn)
6130
+ if (!this.sentReplayFinishedForDisabledOutput) {
6131
+ this.sentReplayFinishedForDisabledOutput = true;
6132
+ this._clientResponseAudioReplayFinished();
6133
+ }
6134
+ break;
6135
+ }
6214
6136
  await this._waitForAudioOutputReady();
6215
6137
  const audioBuffer = base64ToArrayBuffer(message.content);
6216
6138
  const hasAudioSamples = audioBuffer.byteLength > 0;
@@ -6345,6 +6267,9 @@ class LayercodeClient {
6345
6267
  }
6346
6268
  _sendReadyIfNeeded() {
6347
6269
  var _a;
6270
+ // Send client.ready when either:
6271
+ // 1. Recorder is started (audio mode active)
6272
+ // 2. audioInput is false (text-only mode, but server should still be ready)
6348
6273
  const audioReady = this.recorderStarted || !this.audioInput;
6349
6274
  if (audioReady && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.readySent) {
6350
6275
  this._wsSend({ type: 'client.ready' });
@@ -6410,14 +6335,99 @@ class LayercodeClient {
6410
6335
  }
6411
6336
  async audioInputConnect() {
6412
6337
  // Turn mic ON
6413
- await this.wavRecorder.requestPermission();
6338
+ // NOTE: On iOS Safari, each getUserMedia call is expensive (~2-3 seconds).
6339
+ // We optimize by:
6340
+ // 1. Starting the recorder FIRST with begin() (single getUserMedia)
6341
+ // 2. THEN setting up device change listeners (which will skip getUserMedia since permission is cached)
6342
+ console.log('audioInputConnect: recorderStarted =', this.recorderStarted);
6343
+ // If the recorder hasn't spun up yet, start it first with the preferred or default device
6344
+ // This ensures we only make ONE getUserMedia call instead of multiple sequential ones
6345
+ if (!this.recorderStarted) {
6346
+ // Use preferred device if set, otherwise use system default
6347
+ const targetDeviceId = this.useSystemDefaultDevice ? undefined : this.deviceId || undefined;
6348
+ // Mark as using system default if no specific device is set
6349
+ if (!targetDeviceId) {
6350
+ this.useSystemDefaultDevice = true;
6351
+ }
6352
+ console.log('audioInputConnect: starting recorder with device:', targetDeviceId !== null && targetDeviceId !== void 0 ? targetDeviceId : 'system default');
6353
+ await this._startRecorderWithDevice(targetDeviceId);
6354
+ }
6355
+ // Now set up device change listeners - permission is already granted so listDevices() won't call getUserMedia
6356
+ // Skip the first callback since we've already started with the correct device
6357
+ this._skipFirstDeviceCallback = true;
6358
+ console.log('audioInputConnect: setting up device change listener');
6414
6359
  await this._setupDeviceChangeListener();
6415
- // If the recorder hasn't spun up yet, proactively select a device.
6416
- if (!this.recorderStarted && this.deviceChangeListener) {
6417
- await this._initializeRecorderWithDefaultDevice();
6360
+ console.log('audioInputConnect: done, recorderStarted =', this.recorderStarted);
6361
+ }
6362
+ /**
6363
+ * Starts the recorder with a specific device (or default if undefined)
6364
+ * This is the single point where getUserMedia is called during initial setup.
6365
+ * Idempotent: returns early if recorder is already started or has a live stream.
6366
+ */
6367
+ async _startRecorderWithDevice(deviceId) {
6368
+ var _a, _b;
6369
+ // Idempotency guard: don't start again if already running
6370
+ if (this.recorderStarted || this._hasLiveRecorderStream()) {
6371
+ console.debug('_startRecorderWithDevice: already started, skipping');
6372
+ return;
6373
+ }
6374
+ try {
6375
+ this._stopRecorderAmplitudeMonitoring();
6376
+ try {
6377
+ await this.wavRecorder.end();
6378
+ }
6379
+ catch (_c) {
6380
+ // Ignore cleanup errors
6381
+ }
6382
+ await this.wavRecorder.begin(deviceId);
6383
+ await this.wavRecorder.record(this._handleDataAvailable, 1638);
6384
+ // Re-setup amplitude monitoring with the new stream
6385
+ this._setupAmplitudeMonitoring(this.wavRecorder, this.options.onUserAmplitudeChange, (amp) => (this.userAudioAmplitude = amp));
6386
+ if (!this.options.enableAmplitudeMonitoring) {
6387
+ this.userAudioAmplitude = 0;
6388
+ }
6389
+ const stream = this.wavRecorder.getStream();
6390
+ const activeTrack = (stream === null || stream === void 0 ? void 0 : stream.getAudioTracks()[0]) || null;
6391
+ const trackSettings = activeTrack && typeof activeTrack.getSettings === 'function' ? activeTrack.getSettings() : null;
6392
+ const trackDeviceId = trackSettings && typeof trackSettings.deviceId === 'string' ? trackSettings.deviceId : null;
6393
+ this.activeDeviceId = trackDeviceId !== null && trackDeviceId !== void 0 ? trackDeviceId : (this.useSystemDefaultDevice ? null : this.deviceId);
6394
+ if (!this.recorderStarted) {
6395
+ this.recorderStarted = true;
6396
+ this._sendReadyIfNeeded();
6397
+ }
6398
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
6399
+ if (reportedDeviceId !== this.lastReportedDeviceId) {
6400
+ this.lastReportedDeviceId = reportedDeviceId;
6401
+ if (this.options.onDeviceSwitched) {
6402
+ this.options.onDeviceSwitched(reportedDeviceId);
6403
+ }
6404
+ }
6405
+ console.debug('Recorder started successfully with device:', reportedDeviceId);
6406
+ }
6407
+ catch (error) {
6408
+ const permissionDeniedError = await this._microphonePermissionDeniedError(error);
6409
+ if (permissionDeniedError) {
6410
+ console.error(permissionDeniedError.message);
6411
+ this.options.onError(permissionDeniedError);
6412
+ throw permissionDeniedError;
6413
+ }
6414
+ if (await this._shouldWarnAudioDevicesRequireUserGesture(error)) {
6415
+ 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');
6416
+ }
6417
+ console.error('Error starting recorder:', error);
6418
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
6419
+ throw error;
6418
6420
  }
6419
6421
  }
6420
6422
  async audioInputDisconnect() {
6423
+ // If we never started the recorder, avoid touching audio APIs at all.
6424
+ if (!this.recorderStarted && !this._hasLiveRecorderStream()) {
6425
+ this._stopRecorderAmplitudeMonitoring();
6426
+ this.stopVad();
6427
+ this._teardownDeviceListeners();
6428
+ this.recorderStarted = false;
6429
+ return;
6430
+ }
6421
6431
  try {
6422
6432
  // stop amplitude monitoring tied to the recorder
6423
6433
  this._stopRecorderAmplitudeMonitoring();
@@ -6439,7 +6449,9 @@ class LayercodeClient {
6439
6449
  this.audioInput = state;
6440
6450
  this._emitAudioInput();
6441
6451
  if (state) {
6452
+ this._setStatus('connecting');
6442
6453
  await this.audioInputConnect();
6454
+ this._setStatus('connected');
6443
6455
  }
6444
6456
  else {
6445
6457
  await this.audioInputDisconnect();
@@ -6451,7 +6463,20 @@ class LayercodeClient {
6451
6463
  this.audioOutput = state;
6452
6464
  this._emitAudioOutput();
6453
6465
  if (state) {
6454
- this.wavPlayer.unmute();
6466
+ // Initialize audio output if not already connected
6467
+ // This happens when audioOutput was initially false and is now being enabled
6468
+ if (!this.wavPlayer.context) {
6469
+ this._setStatus('connecting');
6470
+ // Store the promise so _waitForAudioOutputReady() can await it
6471
+ // This prevents response.audio from running before AudioContext is ready
6472
+ const setupPromise = this.setupAudioOutput();
6473
+ this.audioOutputReady = setupPromise;
6474
+ await setupPromise;
6475
+ this._setStatus('connected');
6476
+ }
6477
+ else {
6478
+ this.wavPlayer.unmute();
6479
+ }
6455
6480
  // Sync agentSpeaking state with actual playback state when enabling audio output
6456
6481
  this._syncAgentSpeakingState();
6457
6482
  }
@@ -6532,7 +6557,19 @@ class LayercodeClient {
6532
6557
  await audioOutputReady;
6533
6558
  }
6534
6559
  catch (error) {
6535
- console.error('Error connecting to Layercode agent:', error);
6560
+ const permissionDeniedError = await this._microphonePermissionDeniedError(error);
6561
+ if (permissionDeniedError) {
6562
+ console.error(permissionDeniedError.message);
6563
+ this._setStatus('error');
6564
+ this.options.onError(permissionDeniedError);
6565
+ return;
6566
+ }
6567
+ if (await this._shouldWarnAudioDevicesRequireUserGesture(error)) {
6568
+ 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');
6569
+ }
6570
+ else {
6571
+ console.error('Error connecting to Layercode agent:', error);
6572
+ }
6536
6573
  this._setStatus('error');
6537
6574
  this.options.onError(error instanceof Error ? error : new Error(String(error)));
6538
6575
  }
@@ -6608,6 +6645,11 @@ class LayercodeClient {
6608
6645
  return authorizeSessionResponseBody;
6609
6646
  }
6610
6647
  async setupAudioOutput() {
6648
+ // Only initialize audio player if audioOutput is enabled
6649
+ // This prevents AudioContext creation before user gesture when audio is disabled
6650
+ if (!this.audioOutput) {
6651
+ return;
6652
+ }
6611
6653
  // Initialize audio player
6612
6654
  // wavRecorder will be started from the onDeviceSwitched callback,
6613
6655
  // which is called when the device is first initialized and also when the device is switched
@@ -6618,12 +6660,7 @@ class LayercodeClient {
6618
6660
  if (!this.options.enableAmplitudeMonitoring) {
6619
6661
  this.agentAudioAmplitude = 0;
6620
6662
  }
6621
- if (this.audioOutput) {
6622
- this.wavPlayer.unmute();
6623
- }
6624
- else {
6625
- this.wavPlayer.mute();
6626
- }
6663
+ this.wavPlayer.unmute();
6627
6664
  }
6628
6665
  async connectToAudioInput() {
6629
6666
  if (!this.audioInput) {
@@ -6672,6 +6709,7 @@ class LayercodeClient {
6672
6709
  */
6673
6710
  async setInputDevice(deviceId) {
6674
6711
  var _a, _b, _c;
6712
+ console.log('setInputDevice called with:', deviceId, 'audioInput:', this.audioInput);
6675
6713
  const normalizedDeviceId = !deviceId || deviceId === 'default' ? null : deviceId;
6676
6714
  this.useSystemDefaultDevice = normalizedDeviceId === null;
6677
6715
  this.deviceId = normalizedDeviceId;
@@ -6680,6 +6718,7 @@ class LayercodeClient {
6680
6718
  return;
6681
6719
  }
6682
6720
  try {
6721
+ console.log('setInputDevice: calling _queueRecorderRestart');
6683
6722
  // Restart recording with the new device
6684
6723
  await this._queueRecorderRestart();
6685
6724
  // Reinitialize VAD with the new audio stream if VAD is enabled
@@ -6689,7 +6728,7 @@ class LayercodeClient {
6689
6728
  const newStream = this.wavRecorder.getStream();
6690
6729
  await this._reinitializeVAD(newStream);
6691
6730
  }
6692
- 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');
6731
+ 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'));
6693
6732
  console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
6694
6733
  }
6695
6734
  catch (error) {
@@ -6743,7 +6782,7 @@ class LayercodeClient {
6743
6782
  this.recorderStarted = true;
6744
6783
  this._sendReadyIfNeeded();
6745
6784
  }
6746
- const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
6785
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
6747
6786
  if (reportedDeviceId !== previousReportedDeviceId) {
6748
6787
  this.lastReportedDeviceId = reportedDeviceId;
6749
6788
  if (this.options.onDeviceSwitched) {
@@ -6762,29 +6801,6 @@ class LayercodeClient {
6762
6801
  this.recorderRestartChain = run.catch(() => { });
6763
6802
  return run;
6764
6803
  }
6765
- async _initializeRecorderWithDefaultDevice() {
6766
- if (!this.deviceChangeListener) {
6767
- return;
6768
- }
6769
- try {
6770
- const devices = await this.wavRecorder.listDevices();
6771
- if (devices.length) {
6772
- await this.deviceChangeListener(devices);
6773
- return;
6774
- }
6775
- console.warn('No audio input devices available when enabling microphone');
6776
- }
6777
- catch (error) {
6778
- console.warn('Unable to prime audio devices from listDevices()', error);
6779
- }
6780
- try {
6781
- await this.setInputDevice('default');
6782
- }
6783
- catch (error) {
6784
- console.error('Failed to start recording with the system default device:', error);
6785
- throw error;
6786
- }
6787
- }
6788
6804
  /**
6789
6805
  * Disconnect VAD
6790
6806
  */
@@ -6803,7 +6819,7 @@ class LayercodeClient {
6803
6819
  this.stopVad();
6804
6820
  // Reinitialize with new stream only if we're actually capturing audio
6805
6821
  if (stream && this._shouldCaptureUserAudio()) {
6806
- this._initializeVAD();
6822
+ await this._initializeVAD();
6807
6823
  }
6808
6824
  }
6809
6825
  /**
@@ -6825,7 +6841,8 @@ class LayercodeClient {
6825
6841
  };
6826
6842
  });
6827
6843
  this.deviceChangeListener = async (devices) => {
6828
- var _a;
6844
+ var _a, _b;
6845
+ console.log('deviceChangeListener called, devices:', devices.length, 'recorderStarted:', this.recorderStarted, '_skipFirstDeviceCallback:', this._skipFirstDeviceCallback);
6829
6846
  try {
6830
6847
  // Notify user that devices have changed
6831
6848
  this.options.onDevicesChanged(devices);
@@ -6833,7 +6850,17 @@ class LayercodeClient {
6833
6850
  const usingDefaultDevice = this.useSystemDefaultDevice;
6834
6851
  const previousDefaultDeviceKey = this.lastKnownSystemDefaultDeviceKey;
6835
6852
  const currentDefaultDeviceKey = this._getDeviceComparisonKey(defaultDevice);
6853
+ // Skip switching on the first callback after starting the recorder to avoid redundant begin() calls
6854
+ // This is set by audioInputConnect() after _startRecorderWithDevice() completes
6855
+ if (this._skipFirstDeviceCallback) {
6856
+ console.log('deviceChangeListener: skipping first callback after recorder start');
6857
+ this._skipFirstDeviceCallback = false;
6858
+ this.lastKnownSystemDefaultDeviceKey = currentDefaultDeviceKey;
6859
+ (_a = this.resolveDeviceListenerReady) === null || _a === void 0 ? void 0 : _a.call(this);
6860
+ return;
6861
+ }
6836
6862
  let shouldSwitch = !this.recorderStarted;
6863
+ console.log('deviceChangeListener: shouldSwitch initial:', shouldSwitch);
6837
6864
  if (!shouldSwitch) {
6838
6865
  if (usingDefaultDevice) {
6839
6866
  if (!defaultDevice) {
@@ -6842,8 +6869,7 @@ class LayercodeClient {
6842
6869
  else if (this.activeDeviceId && defaultDevice.deviceId !== 'default' && defaultDevice.deviceId !== this.activeDeviceId) {
6843
6870
  shouldSwitch = true;
6844
6871
  }
6845
- else if ((previousDefaultDeviceKey && previousDefaultDeviceKey !== currentDefaultDeviceKey) ||
6846
- (!previousDefaultDeviceKey && !currentDefaultDeviceKey && this.recorderStarted)) {
6872
+ else if ((previousDefaultDeviceKey && previousDefaultDeviceKey !== currentDefaultDeviceKey) || (!previousDefaultDeviceKey && !currentDefaultDeviceKey && this.recorderStarted)) {
6847
6873
  shouldSwitch = true;
6848
6874
  }
6849
6875
  }
@@ -6853,6 +6879,7 @@ class LayercodeClient {
6853
6879
  }
6854
6880
  }
6855
6881
  this.lastKnownSystemDefaultDeviceKey = currentDefaultDeviceKey;
6882
+ console.log('deviceChangeListener: final shouldSwitch:', shouldSwitch);
6856
6883
  if (shouldSwitch) {
6857
6884
  console.debug('Selecting audio input device after change');
6858
6885
  let targetDeviceId = null;
@@ -6882,7 +6909,7 @@ class LayercodeClient {
6882
6909
  this.options.onError(error instanceof Error ? error : new Error(String(error)));
6883
6910
  }
6884
6911
  finally {
6885
- (_a = this.resolveDeviceListenerReady) === null || _a === void 0 ? void 0 : _a.call(this);
6912
+ (_b = this.resolveDeviceListenerReady) === null || _b === void 0 ? void 0 : _b.call(this);
6886
6913
  }
6887
6914
  };
6888
6915
  this.wavRecorder.listenForDeviceChange(this.deviceChangeListener);
@@ -6906,6 +6933,7 @@ class LayercodeClient {
6906
6933
  this.lastKnownSystemDefaultDeviceKey = null;
6907
6934
  this.recorderStarted = false;
6908
6935
  this.readySent = false;
6936
+ this._skipFirstDeviceCallback = false;
6909
6937
  this._stopAmplitudeMonitoring();
6910
6938
  this._teardownDeviceListeners();
6911
6939
  if (this.vad) {
@@ -6941,6 +6969,81 @@ class LayercodeClient {
6941
6969
  }
6942
6970
  return null;
6943
6971
  }
6972
+ _getUserActivationState() {
6973
+ try {
6974
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
6975
+ const act = nav === null || nav === void 0 ? void 0 : nav.userActivation;
6976
+ if (act && typeof act === 'object') {
6977
+ if (typeof act.hasBeenActive === 'boolean')
6978
+ return act.hasBeenActive;
6979
+ if (typeof act.isActive === 'boolean')
6980
+ return act.isActive ? true : null;
6981
+ }
6982
+ const doc = typeof document !== 'undefined' ? document : null;
6983
+ const dact = doc === null || doc === void 0 ? void 0 : doc.userActivation;
6984
+ if (dact && typeof dact === 'object') {
6985
+ if (typeof dact.hasBeenActive === 'boolean')
6986
+ return dact.hasBeenActive;
6987
+ if (typeof dact.isActive === 'boolean')
6988
+ return dact.isActive ? true : null;
6989
+ }
6990
+ }
6991
+ catch (_a) { }
6992
+ return null;
6993
+ }
6994
+ async _isMicrophonePermissionDenied() {
6995
+ try {
6996
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
6997
+ const permissions = nav === null || nav === void 0 ? void 0 : nav.permissions;
6998
+ if (!(permissions === null || permissions === void 0 ? void 0 : permissions.query))
6999
+ return null;
7000
+ const status = await permissions.query({ name: 'microphone' });
7001
+ const state = status === null || status === void 0 ? void 0 : status.state;
7002
+ if (state === 'denied')
7003
+ return true;
7004
+ if (state === 'granted' || state === 'prompt')
7005
+ return false;
7006
+ }
7007
+ catch (_a) { }
7008
+ return null;
7009
+ }
7010
+ async _microphonePermissionDeniedError(error) {
7011
+ const err = error;
7012
+ const message = typeof (err === null || err === void 0 ? void 0 : err.message) === 'string' ? err.message : typeof error === 'string' ? error : '';
7013
+ if (message === 'User has denined audio device permissions') {
7014
+ return err instanceof Error ? err : new Error(message);
7015
+ }
7016
+ const name = typeof (err === null || err === void 0 ? void 0 : err.name) === 'string' ? err.name : '';
7017
+ const isPermissionLike = name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError';
7018
+ if (!isPermissionLike) {
7019
+ return null;
7020
+ }
7021
+ const micDenied = await this._isMicrophonePermissionDenied();
7022
+ if (micDenied === true || /permission denied/i.test(message)) {
7023
+ return new Error('User has denined audio device permissions');
7024
+ }
7025
+ return null;
7026
+ }
7027
+ async _shouldWarnAudioDevicesRequireUserGesture(error) {
7028
+ const e = error;
7029
+ const name = typeof (e === null || e === void 0 ? void 0 : e.name) === 'string' ? e.name : '';
7030
+ const msg = typeof (e === null || e === void 0 ? void 0 : e.message) === 'string'
7031
+ ? e.message
7032
+ : typeof error === 'string'
7033
+ ? error
7034
+ : '';
7035
+ const isPermissionLike = name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError';
7036
+ if (!isPermissionLike)
7037
+ return false;
7038
+ // If the browser can tell us mic permission is explicitly denied, don't show the "user gesture" guidance.
7039
+ const micDenied = await this._isMicrophonePermissionDenied();
7040
+ if (micDenied === true)
7041
+ return false;
7042
+ if (/user activation|user gesture|interacte?d? with( the)? (page|document)|before user has interacted/i.test(msg)) {
7043
+ return true;
7044
+ }
7045
+ return this._getUserActivationState() === false;
7046
+ }
6944
7047
  /**
6945
7048
  * Mutes the microphone to stop sending audio to the server
6946
7049
  * The connection and recording remain active for quick unmute
@@ -6957,13 +7060,13 @@ class LayercodeClient {
6957
7060
  /**
6958
7061
  * Unmutes the microphone to resume sending audio to the server
6959
7062
  */
6960
- unmute() {
7063
+ async unmute() {
6961
7064
  if (this.isMuted) {
6962
7065
  this.isMuted = false;
6963
7066
  console.log('Microphone unmuted');
6964
7067
  this.options.onMuteStateChange(false);
6965
7068
  if (this.audioInput && this.recorderStarted) {
6966
- this._initializeVAD();
7069
+ await this._initializeVAD();
6967
7070
  if (this.stopRecorderAmplitude === undefined) {
6968
7071
  this._setupAmplitudeMonitoring(this.wavRecorder, this.options.onUserAmplitudeChange, (amp) => (this.userAudioAmplitude = amp));
6969
7072
  }