@layercode/js-sdk 2.1.2 → 2.1.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.
@@ -801,6 +801,8 @@ class AudioProcessor extends AudioWorkletProcessor {
801
801
  constructor() {
802
802
  super();
803
803
  this.port.onmessage = this.receive.bind(this);
804
+ this.downsampleRatio = 1;
805
+ this.downsampleOffset = 0;
804
806
  this.initialize();
805
807
  }
806
808
 
@@ -808,6 +810,7 @@ class AudioProcessor extends AudioWorkletProcessor {
808
810
  this.foundAudio = false;
809
811
  this.recording = false;
810
812
  this.chunks = [];
813
+ this.downsampleOffset = 0;
811
814
  }
812
815
 
813
816
  /**
@@ -914,9 +917,12 @@ class AudioProcessor extends AudioWorkletProcessor {
914
917
  }
915
918
 
916
919
  receive(e) {
917
- const { event, id } = e.data;
920
+ const { event, id, data } = e.data;
918
921
  let receiptData = {};
919
922
  switch (event) {
923
+ case 'configure':
924
+ this.configure(data);
925
+ return;
920
926
  case 'start':
921
927
  this.recording = true;
922
928
  break;
@@ -939,6 +945,24 @@ class AudioProcessor extends AudioWorkletProcessor {
939
945
  this.port.postMessage({ event: 'receipt', id, data: receiptData });
940
946
  }
941
947
 
948
+ configure(config = {}) {
949
+ const inputSampleRate = config?.inputSampleRate;
950
+ const targetSampleRate = config?.targetSampleRate;
951
+ if (
952
+ typeof inputSampleRate === 'number' &&
953
+ inputSampleRate > 0 &&
954
+ typeof targetSampleRate === 'number' &&
955
+ targetSampleRate > 0
956
+ ) {
957
+ if (inputSampleRate <= targetSampleRate) {
958
+ this.downsampleRatio = 1;
959
+ } else {
960
+ this.downsampleRatio = inputSampleRate / targetSampleRate;
961
+ }
962
+ this.downsampleOffset = 0;
963
+ }
964
+ }
965
+
942
966
  sendChunk(chunk) {
943
967
  const channels = this.readChannelData([chunk]);
944
968
  const { float32Array, meanValues } = this.formatAudioData(channels);
@@ -991,14 +1015,40 @@ class AudioProcessor extends AudioWorkletProcessor {
991
1015
  }
992
1016
  }
993
1017
  if (inputs && inputs[0] && this.foundAudio && this.recording) {
994
- // We need to copy the TypedArray, because the \`process\`
1018
+ // We need to copy the TypedArray, because the \`process\`
995
1019
  // internals will reuse the same buffer to hold each input
996
1020
  const chunk = inputs.map((input) => input.slice(sliceIndex));
997
- this.chunks.push(chunk);
998
- this.sendChunk(chunk);
1021
+ const processedChunk = this.downsampleChunk(chunk);
1022
+ if (processedChunk[0] && processedChunk[0].length) {
1023
+ this.chunks.push(processedChunk);
1024
+ this.sendChunk(processedChunk);
1025
+ }
999
1026
  }
1000
1027
  return true;
1001
1028
  }
1029
+
1030
+ downsampleChunk(chunk) {
1031
+ if (this.downsampleRatio === 1) {
1032
+ return chunk;
1033
+ }
1034
+ const channelCount = chunk.length;
1035
+ if (!channelCount || !chunk[0]?.length) {
1036
+ return chunk;
1037
+ }
1038
+ const ratio = this.downsampleRatio;
1039
+ const inputLength = chunk[0].length;
1040
+ const outputs = Array.from({ length: channelCount }, () => []);
1041
+ let offset = this.downsampleOffset;
1042
+ while (offset < inputLength) {
1043
+ const sampleIndex = Math.floor(offset);
1044
+ for (let c = 0; c < channelCount; c++) {
1045
+ outputs[c].push(chunk[c][sampleIndex]);
1046
+ }
1047
+ offset += ratio;
1048
+ }
1049
+ this.downsampleOffset = offset - inputLength;
1050
+ return outputs.map((samples) => Float32Array.from(samples));
1051
+ }
1002
1052
  }
1003
1053
 
1004
1054
  registerProcessor('audio_processor', AudioProcessor);
@@ -1044,10 +1094,13 @@ registerProcessor('audio_processor', AudioProcessor);
1044
1094
  this._devices = [];
1045
1095
  // State variables
1046
1096
  this.stream = null;
1097
+ this.audioContext = null;
1047
1098
  this.processor = null;
1048
1099
  this.source = null;
1049
1100
  this.node = null;
1101
+ this.analyser = null;
1050
1102
  this.recording = false;
1103
+ this.contextSampleRate = sampleRate;
1051
1104
  // Event handling with AudioWorklet
1052
1105
  this._lastEventId = 0;
1053
1106
  this.eventReceipts = {};
@@ -1256,20 +1309,53 @@ registerProcessor('audio_processor', AudioProcessor);
1256
1309
  * @returns {Promise<true>}
1257
1310
  */
1258
1311
  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') {
1312
+ const ensureUserMediaAccess = async () => {
1313
+ const stream = await navigator.mediaDevices.getUserMedia({
1314
+ audio: true,
1315
+ });
1316
+ const tracks = stream.getTracks();
1317
+ tracks.forEach((track) => track.stop());
1318
+ };
1319
+
1320
+ const permissionsUnsupported =
1321
+ !navigator.permissions ||
1322
+ typeof navigator.permissions.query !== 'function';
1323
+
1324
+ if (permissionsUnsupported) {
1265
1325
  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) {
1326
+ await ensureUserMediaAccess();
1327
+ } catch (error) {
1328
+ window.alert('You must grant microphone access to use this feature.');
1329
+ throw error;
1330
+ }
1331
+ return true;
1332
+ }
1333
+
1334
+ try {
1335
+ const permissionStatus = await navigator.permissions.query({
1336
+ name: 'microphone',
1337
+ });
1338
+
1339
+ if (permissionStatus.state === 'denied') {
1340
+ window.alert('You must grant microphone access to use this feature.');
1341
+ return true;
1342
+ }
1343
+
1344
+ if (permissionStatus.state === 'prompt') {
1345
+ try {
1346
+ await ensureUserMediaAccess();
1347
+ } catch (error) {
1348
+ window.alert('You must grant microphone access to use this feature.');
1349
+ throw error;
1350
+ }
1351
+ }
1352
+ } catch (error) {
1353
+ // Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
1354
+ try {
1355
+ await ensureUserMediaAccess();
1356
+ } catch (fallbackError) {
1272
1357
  window.alert('You must grant microphone access to use this feature.');
1358
+ throw fallbackError;
1273
1359
  }
1274
1360
  }
