Skip to content

Commit c941d62

Browse files
authored
Default to exact matching and retry on unspecified ideal narrowing (#1479)
1 parent 6bd75d7 commit c941d62

6 files changed

Lines changed: 113 additions & 56 deletions

File tree

.changeset/shy-dolls-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Default to exact matching and retry on unspecified ideal narrowing

src/api/utils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ describe('createRtcUrl', () => {
4949
const parsedResult = new URL(result);
5050
expect(parsedResult.pathname).toBe('/sub/path/rtc');
5151
});
52+
53+
it('should handle sub paths with url params', () => {
54+
const url = 'wss://example.com/sub/path?param=value';
55+
const searchParams = new URLSearchParams();
56+
searchParams.set('token', 'test-token');
57+
const result = createRtcUrl(url, searchParams);
58+
59+
const parsedResult = new URL(result);
60+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
61+
expect(parsedResult.searchParams.get('param')).toBe('value');
62+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
63+
});
5264
});
5365

5466
describe('createValidateUrl', () => {

src/room/DeviceManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default class DeviceManager {
5757
if (isDummyDeviceOrEmpty) {
5858
const permissionsToAcquire = {
5959
video: kind !== 'audioinput' && kind !== 'audiooutput',
60-
audio: kind !== 'videoinput' && { deviceId: 'default' },
60+
audio: kind !== 'videoinput' && { deviceId: { ideal: 'default' } },
6161
};
6262
const stream = await navigator.mediaDevices.getUserMedia(permissionsToAcquire);
6363
devices = await navigator.mediaDevices.enumerateDevices();

src/room/defaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export const publishDefaults: TrackPublishDefaults = {
2222
} as const;
2323

2424
export const audioDefaults: AudioCaptureOptions = {
25-
deviceId: 'default',
25+
deviceId: { ideal: 'default' },
2626
autoGainControl: true,
2727
echoCancellation: true,
2828
noiseSuppression: true,
2929
voiceIsolation: true,
3030
};
3131

3232
export const videoDefaults: VideoCaptureOptions = {
33-
deviceId: 'default',
33+
deviceId: { ideal: 'default' },
3434
resolution: VideoPresets.h720.resolution,
3535
};
3636

src/room/track/create.ts

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -31,70 +31,110 @@ export async function createLocalTracks(
3131
options?: CreateLocalTracksOptions,
3232
): Promise<Array<LocalTrack>> {
3333
// set default options to true
34-
options ??= {};
35-
options.audio ??= { deviceId: 'default' };
36-
options.video ??= { deviceId: 'default' };
34+
const internalOptions = { ...(options ?? {}) };
35+
let attemptExactMatch = false;
36+
let retryAudioOptions: AudioCaptureOptions | undefined | boolean = options?.audio;
37+
let retryVideoOptions: VideoCaptureOptions | undefined | boolean = options?.video;
38+
// if the user passes a device id as a string, we default to exact match
39+
if (
40+
internalOptions.audio &&
41+
typeof internalOptions.audio === 'object' &&
42+
typeof internalOptions.audio.deviceId === 'string'
43+
) {
44+
const deviceId: string = internalOptions.audio.deviceId;
45+
internalOptions.audio.deviceId = { exact: deviceId };
46+
attemptExactMatch = true;
47+
retryAudioOptions = {
48+
...internalOptions.audio,
49+
deviceId: { ideal: deviceId },
50+
};
51+
}
52+
if (
53+
internalOptions.video &&
54+
typeof internalOptions.video === 'object' &&
55+
typeof internalOptions.video.deviceId === 'string'
56+
) {
57+
const deviceId: string = internalOptions.video.deviceId;
58+
internalOptions.video.deviceId = { exact: deviceId };
59+
attemptExactMatch = true;
60+
retryVideoOptions = {
61+
...internalOptions.video,
62+
deviceId: { ideal: deviceId },
63+
};
64+
}
65+
internalOptions.audio ??= { deviceId: 'default' };
66+
internalOptions.video ??= { deviceId: 'default' };
3767

38-
const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
39-
const opts = mergeDefaultOptions(options, audioDefaults, videoDefaults);
68+
const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(internalOptions);
69+
const opts = mergeDefaultOptions(internalOptions, audioDefaults, videoDefaults);
4070
const constraints = constraintsForOptions(opts);
4171

4272
// Keep a reference to the promise on DeviceManager and await it in getLocalDevices()
4373
// works around iOS Safari Bug https://bugs.webkit.org/show_bug.cgi?id=179363
4474
const mediaPromise = navigator.mediaDevices.getUserMedia(constraints);
4575

46-
if (options.audio) {
76+
if (internalOptions.audio) {
4777
DeviceManager.userMediaPromiseMap.set('audioinput', mediaPromise);
4878
mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('audioinput'));
4979
}
50-
if (options.video) {
80+
if (internalOptions.video) {
5181
DeviceManager.userMediaPromiseMap.set('videoinput', mediaPromise);
5282
mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('videoinput'));
5383
}
84+
try {
85+
const stream = await mediaPromise;
86+
return await Promise.all(
87+
stream.getTracks().map(async (mediaStreamTrack) => {
88+
const isAudio = mediaStreamTrack.kind === 'audio';
89+
let trackOptions = isAudio ? opts!.audio : opts!.video;
90+
if (typeof trackOptions === 'boolean' || !trackOptions) {
91+
trackOptions = {};
92+
}
93+
let trackConstraints: MediaTrackConstraints | undefined;
94+
const conOrBool = isAudio ? constraints.audio : constraints.video;
95+
if (typeof conOrBool !== 'boolean') {
96+
trackConstraints = conOrBool;
97+
}
5498

55-
const stream = await mediaPromise;
56-
return Promise.all(
57-
stream.getTracks().map(async (mediaStreamTrack) => {
58-
const isAudio = mediaStreamTrack.kind === 'audio';
59-
let trackOptions = isAudio ? opts!.audio : opts!.video;
60-
if (typeof trackOptions === 'boolean' || !trackOptions) {
61-
trackOptions = {};
62-
}
63-
let trackConstraints: MediaTrackConstraints | undefined;
64-
const conOrBool = isAudio ? constraints.audio : constraints.video;
65-
if (typeof conOrBool !== 'boolean') {
66-
trackConstraints = conOrBool;
67-
}
99+
// update the constraints with the device id the user gave permissions to in the permission prompt
100+
// otherwise each track restart (e.g. mute - unmute) will try to initialize the device again -> causing additional permission prompts
101+
const newDeviceId = mediaStreamTrack.getSettings().deviceId;
102+
if (
103+
trackConstraints?.deviceId &&
104+
unwrapConstraint(trackConstraints.deviceId) !== newDeviceId
105+
) {
106+
trackConstraints.deviceId = newDeviceId;
107+
} else if (!trackConstraints) {
108+
trackConstraints = { deviceId: newDeviceId };
109+
}
68110

69-
// update the constraints with the device id the user gave permissions to in the permission prompt
70-
// otherwise each track restart (e.g. mute - unmute) will try to initialize the device again -> causing additional permission prompts
71-
const newDeviceId = mediaStreamTrack.getSettings().deviceId;
72-
if (
73-
trackConstraints?.deviceId &&
74-
unwrapConstraint(trackConstraints.deviceId) !== newDeviceId
75-
) {
76-
trackConstraints.deviceId = newDeviceId;
77-
} else if (!trackConstraints) {
78-
trackConstraints = { deviceId: newDeviceId };
79-
}
111+
const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints);
112+
if (track.kind === Track.Kind.Video) {
113+
track.source = Track.Source.Camera;
114+
} else if (track.kind === Track.Kind.Audio) {
115+
track.source = Track.Source.Microphone;
116+
}
117+
track.mediaStream = stream;
80118

81-
const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints);
82-
if (track.kind === Track.Kind.Video) {
83-
track.source = Track.Source.Camera;
84-
} else if (track.kind === Track.Kind.Audio) {
85-
track.source = Track.Source.Microphone;
86-
}
87-
track.mediaStream = stream;
119+
if (isAudioTrack(track) && audioProcessor) {
120+
await track.setProcessor(audioProcessor);
121+
} else if (isVideoTrack(track) && videoProcessor) {
122+
await track.setProcessor(videoProcessor);
123+
}
88124

89-
if (isAudioTrack(track) && audioProcessor) {
90-
await track.setProcessor(audioProcessor);
91-
} else if (isVideoTrack(track) && videoProcessor) {
92-
await track.setProcessor(videoProcessor);
93-
}
94-
95-
return track;
96-
}),
97-
);
125+
return track;
126+
}),
127+
);
128+
} catch (e) {
129+
if (!attemptExactMatch) {
130+
throw e;
131+
}
132+
return createLocalTracks({
133+
...options,
134+
audio: retryAudioOptions,
135+
video: retryVideoOptions,
136+
});
137+
}
98138
}
99139

100140
/**

src/room/track/utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function mergeDefaultOptions(
3333
clonedOptions.audio as Record<string, unknown>,
3434
audioDefaults as Record<string, unknown>,
3535
);
36-
clonedOptions.audio.deviceId ??= 'default';
36+
clonedOptions.audio.deviceId ??= { ideal: 'default' };
3737
if (audioProcessor || defaultAudioProcessor) {
3838
clonedOptions.audio.processor = audioProcessor ?? defaultAudioProcessor;
3939
}
@@ -43,7 +43,7 @@ export function mergeDefaultOptions(
4343
clonedOptions.video as Record<string, unknown>,
4444
videoDefaults as Record<string, unknown>,
4545
);
46-
clonedOptions.video.deviceId ??= 'default';
46+
clonedOptions.video.deviceId ??= { ideal: 'default' };
4747
if (videoProcessor || defaultVideoProcessor) {
4848
clonedOptions.video.processor = videoProcessor ?? defaultVideoProcessor;
4949
}
@@ -81,9 +81,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
8181
}
8282
});
8383
constraints.video = videoOptions;
84-
constraints.video.deviceId ??= 'default';
84+
constraints.video.deviceId ??= { ideal: 'default' };
8585
} else {
86-
constraints.video = options.video ? { deviceId: 'default' } : false;
86+
constraints.video = options.video ? { deviceId: { ideal: 'default' } } : false;
8787
}
8888
} else {
8989
constraints.video = false;
@@ -92,9 +92,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
9292
if (options.audio) {
9393
if (typeof options.audio === 'object') {
9494
constraints.audio = options.audio;
95-
constraints.audio.deviceId ??= 'default';
95+
constraints.audio.deviceId ??= { ideal: 'default' };
9696
} else {
97-
constraints.audio = { deviceId: 'default' };
97+
constraints.audio = { deviceId: { ideal: 'default' } };
9898
}
9999
} else {
100100
constraints.audio = false;

0 commit comments

Comments
 (0)