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