1275
1361
  return true;
@@ -1305,6 +1391,10 @@ registerProcessor('audio_processor', AudioProcessor);
1305
1391
  }
1306
1392
  defaultDevice.default = true;
1307
1393
  deviceList.push(defaultDevice);
1394
+ } else if (audioDevices.length) {
1395
+ const fallbackDefault = audioDevices.shift();
1396
+ fallbackDefault.default = true;
1397
+ deviceList.push(fallbackDefault);
1308
1398
  }
1309
1399
  return deviceList.concat(audioDevices);
1310
1400
  }
@@ -1346,8 +1436,36 @@ registerProcessor('audio_processor', AudioProcessor);
1346
1436
  throw new Error('Could not start media stream');
1347
1437
  }
1348
1438
 
1349
- const context = new AudioContext({ sampleRate: this.sampleRate });
1350
- const source = context.createMediaStreamSource(this.stream);
1439
+ const createContext = (rate) => {
1440
+ try {
1441
+ return rate ? new AudioContext({ sampleRate: rate }) : new AudioContext();
1442
+ } catch (error) {
1443
+ console.warn('Failed to create AudioContext with sampleRate', rate, error);
1444
+ return null;
1445
+ }
1446
+ };
1447
+
1448
+ let context = createContext(this.sampleRate);
1449
+ if (!context) {
1450
+ context = createContext();
1451
+ }
1452
+ if (!context) {
1453
+ throw new Error('Could not create AudioContext');
1454
+ }
1455
+
1456
+ let source;
1457
+ try {
1458
+ source = context.createMediaStreamSource(this.stream);
1459
+ } catch (error) {
1460
+ await context.close().catch(() => {});
1461
+ context = createContext();
1462
+ if (!context) {
1463
+ throw error;
1464
+ }
1465
+ source = context.createMediaStreamSource(this.stream);
1466
+ }
1467
+
1468
+ this.contextSampleRate = context.sampleRate;
1351
1469
  // Load and execute the module script.
