@layercode/js-sdk 2.1.2 → 2.1.3

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.
@@ -808,6 +808,8 @@ class AudioProcessor extends AudioWorkletProcessor {
808
808
  this.foundAudio = false;
809
809
  this.recording = false;
810
810
  this.chunks = [];
811
+ this.downsampleRatio = 1;
812
+ this.downsampleOffset = 0;
811
813
  }
812
814
 
813
815
  /**
@@ -914,9 +916,12 @@ class AudioProcessor extends AudioWorkletProcessor {
914
916
  }
915
917
 
916
918
  receive(e) {
917
- const { event, id } = e.data;
919
+ const { event, id, data } = e.data;
918
920
  let receiptData = {};
919
921
  switch (event) {
922
+ case 'configure':
923
+ this.configure(data);
924
+ return;
920
925
  case 'start':
921
926
  this.recording = true;
922
927
  break;
@@ -939,6 +944,24 @@ class AudioProcessor extends AudioWorkletProcessor {
939
944
  this.port.postMessage({ event: 'receipt', id, data: receiptData });
940
945
  }
941
946
 
947
+ configure(config = {}) {
948
+ const inputSampleRate = config?.inputSampleRate;
949
+ const targetSampleRate = config?.targetSampleRate;
950
+ if (
951
+ typeof inputSampleRate === 'number' &&
952
+ inputSampleRate > 0 &&
953
+ typeof targetSampleRate === 'number' &&
954
+ targetSampleRate > 0
955
+ ) {
956
+ if (inputSampleRate <= targetSampleRate) {
957
+ this.downsampleRatio = 1;
958
+ } else {
959
+ this.downsampleRatio = inputSampleRate / targetSampleRate;
960
+ }
961
+ this.downsampleOffset = 0;
962
+ }
963
+ }
964
+
942
965
  sendChunk(chunk) {
943
966
  const channels = this.readChannelData([chunk]);
944
967
  const { float32Array, meanValues } = this.formatAudioData(channels);
@@ -991,14 +1014,40 @@ class AudioProcessor extends AudioWorkletProcessor {
991
1014
  }
992
1015
  }
993
1016
  if (inputs && inputs[0] && this.foundAudio && this.recording) {
994
- // We need to copy the TypedArray, because the \`process\`
1017
+ // We need to copy the TypedArray, because the \`process\`
995
1018
  // internals will reuse the same buffer to hold each input
996
1019
  const chunk = inputs.map((input) => input.slice(sliceIndex));
997
- this.chunks.push(chunk);
998
- this.sendChunk(chunk);
1020
+ const processedChunk = this.downsampleChunk(chunk);
1021
+ if (processedChunk[0] && processedChunk[0].length) {
1022
+ this.chunks.push(processedChunk);
1023
+ this.sendChunk(processedChunk);
1024
+ }
999
1025
  }
1000
1026
  return true;
1001
1027
  }
1028
+
1029
+ downsampleChunk(chunk) {
1030
+ if (this.downsampleRatio === 1) {
1031
+ return chunk;
1032
+ }
1033
+ const channelCount = chunk.length;
1034
+ if (!channelCount || !chunk[0]?.length) {
1035
+ return chunk;
1036
+ }
1037
+ const ratio = this.downsampleRatio;
1038
+ const inputLength = chunk[0].length;
1039
+ const outputs = Array.from({ length: channelCount }, () => []);
1040
+ let offset = this.downsampleOffset;
1041
+ while (offset < inputLength) {
1042
+ const sampleIndex = Math.floor(offset);
1043
+ for (let c = 0; c < channelCount; c++) {
1044
+ outputs[c].push(chunk[c][sampleIndex]);
1045
+ }
1046
+ offset += ratio;
1047
+ }
1048
+ this.downsampleOffset = offset - inputLength;
1049
+ return outputs.map((samples) => Float32Array.from(samples));
1050
+ }
1002
1051
  }
1003
1052
 
1004
1053
  registerProcessor('audio_processor', AudioProcessor);
@@ -1044,10 +1093,13 @@ registerProcessor('audio_processor', AudioProcessor);
1044
1093
  this._devices = [];
1045
1094
  // State variables
1046
1095
  this.stream = null;
1096
+ this.audioContext = null;
1047
1097
  this.processor = null;
1048
1098
  this.source = null;
1049
1099
  this.node = null;
1100
+ this.analyser = null;
1050
1101
  this.recording = false;
1102
+ this.contextSampleRate = sampleRate;
1051
1103
  // Event handling with AudioWorklet
1052
1104
  this._lastEventId = 0;
1053
1105
  this.eventReceipts = {};
@@ -1256,20 +1308,53 @@ registerProcessor('audio_processor', AudioProcessor);
1256
1308
  * @returns {Promise<true>}
1257
1309
  */
