@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.
- package/README.md +15 -5
- package/dist/layercode-js-sdk.esm.js +345 -242
- package/dist/layercode-js-sdk.esm.js.map +1 -1
- package/dist/layercode-js-sdk.min.js +345 -242
- package/dist/layercode-js-sdk.min.js.map +1 -1
- package/dist/types/index.d.ts +21 -3
- package/dist/types/interfaces.d.ts +6 -2
- package/dist/types/wavtools/lib/analysis/audio_analysis.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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}
|
|
5192
|
+
* @param {...any} args
|
|
5200
5193
|
* @returns {true}
|
|
5201
5194
|
*/
|
|
5202
|
-
log() {
|
|
5195
|
+
log(...args) {
|
|
5203
5196
|
if (this.debug) {
|
|
5204
|
-
|
|
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
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
5773
|
-
const MEDIA_DEVICE_KIND_AUDIO = 'audioinput';
|
|
5753
|
+
const DEFAULT_RECORDER_SAMPLE_RATE = 8000;
|
|
5774
5754
|
const hasMediaDevicesSupport = () => typeof navigator !== 'undefined' && !!navigator.mediaDevices;
|
|
5775
|
-
|
|
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
|
-
|
|
5813
|
-
groupId: device.groupId,
|
|
5814
|
-
kind: device.kind,
|
|
5757
|
+
...device,
|
|
5815
5758
|
label: device.label,
|
|
5816
|
-
default:
|
|
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
|
-
|
|
5850
|
-
const devices = await
|
|
5851
|
-
return
|
|
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
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
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
|
-
|
|
5881
|
-
|
|
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
|
-
|
|
5902
|
-
|
|
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:
|
|
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
|
-
|
|
6084
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
}
|