1352
1470
  try {
1353
1471
  await context.audioWorklet.addModule(this.scriptSrc);
@@ -1383,6 +1501,14 @@ registerProcessor('audio_processor', AudioProcessor);
1383
1501
  }
1384
1502
  };
1385
1503
 
1504
+ processor.port.postMessage({
1505
+ event: 'configure',
1506
+ data: {
1507
+ inputSampleRate: this.contextSampleRate,
1508
+ targetSampleRate: this.sampleRate,
1509
+ },
1510
+ });
1511
+
1386
1512
  const node = source.connect(processor);
1387
1513
  const analyser = context.createAnalyser();
1388
1514
  analyser.fftSize = 8192;
@@ -1398,6 +1524,15 @@ registerProcessor('audio_processor', AudioProcessor);
1398
1524
  analyser.connect(context.destination);
1399
1525
  }
1400
1526
 
1527
+ if (context.state === 'suspended') {
1528
+ try {
1529
+ await context.resume();
1530
+ } catch (resumeError) {
1531
+ console.warn('AudioContext resume failed', resumeError);
1532
+ }
1533
+ }
1534
+
1535
+ this.audioContext = context;
1401
1536
  this.source = source;
1402
1537
  this.node = node;
1403
1538
  this.analyser = analyser;
@@ -1595,6 +1730,17 @@ registerProcessor('audio_processor', AudioProcessor);
1595
1730
  this.processor = null;
1596
1731
  this.source = null;
1597
1732
  this.node = null;
1733
+ this.analyser = null;
1734
+
1735
+ if (this.audioContext) {
1736
+ try {
1737
+ await this.audioContext.close();
1738
+ } catch (contextError) {
1739
+ console.warn('Failed to close AudioContext', contextError);
1740
+ }
1741
+ this.audioContext = null;
1742
+ }
1743
+ this.contextSampleRate = null;
1598
1744
 
1599
1745
  const packer = new WavPacker();
1600
1746
  const result = packer.pack(this.sampleRate, exportData.audio);
@@ -3515,7 +3661,7 @@ registerProcessor('audio_processor', AudioProcessor);
3515
3661
  const NOOP = () => { };
3516
3662
  const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
3517
3663
  // SDK version - updated when publishing
3518
- const SDK_VERSION = '2.1.2';
3664
+ const SDK_VERSION = '2.1.3';
3519
3665
  /**
3520
3666
  * @class LayercodeClient
3521
3667
  * @classdesc Core client for Layercode audio agent that manages audio recording, WebSocket communication, and speech processing.
@@ -3872,22 +4018,14 @@ registerProcessor('audio_processor', AudioProcessor);
3872
4018
  this.stopRecorderAmplitude = undefined;
3873
4019
  }
3874
4020
  /**
3875
- * Connects to the Layercode agent and starts the audio conversation
4021
+ * Connects to the Layercode agent using the stored conversation ID and starts the audio conversation
3876
4022
  * @async
3877
4023
  * @returns {Promise<void>}
3878
4024
  */
