@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.
@@ -802,6 +802,8 @@ class AudioProcessor extends AudioWorkletProcessor {
802
802
  this.foundAudio = false;
803
803
  this.recording = false;
804
804
  this.chunks = [];
805
+ this.downsampleRatio = 1;
806
+ this.downsampleOffset = 0;
805
807
  }
806
808
 
807
809
  /**
@@ -908,9 +910,12 @@ class AudioProcessor extends AudioWorkletProcessor {
908
910
  }
909
911
 
910
912
  receive(e) {
911
- const { event, id } = e.data;
913
+ const { event, id, data } = e.data;
912
914
  let receiptData = {};
913
915
  switch (event) {
916
+ case 'configure':
917
+ this.configure(data);
918
+ return;
914
919
  case 'start':
915
920
  this.recording = true;
916
921
  break;
@@ -933,6 +938,24 @@ class AudioProcessor extends AudioWorkletProcessor {
933
938
  this.port.postMessage({ event: 'receipt', id, data: receiptData });
934
939
  }
935
940
 
941
+ configure(config = {}) {
942
+ const inputSampleRate = config?.inputSampleRate;
943
+ const targetSampleRate = config?.targetSampleRate;
944
+ if (
945
+ typeof inputSampleRate === 'number' &&
946
+ inputSampleRate > 0 &&
947
+ typeof targetSampleRate === 'number' &&
948
+ targetSampleRate > 0
949
+ ) {
950
+ if (inputSampleRate <= targetSampleRate) {
951
+ this.downsampleRatio = 1;
952
+ } else {
953
+ this.downsampleRatio = inputSampleRate / targetSampleRate;
954
+ }
955
+ this.downsampleOffset = 0;
956
+ }
957
+ }
958
+
936
959
  sendChunk(chunk) {
937
960
  const channels = this.readChannelData([chunk]);
938
961
  const { float32Array, meanValues } = this.formatAudioData(channels);
@@ -985,14 +1008,40 @@ class AudioProcessor extends AudioWorkletProcessor {
985
1008
  }
986
1009
  }
987
1010
  if (inputs && inputs[0] && this.foundAudio && this.recording) {
988
- // We need to copy the TypedArray, because the \`process\`
1011
+ // We need to copy the TypedArray, because the \`process\`
989
1012
  // internals will reuse the same buffer to hold each input
990
1013
  const chunk = inputs.map((input) => input.slice(sliceIndex));
991
- this.chunks.push(chunk);
992
- this.sendChunk(chunk);
1014
+ const processedChunk = this.downsampleChunk(chunk);
1015
+ if (processedChunk[0] && processedChunk[0].length) {
1016
+ this.chunks.push(processedChunk);
1017
+ this.sendChunk(processedChunk);
1018
+ }
993
1019
  }
994
1020
  return true;
995
1021
  }
1022
+
1023
+ downsampleChunk(chunk) {
1024
+ if (this.downsampleRatio === 1) {
1025
+ return chunk;
1026
+ }
1027
+ const channelCount = chunk.length;
1028
+ if (!channelCount || !chunk[0]?.length) {
1029
+ return chunk;
1030
+ }
1031
+ const ratio = this.downsampleRatio;
1032
+ const inputLength = chunk[0].length;
1033
+ const outputs = Array.from({ length: channelCount }, () => []);
1034
+ let offset = this.downsampleOffset;
1035
+ while (offset < inputLength) {
1036
+ const sampleIndex = Math.floor(offset);
1037
+ for (let c = 0; c < channelCount; c++) {
1038
+ outputs[c].push(chunk[c][sampleIndex]);
1039
+ }
1040
+ offset += ratio;
1041
+ }
1042
+ this.downsampleOffset = offset - inputLength;
1043
+ return outputs.map((samples) => Float32Array.from(samples));
1044
+ }
996
1045
  }
997
1046
 
998
1047
  registerProcessor('audio_processor', AudioProcessor);
@@ -1038,10 +1087,13 @@ class WavRecorder {
1038
1087
  this._devices = [];
1039
1088
  // State variables
1040
1089
  this.stream = null;
1090
+ this.audioContext = null;
1041
1091
  this.processor = null;
1042
1092
  this.source = null;
1043
1093
  this.node = null;
1094
+ this.analyser = null;
1044
1095
  this.recording = false;
1096
+ this.contextSampleRate = sampleRate;
1045
1097
  // Event handling with AudioWorklet
1046
1098
  this._lastEventId = 0;
1047
1099
  this.eventReceipts = {};
@@ -1250,20 +1302,53 @@ class WavRecorder {
1250
1302
  * @returns {Promise<true>}
1251
1303
  */
