Skip to content

Commit cdb835d

Browse files
committed
chore: bump version to 2.1.5 and enhance device setup handling in LayercodeClient
- Updated version in package.json to 2.1.5. - Added a device setup listener to ensure the recorder is initialized before proceeding, preventing race conditions. - Implemented a timeout mechanism to avoid hanging during device setup.
1 parent fb09b0b commit cdb835d

File tree

3 files changed

+28
-53
lines changed

3 files changed

+28
-53
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"author": "Layercode",
33
"license": "MIT",
44
"name": "@layercode/js-sdk",
5-
"version": "2.1.6",
5+
"version": "2.1.7",
66
"description": "Layercode JavaScript SDK for browser usage",
77
"type": "module",
88
"main": "dist/layercode-js-sdk.esm.js",
@@ -48,4 +48,4 @@
4848
"@ricky0123/vad-web": "^0.0.24",
4949
"onnxruntime-web": "^1.21.1"
5050
}
51-
}
51+
}

src/index.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ interface ILayercodeClient {
9595
triggerUserTurnFinished(): Promise<void>;
9696
getStream(): MediaStream | null;
9797
setInputDevice(deviceId: string): Promise<void>;
98+
listDevices(): Promise<Array<MediaDeviceInfo & { default: boolean }>>;
9899
mute(): void;
99100
unmute(): void;
100101
readonly status: string;
@@ -126,6 +127,8 @@ interface LayercodeClientOptions {
126127
onError?: (error: Error) => void;
127128
/** Callback when a device is switched */
128129
onDeviceSwitched?: (deviceId: string) => void;
130+
/** Callback when available devices change (devices added/removed) */
131+
onDevicesChanged?: (devices: Array<MediaDeviceInfo & { default: boolean }>) => void;
129132
/** Callback for data messages */
130133
onDataMessage?: (message: any) => void;
131134
/** Callback for other messages (excluding audio msgs) */
@@ -192,6 +195,7 @@ class LayercodeClient implements ILayercodeClient {
192195
onDisconnect: options.onDisconnect ?? NOOP,
193196
onError: options.onError ?? NOOP,
194197
onDeviceSwitched: options.onDeviceSwitched ?? NOOP,
198+
onDevicesChanged: options.onDevicesChanged ?? NOOP,
195199
onDataMessage: options.onDataMessage ?? NOOP,
196200
onMessage: options.onMessage ?? NOOP,
197201
onUserAmplitudeChange: options.onUserAmplitudeChange ?? NOOP,
@@ -562,7 +566,6 @@ class LayercodeClient implements ILayercodeClient {
562566
// Reset turn tracking for clean start
563567
this._resetTurnTracking();
564568
this._stopAmplitudeMonitoring();
565-
this._setupDeviceChangeListener();
566569

567570
// Get conversation key from server
568571
let authorizeSessionRequestBody = {
@@ -588,6 +591,9 @@ class LayercodeClient implements ILayercodeClient {
588591
this.conversationId = authorizeSessionResponseBody.conversation_id; // Save the conversation_id for use in future reconnects
589592
this.options.conversationId = this.conversationId;
590593

594+
await this.wavRecorder.requestPermission();
595+
this._setupDeviceChangeListener();
596+
591597
// Connect WebSocket
592598
this.ws = new WebSocket(
593599
`${this._websocketUrl}?${new URLSearchParams({
@@ -645,7 +651,6 @@ class LayercodeClient implements ILayercodeClient {
645651
console.error('Error connecting to Layercode agent:', error);
646652
this._setStatus('error');
647653
this.options.onError(error instanceof Error ? error : new Error(String(error)));
648-
throw error;
649654
}
650655
}
651656

@@ -679,6 +684,14 @@ class LayercodeClient implements ILayercodeClient {
679684
return this.wavRecorder.getStream();
680685
}
681686

687+
/**
688+
* List all available audio input devices
689+
* @returns {Promise<Array<MediaDeviceInfo & {default: boolean}>>}
690+
*/
691+
async listDevices(): Promise<Array<MediaDeviceInfo & { default: boolean }>> {
692+
return this.wavRecorder.listDevices();
693+
}
694+
682695
/**
683696
* Switches the input device for the microphone and restarts recording
684697
* @param {string} deviceId - The deviceId of the new microphone
@@ -778,6 +791,9 @@ class LayercodeClient implements ILayercodeClient {
778791
if (!this.deviceChangeListener) {
779792
this.deviceChangeListener = async (devices: any[]) => {
780793
try {
794+
// Notify user that devices have changed
795+
this.options.onDevicesChanged(devices);
796+
781797
const defaultDevice = devices.find((device: any) => device.default);
782798
const usingDefaultDevice = this.useSystemDefaultDevice;
783799
const previousDefaultDeviceKey = this.lastKnownSystemDefaultDeviceKey;

src/wavtools/lib/wav_recorder.js

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -251,54 +251,14 @@ export class WavRecorder {
251251
* @returns {Promise<true>}
252252
*/
253253
async requestPermission() {
254-
const ensureUserMediaAccess = async () => {
255-
const stream = await navigator.mediaDevices.getUserMedia({
256-
audio: true,
257-
});
258-
const tracks = stream.getTracks();
259-
tracks.forEach((track) => track.stop());
260-
};
261-
262-
const permissionsUnsupported =
263-
!navigator.permissions ||
264-
typeof navigator.permissions.query !== 'function';
265-
266-
if (permissionsUnsupported) {
267-
try {
268-
await ensureUserMediaAccess();
269-
} catch (error) {
270-
window.alert('You must grant microphone access to use this feature.');
271-
throw error;
272-
}
273-
return true;
274-
}
275-
276254
try {
277-
const permissionStatus = await navigator.permissions.query({
278-
name: 'microphone',
255+
console.log('ensureUserMediaAccess');
256+
await navigator.mediaDevices.getUserMedia({
257+
audio: true,
279258
});
280-
281-
if (permissionStatus.state === 'denied') {
282-
window.alert('You must grant microphone access to use this feature.');
283-
return true;
284-
}
285-
286-
if (permissionStatus.state === 'prompt') {
287-
try {
288-
await ensureUserMediaAccess();
289-
} catch (error) {
290-
window.alert('You must grant microphone access to use this feature.');
291-
throw error;
292-
}
293-
}
294-
} catch (error) {
295-
// Firefox rejects permissions.query with NotSupportedError – fall back to getUserMedia directly
296-
try {
297-
await ensureUserMediaAccess();
298-
} catch (fallbackError) {
299-
window.alert('You must grant microphone access to use this feature.');
300-
throw fallbackError;
301-
}
259+
} catch (fallbackError) {
260+
window.alert('You must grant microphone access to use this feature.');
261+
throw fallbackError;
302262
}
303263
return true;
304264
}
@@ -315,10 +275,9 @@ export class WavRecorder {
315275
throw new Error('Could not request user devices');
316276
}
317277
await this.requestPermission();
278+
318279
const devices = await navigator.mediaDevices.enumerateDevices();
319-
const audioDevices = devices.filter(
320-
(device) => device.kind === 'audioinput',
321-
);
280+
const audioDevices = devices.filter((device) => device.kind === 'audioinput');
322281
const defaultDeviceIndex = audioDevices.findIndex(
323282
(device) => device.deviceId === 'default',
324283
);

0 commit comments

Comments
 (0)