@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
|
|
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.
|
|
998
|
-
|
|
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
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
|
1350
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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' :
|
|
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' : (
|
|
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
|
-
|
|
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
|
-
|
|
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');
|