1252
1304
  async requestPermission() {
1253
- const permissionStatus = await navigator.permissions.query({
1254
- name: 'microphone',
1255
- });
1256
- if (permissionStatus.state === 'denied') {
1257
- window.alert('You must grant microphone access to use this feature.');
1258
- } else if (permissionStatus.state === 'prompt') {
1305
+ const ensureUserMediaAccess = async () => {
1306
+ const stream = await navigator.mediaDevices.getUserMedia({
1307
+ audio: true,
1308
+ });
1309
+ const tracks = stream.getTracks();
1310
+ tracks.forEach((track) => track.stop());
1311
+ };
1312
+
1313
+ const permissionsUnsupported =
1314
+ !navigator.permissions ||
1315
+ typeof navigator.permissions.query !== 'function';
1316
+
1317
+ if (permissionsUnsupported) {
1259
1318
  try {
1260
- const stream = await navigator.mediaDevices.getUserMedia({
1261
- audio: true,
1262
- });
1263
- const tracks = stream.getTracks();
1264
- tracks.forEach((track) => track.stop());
1265
- } catch (e) {
1319
+ await ensureUserMediaAccess();
1320
+ } catch (error) {
1321
+ window.alert('You must grant microphone access to use this feature.');
1322
+ throw error;
1323
+ }
1324
+ return true;
1325
+ }
1326
+
1327
+ try {
1328
+ const permissionStatus = await navigator.permissions.query({
1329
+ name: 'microphone',
1330
+ });
1331
+
1332
+ if (permissionStatus.state === 'denied') {
1333
+ window.alert('You must grant microphone access to use this feature.');
1334
+ return true;
1335
+ }
1336
+
1337
+ if (permissionStatus.state === 'prompt') {
1338
+ try {
1339
+ await ensureUserMediaAccess();
1340
+ } catch (error) {
1341
+ window.alert('You must grant microphone access to use this feature.');
1342
+ throw error;
1343
+ }
1344
+ }
1345
+ } catch (error) {
1346
+ // Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
1347
+ try {
1348
+ await ensureUserMediaAccess();
1349
+ } catch (fallbackError) {
1266
1350
  window.alert('You must grant microphone access to use this feature.');
1351
+ throw fallbackError;
1267
1352
  }
1268
1353
  }
1269
1354
  return true;
@@ -1299,6 +1384,10 @@ class WavRecorder {
1299
1384
  }
1300
1385
  defaultDevice.default = true;
1301
1386
  deviceList.push(defaultDevice);
1387
+ } else if (audioDevices.length) {
1388
+ const fallbackDefault = audioDevices.shift();
1389
+ fallbackDefault.default = true;
1390
+ deviceList.push(fallbackDefault);
1302
1391
  }
1303
1392
  return deviceList.concat(audioDevices);
1304
1393
  }
@@ -1340,8 +1429,36 @@ class WavRecorder {
1340
1429
  throw new Error('Could not start media stream');
1341
1430
  }
1342
1431
 
