@layercode/js-sdk 2.0.5 → 2.1.0

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.
@@ -3228,6 +3228,8 @@ class StreamProcessor extends AudioWorkletProcessor {
3228
3228
  this.isPaused = true;
3229
3229
  } else if (payload.event === 'play') {
3230
3230
  this.isPaused = false;
3231
+ } else if (payload.event === 'stop') {
3232
+ this.hasInterrupted = true;
3231
3233
  } else {
3232
3234
  throw new Error(\`Unhandled event "\${payload.event}"\`);
3233
3235
  }
@@ -3324,6 +3326,7 @@ class WavStreamPlayer {
3324
3326
  this.interruptedTrackIds = {};
3325
3327
  this.finishedPlayingCallback = finishedPlayingCallback;
3326
3328
  this.isPlaying = false;
3329
+ this.amplitudeMonitorRaf = undefined;
3327
3330
  }
3328
3331
 
3329
3332
  /**
@@ -3409,14 +3412,22 @@ class WavStreamPlayer {
3409
3412
  * @param {function} callback - Function to call with amplitude value
3410
3413
  */
3411
3414
  startAmplitudeMonitoring(callback) {
3415
+ this.stopAmplitudeMonitoring();
3412
3416
  const monitor = () => {
3413
3417
  const amplitude = this.getAmplitude();
3414
3418
  callback(amplitude);
3415
- requestAnimationFrame(monitor);
3419
+ this.amplitudeMonitorRaf = requestAnimationFrame(monitor);
3416
3420
  };
3417
3421
  monitor();
3418
3422
  }
3419
3423
 
3424
+ stopAmplitudeMonitoring() {
3425
+ if (this.amplitudeMonitorRaf !== undefined) {
3426
+ cancelAnimationFrame(this.amplitudeMonitorRaf);
3427
+ this.amplitudeMonitorRaf = undefined;
3428
+ }
3429
+ }
3430
+
3420
3431
  /**
3421
3432
  * Starts audio streaming
3422
3433
  * @private
@@ -3563,11 +3574,18 @@ class WavStreamPlayer {
3563
3574
  return true;
3564
3575
  }
3565
3576
 
3577
+ stop() {
3578
+ if (this.stream) {
3579
+ this.stream.port.postMessage({ event: 'stop' });
3580
+ }
3581
+ }
3582
+
3566
3583
  /**
3567
3584
  * Disconnects the audio context and cleans up resources
3568
3585
  * @returns {void}
3569
3586
  */
3570
3587
  disconnect() {
3588
+ this.stopAmplitudeMonitoring();
3571
3589
  if (this.stream) {
3572
3590
  this.stream.disconnect();
3573
3591
  this.stream = null;
@@ -3851,6 +3869,7 @@ class WavRecorder {
3851
3869
  raw: new ArrayBuffer(0),
3852
3870
  mono: new ArrayBuffer(0),
3853
3871
  };
3872
+ this.amplitudeMonitorRaf = void 0;
3854
3873
  }
3855
3874
 
3856
3875
  /**
@@ -4250,14 +4269,22 @@ class WavRecorder {
4250
4269
  * @param {function} callback - Function to call with amplitude value
4251
4270
  */
4252
4271
  startAmplitudeMonitoring(callback) {
4272
+ this.stopAmplitudeMonitoring();
4253
4273
  const monitor = () => {
4254
4274
  const amplitude = this.getAmplitude();
4255
4275
  callback(amplitude);
4256
- requestAnimationFrame(monitor);
4276
+ this.amplitudeMonitorRaf = requestAnimationFrame(monitor);
4257
4277
  };
4258
4278
  monitor();
4259
4279
  }
4260
4280
 
4281
+ stopAmplitudeMonitoring() {
4282
+ if (this.amplitudeMonitorRaf !== void 0) {
4283
+ cancelAnimationFrame(this.amplitudeMonitorRaf);
4284
+ this.amplitudeMonitorRaf = void 0;
4285
+ }
4286
+ }
4287
+
4261
4288
  /**
4262
4289
  * Pauses the recording
4263
4290
  * Keeps microphone stream open but halts storage of audio
@@ -4390,6 +4417,7 @@ class WavRecorder {
4390
4417
  * @returns {Promise<true>}
4391
4418
  */
4392
4419
  async quit() {
4420
+ this.stopAmplitudeMonitoring();
4393
4421
  this.listenForDeviceChange(null);
4394
4422
  if (this.processor) {
4395
4423
  await this.end();
@@ -6293,6 +6321,8 @@ function arrayBufferToBase64(arrayBuffer) {
6293
6321
  }
6294
6322
 
6295
6323
  var _a;
6324
+ const NOOP = () => { };
6325
+ const DEFAULT_WS_URL = 'wss://api.layercode.com/v1/agents/web/websocket';
6296
6326
  // SDK version - updated when publishing
6297
6327
  const SDK_VERSION = '2.0.2';
6298
6328
  const ORT_WARNING_MUTE_LEVEL = 'error';
@@ -6351,27 +6381,28 @@ class LayercodeClient {
6351
6381
  * @param {Object} options - Configuration options
6352
6382
  */
6353
6383
  constructor(options) {
6384
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
6354
6385
  this.deviceId = null;
6355
6386
  this.options = {
6356
6387
  agentId: options.agentId,
6357
- conversationId: options.conversationId || null,
6388
+ conversationId: (_a = options.conversationId) !== null && _a !== void 0 ? _a : null,
6358
6389
  authorizeSessionEndpoint: options.authorizeSessionEndpoint,
6359
- metadata: options.metadata || {},
6360
- vadResumeDelay: options.vadResumeDelay || 500,
6361
- onConnect: options.onConnect || (() => { }),
6362
- onDisconnect: options.onDisconnect || (() => { }),
6363
- onError: options.onError || (() => { }),
6364
- onDeviceSwitched: options.onDeviceSwitched || (() => { }),
6365
- onDataMessage: options.onDataMessage || (() => { }),
6366
- onMessage: options.onMessage || (() => { }),
6367
- onUserAmplitudeChange: options.onUserAmplitudeChange || (() => { }),
6368
- onAgentAmplitudeChange: options.onAgentAmplitudeChange || (() => { }),
6369
- onStatusChange: options.onStatusChange || (() => { }),
6370
- onUserIsSpeakingChange: options.onUserIsSpeakingChange || (() => { }),
6371
- onMuteStateChange: options.onMuteStateChange || (() => { }),
6390
+ metadata: (_b = options.metadata) !== null && _b !== void 0 ? _b : {},
6391
+ vadResumeDelay: (_c = options.vadResumeDelay) !== null && _c !== void 0 ? _c : 500,
6392
+ onConnect: (_d = options.onConnect) !== null && _d !== void 0 ? _d : NOOP,
6393
+ onDisconnect: (_e = options.onDisconnect) !== null && _e !== void 0 ? _e : NOOP,
6394
+ onError: (_f = options.onError) !== null && _f !== void 0 ? _f : NOOP,
6395
+ onDeviceSwitched: (_g = options.onDeviceSwitched) !== null && _g !== void 0 ? _g : NOOP,
6396
+ onDataMessage: (_h = options.onDataMessage) !== null && _h !== void 0 ? _h : NOOP,
6397
+ onMessage: (_j = options.onMessage) !== null && _j !== void 0 ? _j : NOOP,
6398
+ onUserAmplitudeChange: (_k = options.onUserAmplitudeChange) !== null && _k !== void 0 ? _k : NOOP,
6399
+ onAgentAmplitudeChange: (_l = options.onAgentAmplitudeChange) !== null && _l !== void 0 ? _l : NOOP,
6400
+ onStatusChange: (_m = options.onStatusChange) !== null && _m !== void 0 ? _m : NOOP,
6401
+ onUserIsSpeakingChange: (_o = options.onUserIsSpeakingChange) !== null && _o !== void 0 ? _o : NOOP,
6402
+ onMuteStateChange: (_p = options.onMuteStateChange) !== null && _p !== void 0 ? _p : NOOP,
6372
6403
  };
6373
6404
  this.AMPLITUDE_MONITORING_SAMPLE_RATE = 2;
6374
- this._websocketUrl = 'wss://api.layercode.com/v1/agents/web/websocket';
6405
+ this._websocketUrl = DEFAULT_WS_URL;
6375
6406
  this.wavRecorder = new WavRecorder({ sampleRate: 8000 }); // TODO should be set my fetched agent config
6376
6407
  this.wavPlayer = new WavStreamPlayer({
6377
6408
  finishedPlayingCallback: this._clientResponseAudioReplayFinished.bind(this),
@@ -6382,7 +6413,7 @@ class LayercodeClient {
6382
6413
  this.status = 'disconnected';
6383
6414
  this.userAudioAmplitude = 0;
6384
6415
  this.agentAudioAmplitude = 0;
6385
- this.conversationId = options.conversationId || null;
6416
+ this.conversationId = this.options.conversationId;
6386
6417
  this.pushToTalkActive = false;
6387
6418
  this.pushToTalkEnabled = false;
6388
6419
  this.canInterrupt = false;
@@ -6395,12 +6426,15 @@ class LayercodeClient {
6395
6426
  this.activeDeviceId = null;
6396
6427
  this.useSystemDefaultDevice = false;
6397
6428
  this.lastReportedDeviceId = null;
6429
+ this.lastKnownSystemDefaultDeviceKey = null;
6398
6430
  this.isMuted = false;
6431
+ this.stopPlayerAmplitude = undefined;
6432
+ this.stopRecorderAmplitude = undefined;
6433
+ this.deviceChangeListener = null;
6399
6434
  // this.audioPauseTime = null;
6400
6435
  // Bind event handlers
6401
6436
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
6402
6437
  this._handleDataAvailable = this._handleDataAvailable.bind(this);
6403
- this._setupDeviceChangeListener();
6404
6438
  }
6405
6439
  _initializeVAD() {
6406
6440
  var _a;
@@ -6665,31 +6699,56 @@ class LayercodeClient {
6665
6699
  * @param {(amplitude: number) => void} updateInternalState - Function to update the internal amplitude state.
6666
6700
  */
6667
6701
  _setupAmplitudeMonitoring(source, callback, updateInternalState) {
6668
- // Set up amplitude monitoring only if a callback is provided
6669
- // Check against the default no-op function defined in the constructor options
6670
- if (callback !== (() => { })) {
6671
- let updateCounter = 0;
6672
- source.startAmplitudeMonitoring((amplitude) => {
6673
- // Only update and call callback at the specified sample rate
6674
- if (updateCounter >= this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
6675
- updateInternalState(amplitude);
6702
+ let updateCounter = 0;
6703
+ source.startAmplitudeMonitoring((amplitude) => {
6704
+ // Only update and call callback at the specified sample rate
6705
+ if (updateCounter >= this.AMPLITUDE_MONITORING_SAMPLE_RATE) {
6706
+ updateInternalState(amplitude);
6707
+ if (callback !== NOOP) {
6676
6708
  callback(amplitude);
6677
- updateCounter = 0; // Reset counter after sampling
6678
6709
  }
6679
- updateCounter++;
6680
- });
6710
+ updateCounter = 0; // Reset counter after sampling
6711
+ }
6712
+ updateCounter++;
6713
+ });
6714
+ const stop = () => { var _a; return (_a = source.stopAmplitudeMonitoring) === null || _a === void 0 ? void 0 : _a.call(source); };
6715
+ if (source === this.wavPlayer) {
6716
+ this.stopPlayerAmplitude = stop;
6681
6717
  }
6718
+ if (source === this.wavRecorder) {
6719
+ this.stopRecorderAmplitude = stop;
6720
+ }
6721
+ }
6722
+ _stopAmplitudeMonitoring() {
6723
+ var _a, _b;
6724
+ (_a = this.stopPlayerAmplitude) === null || _a === void 0 ? void 0 : _a.call(this);
6725
+ (_b = this.stopRecorderAmplitude) === null || _b === void 0 ? void 0 : _b.call(this);
6726
+ this.stopPlayerAmplitude = undefined;
6727
+ this.stopRecorderAmplitude = undefined;
6682
6728
  }
6683
6729
  /**
6684
6730
  * Connects to the Layercode agent and starts the audio conversation
6685
6731
  * @async
6686
6732
  * @returns {Promise<void>}
6687
6733
  */
6688
- async connect() {
6734
+ async connect(opts) {
6735
+ if (this.status === 'connecting') {
6736
+ return;
6737
+ }
6738
+ if (opts === null || opts === void 0 ? void 0 : opts.newConversation) {
6739
+ this.options.conversationId = null;
6740
+ this.conversationId = null;
6741
+ }
6742
+ else if (opts === null || opts === void 0 ? void 0 : opts.conversationId) {
6743
+ this.options.conversationId = opts.conversationId;
6744
+ this.conversationId = opts.conversationId;
6745
+ }
6689
6746
  try {
6690
6747
  this._setStatus('connecting');
6691
6748
  // Reset turn tracking for clean start
6692
6749
  this._resetTurnTracking();
6750
+ this._stopAmplitudeMonitoring();
6751
+ this._setupDeviceChangeListener();
6693
6752
  // Get conversation key from server
6694
6753
  let authorizeSessionRequestBody = {
6695
6754
  agent_id: this.options.agentId,
@@ -6712,6 +6771,7 @@ class LayercodeClient {
6712
6771
  }
6713
6772
  const authorizeSessionResponseBody = await authorizeSessionResponse.json();
6714
6773
  this.conversationId = authorizeSessionResponseBody.conversation_id; // Save the conversation_id for use in future reconnects
6774
+ this.options.conversationId = this.conversationId;
6715
6775
  // Connect WebSocket
6716
6776
  this.ws = new WebSocket(`${this._websocketUrl}?${new URLSearchParams({
6717
6777
  client_session_key: authorizeSessionResponseBody.client_session_key,
@@ -6741,8 +6801,12 @@ class LayercodeClient {
6741
6801
  };
6742
6802
  this.ws.onclose = () => {
6743
6803
  console.log('WebSocket connection closed');
6744
- this._setStatus('disconnected');
6745
- this.options.onDisconnect();
6804
+ this.ws = null;
6805
+ this._performDisconnectCleanup()
6806
+ .catch((error) => {
6807
+ console.error('Error during disconnect cleanup:', error);
6808
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
6809
+ });
6746
6810
  };
6747
6811
  this.ws.onerror = (error) => {
6748
6812
  console.error('WebSocket error:', error);
@@ -6768,30 +6832,19 @@ class LayercodeClient {
6768
6832
  this.currentTurnId = null;
6769
6833
  console.debug('Reset turn tracking state');
6770
6834
  }
6771
- async disconnect() {
6772
- this.deviceId = null;
6773
- this.activeDeviceId = null;
6774
- this.useSystemDefaultDevice = false;
6775
- this.lastReportedDeviceId = null;
6776
- this.recorderStarted = false;
6777
- this.readySent = false;
6778
- // Clean up VAD if it exists
6779
- if (this.vad) {
6780
- this.vad.pause();
6781
- this.vad.destroy();
6782
- this.vad = null;
6835
+ async disconnect(opts) {
6836
+ if (this.status === 'disconnected') {
6837
+ return;
6783
6838
  }
6784
- this.wavRecorder.listenForDeviceChange(null);
6785
- this.wavRecorder.quit();
6786
- this.wavPlayer.disconnect();
6787
- // Reset turn tracking
6788
- this._resetTurnTracking();
6789
- // Close websocket and ensure status is updated
6790
6839
  if (this.ws) {
6840
+ this.ws.onopen = null;
6841
+ this.ws.onclose = null;
6842
+ this.ws.onerror = null;
6843
+ this.ws.onmessage = null;
6791
6844
  this.ws.close();
6792
- this._setStatus('disconnected');
6793
- this.options.onDisconnect();
6845
+ this.ws = null;
6794
6846
  }
6847
+ await this._performDisconnectCleanup(opts === null || opts === void 0 ? void 0 : opts.clearConversationId);
6795
6848
  }
6796
6849
  /**
6797
6850
  * Gets the microphone MediaStream used by this client
@@ -6889,43 +6942,106 @@ class LayercodeClient {
6889
6942
  * Sets up the device change event listener
6890
6943
  */
6891
6944
  _setupDeviceChangeListener() {
6892
- this.wavRecorder.listenForDeviceChange(async (devices) => {
6893
- try {
6894
- const defaultDevice = devices.find((device) => device.default);
6895
- const usingDefaultDevice = this.useSystemDefaultDevice;
6896
- let shouldSwitch = !this.recorderStarted;
6897
- if (!shouldSwitch) {
6898
- if (usingDefaultDevice) {
6899
- if (!defaultDevice) {
6900
- shouldSwitch = true;
6945
+ if (!this.deviceChangeListener) {
6946
+ this.deviceChangeListener = async (devices) => {
6947
+ try {
6948
+ const defaultDevice = devices.find((device) => device.default);
6949
+ const usingDefaultDevice = this.useSystemDefaultDevice;
6950
+ const previousDefaultDeviceKey = this.lastKnownSystemDefaultDeviceKey;
6951
+ const currentDefaultDeviceKey = this._getDeviceComparisonKey(defaultDevice);
6952
+ let shouldSwitch = !this.recorderStarted;
6953
+ if (!shouldSwitch) {
6954
+ if (usingDefaultDevice) {
6955
+ if (!defaultDevice) {
6956
+ shouldSwitch = true;
6957
+ }
6958
+ else if (this.activeDeviceId &&
6959
+ defaultDevice.deviceId !== 'default' &&
6960
+ defaultDevice.deviceId !== this.activeDeviceId) {
6961
+ shouldSwitch = true;
6962
+ }
6963
+ else if ((previousDefaultDeviceKey && previousDefaultDeviceKey !== currentDefaultDeviceKey) ||
6964
+ (!previousDefaultDeviceKey && !currentDefaultDeviceKey && this.recorderStarted)) {
6965
+ shouldSwitch = true;
6966
+ }
6901
6967
  }
6902
- else if (this.activeDeviceId &&
6903
- defaultDevice.deviceId !== 'default' &&
6904
- defaultDevice.deviceId !== this.activeDeviceId) {
6905
- shouldSwitch = true;
6968
+ else {
6969
+ const matchesRequestedDevice = devices.some((device) => device.deviceId === this.deviceId || device.deviceId === this.activeDeviceId);
6970
+ shouldSwitch = !matchesRequestedDevice;
6906
6971
  }
6907
6972
  }
6908
- else {
6909
- const matchesRequestedDevice = devices.some((device) => device.deviceId === this.deviceId || device.deviceId === this.activeDeviceId);
6910
- shouldSwitch = !matchesRequestedDevice;
6973
+ this.lastKnownSystemDefaultDeviceKey = currentDefaultDeviceKey;
6974
+ if (shouldSwitch) {
6975
+ console.debug('Selecting fallback audio input device');
6976
+ const fallbackDevice = defaultDevice || devices[0];
6977
+ if (fallbackDevice) {
6978
+ const fallbackId = fallbackDevice.default ? 'default' : fallbackDevice.deviceId;
6979
+ await this.setInputDevice(fallbackId);
6980
+ }
6981
+ else {
6982
+ console.warn('No alternative audio device found');
6983
+ }
6911
6984
  }
6912
6985
  }
6913
- if (shouldSwitch) {
6914
- console.debug('Selecting fallback audio input device');
6915
- const fallbackDevice = defaultDevice || devices[0];
6916
- if (fallbackDevice) {
6917
- const fallbackId = fallbackDevice.default ? 'default' : fallbackDevice.deviceId;
6918
- await this.setInputDevice(fallbackId);
6919
- }
6920
- else {
6921
- console.warn('No alternative audio device found');
6922
- }
6986
+ catch (error) {
6987
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
6923
6988
  }
6924
- }
6925
- catch (error) {
6926
- this.options.onError(error instanceof Error ? error : new Error(String(error)));
6927
- }
6928
- });
6989
+ };
6990
+ }
6991
+ this.wavRecorder.listenForDeviceChange(this.deviceChangeListener);
6992
+ }
6993
+ _teardownDeviceListeners() {
6994
+ this.wavRecorder.listenForDeviceChange(null);
6995
+ }
6996
+ async _performDisconnectCleanup(clearConversationId) {
6997
+ var _a, _b;
6998
+ this.deviceId = null;
6999
+ this.activeDeviceId = null;
7000
+ this.useSystemDefaultDevice = false;
7001
+ this.lastReportedDeviceId = null;
7002
+ this.lastKnownSystemDefaultDeviceKey = null;
7003
+ this.recorderStarted = false;
7004
+ this.readySent = false;
7005
+ this._stopAmplitudeMonitoring();
7006
+ this._teardownDeviceListeners();
7007
+ if (this.vad) {
7008
+ this.vad.pause();
7009
+ this.vad.destroy();
7010
+ this.vad = null;
7011
+ }
7012
+ await this.wavRecorder.quit();
7013
+ (_b = (_a = this.wavPlayer).stop) === null || _b === void 0 ? void 0 : _b.call(_a);
7014
+ this.wavPlayer.disconnect();
7015
+ this._resetTurnTracking();
7016
+ if (clearConversationId) {
7017
+ this.options.conversationId = null;
7018
+ this.conversationId = null;
7019
+ }
7020
+ else {
7021
+ this.options.conversationId = this.conversationId;
7022
+ }
7023
+ this.userAudioAmplitude = 0;
7024
+ this.agentAudioAmplitude = 0;
7025
+ this._setStatus('disconnected');
7026
+ this.options.onDisconnect();
7027
+ }
7028
+ _getDeviceComparisonKey(device) {
7029
+ if (!device || typeof device !== 'object') {
7030
+ return null;
7031
+ }
7032
+ const deviceId = typeof device.deviceId === 'string' ? device.deviceId : '';
7033
+ if (deviceId && deviceId !== 'default') {
7034
+ return deviceId;
7035
+ }
7036
+ const groupId = typeof device.groupId === 'string' ? device.groupId : '';
7037
+ if (groupId) {
7038
+ return groupId;
7039
+ }
7040
+ const label = typeof device.label === 'string' ? device.label : '';
7041
+ if (label) {
7042
+ return label;
7043
+ }
7044
+ return null;
6929
7045
  }
6930
7046
  /**
6931
7047
  * Mutes the microphone to stop sending audio to the server