1258
1310
  async requestPermission() {
1259
- const permissionStatus = await navigator.permissions.query({
1260
- name: 'microphone',
1261
- });
1262
- if (permissionStatus.state === 'denied') {
1263
- window.alert('You must grant microphone access to use this feature.');
1264
- } else if (permissionStatus.state === 'prompt') {
1311
+ const ensureUserMediaAccess = async () => {
1312
+ const stream = await navigator.mediaDevices.getUserMedia({
1313
+ audio: true,
1314
+ });
1315
+ const tracks = stream.getTracks();
1316
+ tracks.forEach((track) => track.stop());
1317
+ };
1318
+
1319
+ const permissionsUnsupported =
1320
+ !navigator.permissions ||
1321
+ typeof navigator.permissions.query !== 'function';
1322
+
1323
+ if (permissionsUnsupported) {
1265
1324
  try {
1266
- const stream = await navigator.mediaDevices.getUserMedia({
1267
- audio: true,
1268
- });
1269
- const tracks = stream.getTracks();
1270
- tracks.forEach((track) => track.stop());
1271
- } catch (e) {
1325
+ await ensureUserMediaAccess();
1326
+ } catch (error) {
1327
+ window.alert('You must grant microphone access to use this feature.');
1328
+ throw error;
1329
+ }
1330
+ return true;
1331
+ }
1332
+
1333
+ try {
1334
+ const permissionStatus = await navigator.permissions.query({
1335
+ name: 'microphone',
1336
+ });
1337
+
1338
+ if (permissionStatus.state === 'denied') {
1339
+ window.alert('You must grant microphone access to use this feature.');
1340
+ return true;
1341
+ }
1342
+
1343
+ if (permissionStatus.state === 'prompt') {
1344
+ try {
1345
+ await ensureUserMediaAccess();
1346
+ } catch (error) {
1347
+ window.alert('You must grant microphone access to use this feature.');
1348
+ throw error;
1349
+ }
1350
+ }
1351
+ } catch (error) {
1352
+ // Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
1353
+ try {
1354
+ await ensureUserMediaAccess();
1355
+ } catch (fallbackError) {
1272
1356
  window.alert('You must grant microphone access to use this feature.');
1357
+ throw fallbackError;
1273
1358
  }
1274
1359
  }
1275
1360
  return true;
@@ -1305,6 +1390,10 @@ registerProcessor('audio_processor', AudioProcessor);
1305
1390
  }
1306
1391
  defaultDevice.default = true;
1307
1392
  deviceList.push(defaultDevice);
1393
+ } else if (audioDevices.length) {
1394
+ const fallbackDefault = audioDevices.shift();
1395
+ fallbackDefault.default = true;
1396
+ deviceList.push(fallbackDefault);
1308
1397
  }
1309
1398
  return deviceList.concat(audioDevices);
1310
1399
  }
@@ -1346,8 +1435,36 @@ registerProcessor('audio_processor', AudioProcessor);
1346
1435
  throw new Error('Could not start media stream');
1347
1436
  }
1348
1437
 