1343
- const context = new AudioContext({ sampleRate: this.sampleRate });
1344
- const source = context.createMediaStreamSource(this.stream);
1432
+ const createContext = (rate) => {
1433
+ try {
1434
+ return rate ? new AudioContext({ sampleRate: rate }) : new AudioContext();
1435
+ } catch (error) {
1436
+ console.warn('Failed to create AudioContext with sampleRate', rate, error);
1437
+ return null;
1438
+ }
1439
+ };
1440
+
1441
+ let context = createContext(this.sampleRate);
1442
+ if (!context) {
1443
+ context = createContext();
1444
+ }
1445
+ if (!context) {
1446
+ throw new Error('Could not create AudioContext');
1447
+ }
1448
+
1449
+ let source;
1450
+ try {
1451
+ source = context.createMediaStreamSource(this.stream);
1452
+ } catch (error) {
1453
+ await context.close().catch(() => {});
1454
+ context = createContext();
1455
+ if (!context) {
1456
+ throw error;
1457
+ }
1458
+ source = context.createMediaStreamSource(this.stream);
1459
+ }
1460
+
1461
+ this.contextSampleRate = context.sampleRate;
1345
1462
  // Load and execute the module script.
1346
1463
  try {
1347
1464
  await context.audioWorklet.addModule(this.scriptSrc);
@@ -1377,6 +1494,14 @@ class WavRecorder {
1377
1494
  }
1378
1495
  };
1379
1496
 
1497
+ processor.port.postMessage({
1498
+ event: 'configure',
1499
+ data: {
1500
+ inputSampleRate: this.contextSampleRate,
1501
+ targetSampleRate: this.sampleRate,
1502
+ },
1503
+ });
1504
+
1380
1505
  const node = source.connect(processor);
1381
1506
  const analyser = context.createAnalyser();
1382
1507
  analyser.fftSize = 8192;
@@ -1392,6 +1517,15 @@ class WavRecorder {
1392
1517
  analyser.connect(context.destination);
1393
1518
  }
1394
1519
 
1520
+ if (context.state === 'suspended') {
1521
+ try {
1522
+ await context.resume();
1523
+ } catch (resumeError) {
1524
+ console.warn('AudioContext resume failed', resumeError);
1525
+ }
1526
+ }
1527
+
1528
+ this.audioContext = context;
1395
1529
  this.source = source;
1396
1530
  this.node = node;
1397
1531
  this.analyser = analyser;
@@ -1589,6 +1723,17 @@ class WavRecorder {
1589
1723
  this.processor = null;
1590
1724
  this.source = null;
1591
1725
  this.node = null;
1726
+ this.analyser = null;
1727
+
1728
+ if (this.audioContext) {
1729
+ try {
1730
+ await this.audioContext.close();
1731
+ } catch (contextError) {
1732
+ console.warn('Failed to close AudioContext', contextError);
1733
+ }
1734
+ this.audioContext = null;
1735
+ }
1736
+ this.contextSampleRate = null;
1592
1737
 
1593
1738
  const packer = new WavPacker();
1594
1739
  const result = packer.pack(this.sampleRate, exportData.audio);
@@ -3509,7 +3654,7 @@ function arrayBufferToBase64(arrayBuffer) {
3509
3654
  const NOOP = () => { };
3510
3655
  const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
3511
3656
  // SDK version - updated when publishing
3512
- const SDK_VERSION = '2.1.2';
3657
+ const SDK_VERSION = '2.1.3';
3513
3658
  /**
3514
3659
  * @class LayercodeClient
3515
3660
  * @classdesc Core client for Layercode audio agent that manages audio recording, WebSocket communication, and speech processing.
@@ -4010,7 +4155,7 @@ class LayercodeClient {
4010
4155
  const newStream = this.wavRecorder.getStream();
4011
4156
  await this._reinitializeVAD(newStream);
4012
4157
  }
4013
- 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'));
4158
+ 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');
4014
4159
  console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
4015
4160
  }
4016
4161
  catch (error) {
@@ -4047,7 +4192,7 @@ class LayercodeClient {
4047
4192
  this.recorderStarted = true;
4048
4193
  this._sendReadyIfNeeded();
4049
4194
  }
4050
- const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
4195
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
4051
4196
  if (reportedDeviceId !== previousReportedDeviceId) {
4052
4197
  this.lastReportedDeviceId = reportedDeviceId;
4053
4198
  if (this.options.onDeviceSwitched) {