3879
- async connect(opts) {
4025
+ async connect() {
3880
4026
  if (this.status === 'connecting') {
3881
4027
  return;
3882
4028
  }
3883
- if (opts === null || opts === void 0 ? void 0 : opts.newConversation) {
3884
- this.options.conversationId = null;
3885
- this.conversationId = null;
3886
- }
3887
- else if (opts === null || opts === void 0 ? void 0 : opts.conversationId) {
3888
- this.options.conversationId = opts.conversationId;
3889
- this.conversationId = opts.conversationId;
3890
- }
3891
4029
  try {
3892
4030
  this._setStatus('connecting');
3893
4031
  // Reset turn tracking for clean start
@@ -3976,7 +4114,7 @@ registerProcessor('audio_processor', AudioProcessor);
3976
4114
  this.currentTurnId = null;
3977
4115
  console.debug('Reset turn tracking state');
3978
4116
  }
3979
- async disconnect(opts) {
4117
+ async disconnect() {
3980
4118
  if (this.status === 'disconnected') {
3981
4119
  return;
3982
4120
  }
@@ -3988,7 +4126,7 @@ registerProcessor('audio_processor', AudioProcessor);
3988
4126
  this.ws.close();
3989
4127
  this.ws = null;
3990
4128
  }
3991
- await this._performDisconnectCleanup(opts === null || opts === void 0 ? void 0 : opts.clearConversationId);
4129
+ await this._performDisconnectCleanup();
3992
4130
  }
3993
4131
  /**
3994
4132
  * Gets the microphone MediaStream used by this client
@@ -4016,7 +4154,7 @@ registerProcessor('audio_processor', AudioProcessor);
4016
4154
  const newStream = this.wavRecorder.getStream();
4017
4155
  await this._reinitializeVAD(newStream);
4018
4156
  }
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'));
4157
+ 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
4158
  console.debug(`Successfully switched to input device: ${reportedDeviceId}`);
4021
4159
  }
4022
4160
  catch (error) {
@@ -4053,7 +4191,7 @@ registerProcessor('audio_processor', AudioProcessor);
4053
4191
  this.recorderStarted = true;
4054
4192
  this._sendReadyIfNeeded();
4055
4193
  }
4056
- const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : ((_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default'));
4194
+ const reportedDeviceId = (_a = this.activeDeviceId) !== null && _a !== void 0 ? _a : (this.useSystemDefaultDevice ? 'default' : (_b = this.deviceId) !== null && _b !== void 0 ? _b : 'default');
4057
4195
  if (reportedDeviceId !== previousReportedDeviceId) {
4058
4196
  this.lastReportedDeviceId = reportedDeviceId;
4059
4197
  if (this.options.onDeviceSwitched) {
@@ -4135,7 +4273,10 @@ registerProcessor('audio_processor', AudioProcessor);
4135
4273
  _teardownDeviceListeners() {
4136
4274
  this.wavRecorder.listenForDeviceChange(null);
4137
4275
  }
4138
- async _performDisconnectCleanup(clearConversationId) {
4276
+ /**
4277
+ * Releases audio resources and listeners after a disconnect
4278
+ */
4279
+ async _performDisconnectCleanup() {
4139
4280
  var _a, _b;
4140
4281
  this.deviceId = null;
4141
4282
  this.activeDeviceId = null;
@@ -4155,13 +4296,7 @@ registerProcessor('audio_processor', AudioProcessor);
4155
4296
  (_b = (_a = this.wavPlayer).stop) === null || _b === void 0 ? void 0 : _b.call(_a);
4156
4297
  this.wavPlayer.disconnect();
4157
4298
  this._resetTurnTracking();
4158
- if (clearConversationId) {
4159
- this.options.conversationId = null;
4160
- this.conversationId = null;
4161
- }
4162
- else {
4163
- this.options.conversationId = this.conversationId;
4164
- }
4299
+ this.options.conversationId = this.conversationId;
4165
4300
  this.userAudioAmplitude = 0;
4166
4301
  this.agentAudioAmplitude = 0;
4167
4302
  this._setStatus('disconnected');