1349
- const context = new AudioContext({ sampleRate: this.sampleRate });
1350
- const source = context.createMediaStreamSource(this.stream);
1438
+ const createContext = (rate) => {
1439
+ try {
1440
+ return rate ? new AudioContext({ sampleRate: rate }) : new AudioContext();
1441
+ } catch (error) {
1442
+ console.warn('Failed to create AudioContext with sampleRate', rate, error);
1443
+ return null;
1444
+ }
1445
+ };
1446
+
1447
+ let context = createContext(this.sampleRate);
1448
+ if (!context) {
1449
+ context = createContext();
1450
+ }
1451
+ if (!context) {
1452
+ throw new Error('Could not create AudioContext');
1453
+ }
1454
+
1455
+ let source;
1456
+ try {
1457
+ source = context.createMediaStreamSource(this.stream);
1458
+ } catch (error) {
1459
+ await context.close().catch(() => {});
1460
+ context = createContext();
1461
+ if (!context) {
1462
+ throw error;
1463
+ }
1464
+ source = context.createMediaStreamSource(this.stream);
1465
+ }
1466
+
1467
+ this.contextSampleRate = context.sampleRate;
1351
1468
  // Load and execute the module script.
1352
1469
  try {
1353
1470
  await context.audioWorklet.addModule(this.scriptSrc);
@@ -1383,6 +1500,14 @@ registerProcessor('audio_processor', AudioProcessor);
1383
1500
  }
1384
1501
  };
1385
1502
 
1503
+ processor.port.postMessage({
1504
+ event: 'configure',
1505
+ data: {
1506
+ inputSampleRate: this.contextSampleRate,
1507
+ targetSampleRate: this.sampleRate,
1508
+ },
1509
+ });
1510
+
1386
1511
  const node = source.connect(processor);
1387
1512
  const analyser = context.createAnalyser();
1388
1513
  analyser.fftSize = 8192;
@@ -1398,6 +1523,15 @@ registerProcessor('audio_processor', AudioProcessor);
1398
1523
  analyser.connect(context.destination);
1399
1524
  }
1400
1525
 
1526
+ if (context.state === 'suspended') {
1527
+ try {
1528
+ await context.resume();
1529
+ } catch (resumeError) {
1530
+ console.warn('AudioContext resume failed', resumeError);
1531
+ }
1532
+ }
1533
+
1534
+ this.audioContext = context;
1401
1535
  this.source = source;
1402
1536
  this.node = node;
1403
1537
  this.analyser = analyser;
@@ -1595,6 +1729,17 @@ registerProcessor('audio_processor', AudioProcessor);
1595
1729
  this.processor = null;
1596
1730
  this.source = null;
1597
1731
  this.node = null;
1732
+ this.analyser = null;
1733
+
1734
+ if (this.audioContext) {
1735
+ try {
1736
+ await this.audioContext.close();
1737
+ } catch (contextError) {
1738
+ console.warn('Failed to close AudioContext', contextError);
1739
+ }
1740
+ this.audioContext = null;
1741
+ }
1742
+ this.contextSampleRate = null;
1598
1743
 
1599
1744
  const packer = new WavPacker();
1600
1745
  const result = packer.pack(this.sampleRate, exportData.audio);
@@ -3515,7 +3660,7 @@ registerProcessor('audio_processor', AudioProcessor);
3515
3660
  const NOOP = () => { };
3516
3661
  const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
3517
3662
  // SDK version - updated when publishing
3518
- const SDK_VERSION = '2.1.2';
3663
+ const SDK_VERSION = '2.1.3';
3519
3664
  /**
3520
3665
  * @class LayercodeClient
3521
3666
  * @classdesc Core client for Layercode audio agent that manages audio recording, WebSocket communication, and speech processing.
@@ -4016,7 +4161,7 @@ registerProcessor('audio_processor', AudioProcessor);
4016
4161
  const newStream = this.wavRecorder.getStream();
4017
4162
  await this._reinitializeVAD(newStream);
4018
4163
  }
4019
- 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'));
4164
+ 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');
4020
4165
  console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
4021
4166
  }
4022
4167
  catch (error) {
@@ -4053,7 +4198,7 @@ registerProcessor('audio_processor', AudioProcessor);
4053
4198
  this.recorderStarted = true;
4054
4199
  this._sendReadyIfNeeded();
4055
4200
  }
4056
- const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
4201
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
4057
4202
  if (reportedDeviceId !== previousReportedDeviceId) {
4058
4203
  this.lastReportedDeviceId = reportedDeviceId;
4059
4204
  if (this.options.onDeviceSwitched) {