livekit-client 2.7.5 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  2. package/dist/livekit-client.e2ee.worker.mjs +14 -14
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +158 -92
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  9. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  11. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  12. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  13. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  14. package/dist/src/room/DeviceManager.d.ts +2 -0
  15. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +1 -0
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/defaults.d.ts.map +1 -1
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  22. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  23. package/dist/src/room/track/LocalAudioTrack.d.ts +0 -1
  24. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/LocalTrack.d.ts +2 -0
  26. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  27. package/dist/src/room/track/LocalVideoTrack.d.ts +0 -1
  28. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  29. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/Track.d.ts.map +1 -1
  32. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  33. package/dist/src/room/track/utils.d.ts.map +1 -1
  34. package/dist/ts4.2/src/room/DeviceManager.d.ts +2 -0
  35. package/dist/ts4.2/src/room/Room.d.ts +1 -0
  36. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +0 -1
  37. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +2 -0
  38. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +0 -1
  39. package/package.json +16 -15
  40. package/src/e2ee/worker/tsconfig.json +1 -1
  41. package/src/room/DeviceManager.ts +9 -2
  42. package/src/room/Room.ts +75 -10
  43. package/src/room/defaults.ts +2 -0
  44. package/src/room/participant/LocalParticipant.ts +15 -4
  45. package/src/room/participant/publishUtils.ts +14 -4
  46. package/src/room/track/LocalAudioTrack.ts +0 -16
  47. package/src/room/track/LocalTrack.ts +23 -1
  48. package/src/room/track/LocalVideoTrack.ts +1 -19
  49. package/src/room/track/RemoteAudioTrack.ts +1 -0
  50. package/src/room/track/RemoteVideoTrack.ts +1 -0
  51. package/src/room/track/create.ts +2 -2
  52. package/src/room/track/utils.test.ts +4 -12
  53. package/src/room/track/utils.ts +6 -2
package/src/room/Room.ts CHANGED
@@ -86,6 +86,7 @@ import {
86
86
  isBrowserSupported,
87
87
  isCloud,
88
88
  isReactNative,
89
+ isSafari,
89
90
  isWeb,
90
91
  supportsSetSinkId,
91
92
  toHttpUrl,
@@ -234,6 +235,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
234
235
  if (this.options.e2ee) {
235
236
  this.setupE2EE();
236
237
  }
238
+
239
+ if (isWeb()) {
240
+ const abortController = new AbortController();
241
+
242
+ // in order to catch device changes prior to room connection we need to register the event in the constructor
243
+ navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange, {
244
+ signal: abortController.signal,
245
+ });
246
+
247
+ if (Room.cleanupRegistry) {
248
+ Room.cleanupRegistry.register(this, () => {
249
+ abortController.abort();
250
+ });
251
+ }
252
+ }
237
253
  }
238
254
 
239
255
  /**
@@ -434,6 +450,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
434
450
  return DeviceManager.getInstance().getDevices(kind, requestPermissions);
435
451
  }
436
452
 
453
+ static cleanupRegistry =
454
+ typeof FinalizationRegistry !== 'undefined' &&
455
+ new FinalizationRegistry((cleanup: () => void) => {
456
+ cleanup();
457
+ });
458
+
437
459
  /**
438
460
  * prepareConnection should be called as soon as the page is loaded, in order
439
461
  * to speed up the connection attempt. This function will
@@ -769,7 +791,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
769
791
  }
770
792
  if (isWeb()) {
771
793
  document.addEventListener('freeze', this.onPageLeave);
772
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
773
794
  }
774
795
  this.setAndEmitConnectionState(ConnectionState.Connected);
775
796
  this.emit(RoomEvent.Connected);
@@ -1097,14 +1118,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1097
1118
  * @param deviceId
1098
1119
  */
1099
1120
  async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean = false) {
1100
- let deviceHasChanged = false;
1101
1121
  let success = true;
1122
+ let needsUpdateWithoutTracks = false;
1102
1123
  const deviceConstraint = exact ? { exact: deviceId } : deviceId;
1103
1124
  if (kind === 'audioinput') {
1125
+ needsUpdateWithoutTracks = this.localParticipant.audioTrackPublications.size === 0;
1104
1126
  const prevDeviceId =
1105
1127
  this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
1106
1128
  this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
1107
- deviceHasChanged = prevDeviceId !== deviceConstraint;
1108
1129
  const tracks = Array.from(this.localParticipant.audioTrackPublications.values()).filter(
1109
1130
  (track) => track.source === Track.Source.Microphone,
1110
1131
  );
@@ -1117,10 +1138,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1117
1138
  throw e;
1118
1139
  }
1119
1140
  } else if (kind === 'videoinput') {
1141
+ needsUpdateWithoutTracks = this.localParticipant.videoTrackPublications.size === 0;
1120
1142
  const prevDeviceId =
1121
1143
  this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
1122
1144
  this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
1123
- deviceHasChanged = prevDeviceId !== deviceConstraint;
1124
1145
  const tracks = Array.from(this.localParticipant.videoTrackPublications.values()).filter(
1125
1146
  (track) => track.source === Track.Source.Camera,
1126
1147
  );
@@ -1147,7 +1168,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1147
1168
  this.options.audioOutput ??= {};
1148
1169
  const prevDeviceId = this.getActiveDevice(kind) ?? this.options.audioOutput.deviceId;
1149
1170
  this.options.audioOutput.deviceId = deviceId;
1150
- deviceHasChanged = prevDeviceId !== deviceConstraint;
1151
1171
 
1152
1172
  try {
1153
1173
  if (this.options.webAudioMix) {
@@ -1164,8 +1184,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1164
1184
  throw e;
1165
1185
  }
1166
1186
  }
1167
- if (deviceHasChanged && success) {
1168
- this.localParticipant.activeDeviceMap.set(kind, deviceId);
1187
+ if (needsUpdateWithoutTracks || kind === 'audiooutput') {
1188
+ // if there are not active tracks yet or we're switching audiooutput, we need to manually update the active device map here as changing audio output won't result in a track restart
1189
+ this.localParticipant.activeDeviceMap.set(
1190
+ kind,
1191
+ (kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
1192
+ );
1169
1193
  this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
1170
1194
  }
1171
1195
 
@@ -1654,13 +1678,54 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1654
1678
  };
1655
1679
 
1656
1680
  private handleDeviceChange = async () => {
1681
+ const previousDevices = DeviceManager.getInstance().previousDevices;
1657
1682
  // check for available devices, but don't request permissions in order to avoid prompts for kinds that haven't been used before
1658
1683
  const availableDevices = await DeviceManager.getInstance().getDevices(undefined, false);
1684
+ const browser = getBrowser();
1685
+ if (browser?.name === 'Chrome' && browser.os !== 'iOS') {
1686
+ for (let availableDevice of availableDevices) {
1687
+ const previousDevice = previousDevices.find(
1688
+ (info) => info.deviceId === availableDevice.deviceId,
1689
+ );
1690
+ if (
1691
+ previousDevice &&
1692
+ previousDevice.label !== '' &&
1693
+ previousDevice.kind === availableDevice.kind &&
1694
+ previousDevice.label !== availableDevice.label
1695
+ ) {
1696
+ // label has changed on device the same deviceId, indicating that the default device has changed on the OS level
1697
+ if (this.getActiveDevice(availableDevice.kind) === 'default') {
1698
+ // emit an active device change event only if the selected output device is actually on `default`
1699
+ this.emit(
1700
+ RoomEvent.ActiveDeviceChanged,
1701
+ availableDevice.kind,
1702
+ availableDevice.deviceId,
1703
+ );
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+
1659
1709
  // inputs are automatically handled via TrackEvent.Ended causing a TrackEvent.Restarted. Here we only need to worry about audiooutputs changing
1660
- const kinds: MediaDeviceKind[] = ['audiooutput'];
1710
+ const kinds: MediaDeviceKind[] = ['audiooutput', 'audioinput', 'videoinput'];
1661
1711
  for (let kind of kinds) {
1662
- // switch to first available device if previously active device is not available any more
1663
1712
  const devicesOfKind = availableDevices.filter((d) => d.kind === kind);
1713
+ const activeDevice = this.getActiveDevice(kind);
1714
+
1715
+ if (activeDevice === previousDevices.filter((info) => info.kind === kind)[0]?.deviceId) {
1716
+ // in Safari the first device is always the default, so we assume a user on the default device would like to switch to the default once it changes
1717
+ // FF doesn't emit an event when the default device changes, so we perform the same best effort and switch to the new device once connected and if it's the first in the array
1718
+ if (devicesOfKind.length > 0 && devicesOfKind[0]?.deviceId !== activeDevice) {
1719
+ await this.switchActiveDevice(kind, devicesOfKind[0].deviceId);
1720
+ continue;
1721
+ }
1722
+ }
1723
+
1724
+ if ((kind === 'audioinput' && !isSafari()) || kind === 'videoinput') {
1725
+ // airpods on Safari need special handling for audioinput as the track doesn't end as soon as you take them out
1726
+ continue;
1727
+ }
1728
+ // switch to first available device if previously active device is not available any more
1664
1729
  if (
1665
1730
  devicesOfKind.length > 0 &&
1666
1731
  !devicesOfKind.find((deviceInfo) => deviceInfo.deviceId === this.getActiveDevice(kind))
@@ -2013,7 +2078,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2013
2078
  this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
2014
2079
  }
2015
2080
  }
2016
- const deviceId = await pub.track?.getDeviceId();
2081
+ const deviceId = await pub.track?.getDeviceId(false);
2017
2082
  const deviceKind = sourceToKind(pub.source);
2018
2083
  if (
2019
2084
  deviceKind &&
@@ -22,6 +22,7 @@ export const publishDefaults: TrackPublishDefaults = {
22
22
  } as const;
23
23
 
24
24
  export const audioDefaults: AudioCaptureOptions = {
25
+ deviceId: 'default',
25
26
  autoGainControl: true,
26
27
  echoCancellation: true,
27
28
  noiseSuppression: true,
@@ -29,6 +30,7 @@ export const audioDefaults: AudioCaptureOptions = {
29
30
  };
30
31
 
31
32
  export const videoDefaults: VideoCaptureOptions = {
33
+ deviceId: 'default',
32
34
  resolution: VideoPresets.h720.resolution,
33
35
  };
34
36
 
@@ -154,7 +154,11 @@ export default class LocalParticipant extends Participant {
154
154
  this.engine = engine;
155
155
  this.roomOptions = options;
156
156
  this.setupEngine(engine);
157
- this.activeDeviceMap = new Map();
157
+ this.activeDeviceMap = new Map([
158
+ ['audioinput', 'default'],
159
+ ['videoinput', 'default'],
160
+ ['audiooutput', 'default'],
161
+ ]);
158
162
  this.pendingSignalRequests = new Map();
159
163
  }
160
164
 
@@ -486,6 +490,16 @@ export default class LocalParticipant extends Participant {
486
490
  default:
487
491
  throw new TrackInvalidError(source);
488
492
  }
493
+ } catch (e: unknown) {
494
+ localTracks?.forEach((tr) => {
495
+ tr.stop();
496
+ });
497
+ if (e instanceof Error) {
498
+ this.emit(ParticipantEvent.MediaDevicesError, e);
499
+ }
500
+ throw e;
501
+ }
502
+ try {
489
503
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
490
504
  for (const localTrack of localTracks) {
491
505
  this.log.info('publishing track', {
@@ -502,9 +516,6 @@ export default class LocalParticipant extends Participant {
502
516
  localTracks?.forEach((tr) => {
503
517
  tr.stop();
504
518
  });
505
- if (e instanceof Error && !(e instanceof TrackInvalidError)) {
506
- this.emit(ParticipantEvent.MediaDevicesError, e);
507
- }
508
519
  throw e;
509
520
  } finally {
510
521
  this.pendingPublishing.delete(source);
@@ -125,6 +125,8 @@ export function computeVideoEncodings(
125
125
  log.debug('using video encoding', videoEncoding);
126
126
  }
127
127
 
128
+ const sourceFramerate = videoEncoding.maxFramerate;
129
+
128
130
  const original = new VideoPreset(
129
131
  width,
130
132
  height,
@@ -216,10 +218,10 @@ export function computeVideoEncodings(
216
218
  // based on other conditions.
217
219
  const size = Math.max(width, height);
218
220
  if (size >= 960 && midPreset) {
219
- return encodingsFromPresets(width, height, [lowPreset, midPreset, original]);
221
+ return encodingsFromPresets(width, height, [lowPreset, midPreset, original], sourceFramerate);
220
222
  }
221
223
  if (size >= 480) {
222
- return encodingsFromPresets(width, height, [lowPreset, original]);
224
+ return encodingsFromPresets(width, height, [lowPreset, original], sourceFramerate);
223
225
  }
224
226
  }
225
227
  return encodingsFromPresets(width, height, [original]);
@@ -344,6 +346,7 @@ function encodingsFromPresets(
344
346
  width: number,
345
347
  height: number,
346
348
  presets: VideoPreset[],
349
+ sourceFramerate?: number | undefined,
347
350
  ): RTCRtpEncodingParameters[] {
348
351
  const encodings: RTCRtpEncodingParameters[] = [];
349
352
  presets.forEach((preset, idx) => {
@@ -352,13 +355,20 @@ function encodingsFromPresets(
352
355
  }
353
356
  const size = Math.min(width, height);
354
357
  const rid = videoRids[idx];
358
+
355
359
  const encoding: RTCRtpEncodingParameters = {
356
360
  rid,
357
361
  scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
358
362
  maxBitrate: preset.encoding.maxBitrate,
359
363
  };
360
- if (preset.encoding.maxFramerate) {
361
- encoding.maxFramerate = preset.encoding.maxFramerate;
364
+ // ensure that the sourceFramerate is the highest framerate applied across all layers so that the
365
+ // original encoding doesn't get bumped unintentionally by any of the other layers
366
+ const maxFramerate =
367
+ sourceFramerate && preset.encoding.maxFramerate
368
+ ? Math.min(sourceFramerate, preset.encoding.maxFramerate)
369
+ : preset.encoding.maxFramerate;
370
+ if (maxFramerate) {
371
+ encoding.maxFramerate = maxFramerate;
362
372
  }
363
373
  const canSetPriority = isFireFox() || idx === 0;
364
374
  if (preset.encoding.priority && canSetPriority) {
@@ -45,22 +45,6 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
45
45
  this.checkForSilence();
46
46
  }
47
47
 
48
- async setDeviceId(deviceId: ConstrainDOMString): Promise<boolean> {
49
- if (
50
- this._constraints.deviceId === deviceId &&
51
- this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)
52
- ) {
53
- return true;
54
- }
55
- this._constraints.deviceId = deviceId;
56
- if (!this.isMuted) {
57
- await this.restartTrack();
58
- }
59
- return (
60
- this.isMuted || unwrapConstraint(deviceId) === this._mediaStreamTrack.getSettings().deviceId
61
- );
62
- }
63
-
64
48
  async mute(): Promise<typeof this> {
65
49
  const unlock = await this.muteLock.lock();
66
50
  try {
@@ -5,7 +5,7 @@ import DeviceManager from '../DeviceManager';
5
5
  import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
6
6
  import { TrackEvent } from '../events';
7
7
  import type { LoggerOptions } from '../types';
8
- import { compareVersions, isMobile, sleep } from '../utils';
8
+ import { compareVersions, isMobile, sleep, unwrapConstraint } from '../utils';
9
9
  import { Track, attachToElement, detachTrack } from './Track';
10
10
  import type { VideoCodec } from './options';
11
11
  import type { TrackProcessor } from './processor/types';
@@ -221,6 +221,28 @@ export default abstract class LocalTrack<
221
221
  throw new TrackInvalidError('unable to get track dimensions after timeout');
222
222
  }
223
223
 
224
+ async setDeviceId(deviceId: ConstrainDOMString): Promise<boolean> {
225
+ if (
226
+ this._constraints.deviceId === deviceId &&
227
+ this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)
228
+ ) {
229
+ return true;
230
+ }
231
+ this._constraints.deviceId = deviceId;
232
+
233
+ // when track is muted, underlying media stream track is stopped and
234
+ // will be restarted later
235
+ if (this.isMuted) {
236
+ return true;
237
+ }
238
+
239
+ await this.restartTrack();
240
+
241
+ return unwrapConstraint(deviceId) === this._mediaStreamTrack.getSettings().deviceId;
242
+ }
243
+
244
+ abstract restartTrack(constraints?: unknown): Promise<void>;
245
+
224
246
  /**
225
247
  * @returns DeviceID of the device that is currently being used for this track
226
248
  */
@@ -11,7 +11,7 @@ import { ScalabilityMode } from '../participant/publishUtils';
11
11
  import type { VideoSenderStats } from '../stats';
12
12
  import { computeBitrate, monitorFrequency } from '../stats';
13
13
  import type { LoggerOptions } from '../types';
14
- import { isFireFox, isMobile, isWeb, unwrapConstraint } from '../utils';
14
+ import { isFireFox, isMobile, isWeb } from '../utils';
15
15
  import LocalTrack from './LocalTrack';
16
16
  import { Track, VideoQuality } from './Track';
17
17
  import type { VideoCaptureOptions, VideoCodec } from './options';
@@ -241,24 +241,6 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
241
241
  this.setPublishingLayers(qualities);
242
242
  }
243
243
 
244
- async setDeviceId(deviceId: ConstrainDOMString): Promise<boolean> {
245
- if (
246
- this._constraints.deviceId === deviceId &&
247
- this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)
248
- ) {
249
- return true;
250
- }
251
- this._constraints.deviceId = deviceId;
252
- // when video is muted, underlying media stream track is stopped and
253
- // will be restarted later
254
- if (!this.isMuted) {
255
- await this.restartTrack();
256
- }
257
- return (
258
- this.isMuted || unwrapConstraint(deviceId) === this._mediaStreamTrack.getSettings().deviceId
259
- );
260
- }
261
-
262
244
  async restartTrack(options?: VideoCaptureOptions) {
263
245
  let constraints: MediaTrackConstraints | undefined;
264
246
  if (options) {
@@ -244,6 +244,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
244
244
  if (v.type === 'inbound-rtp') {
245
245
  receiverStats = {
246
246
  type: 'audio',
247
+ streamId: v.id,
247
248
  timestamp: v.timestamp,
248
249
  jitter: v.jitter,
249
250
  bytesReceived: v.bytesReceived,
@@ -177,6 +177,7 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
177
177
  codecID = v.codecId;
178
178
  receiverStats = {
179
179
  type: 'video',
180
+ streamId: v.id,
180
181
  framesDecoded: v.framesDecoded,
181
182
  framesDropped: v.framesDropped,
182
183
  framesReceived: v.framesReceived,
@@ -32,8 +32,8 @@ export async function createLocalTracks(
32
32
  ): Promise<Array<LocalTrack>> {
33
33
  // set default options to true
34
34
  options ??= {};
35
- options.audio ??= true;
36
- options.video ??= true;
35
+ options.audio ??= { deviceId: 'default' };
36
+ options.video ??= { deviceId: 'default' };
37
37
 
38
38
  const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
39
39
  const opts = mergeDefaultOptions(options, audioDefaults, videoDefaults);
@@ -1,17 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { AudioCaptureOptions, VideoCaptureOptions, VideoPresets } from './options';
2
+ import { audioDefaults, videoDefaults } from '../defaults';
3
+ import { type AudioCaptureOptions, VideoPresets } from './options';
3
4
  import { constraintsForOptions, diffAttributes, mergeDefaultOptions } from './utils';
4
5
 
5
6
  describe('mergeDefaultOptions', () => {
6
- const audioDefaults: AudioCaptureOptions = {
7
- autoGainControl: true,
8
- channelCount: 2,
9
- };
10
- const videoDefaults: VideoCaptureOptions = {
11
- deviceId: 'video123',
12
- resolution: VideoPresets.h1080.resolution,
13
- };
14
-
15
7
  it('does not enable undefined options', () => {
16
8
  const opts = mergeDefaultOptions(undefined, audioDefaults, videoDefaults);
17
9
  expect(opts.audio).toEqual(undefined);
@@ -69,7 +61,7 @@ describe('constraintsForOptions', () => {
69
61
  const constraints = constraintsForOptions({
70
62
  audio: true,
71
63
  });
72
- expect(constraints.audio).toEqual(true);
64
+ expect(constraints.audio).toEqual({ deviceId: audioDefaults.deviceId });
73
65
  expect(constraints.video).toEqual(false);
74
66
  });
75
67
 
@@ -81,7 +73,7 @@ describe('constraintsForOptions', () => {
81
73
  },
82
74
  });
83
75
  const audioOpts = constraints.audio as MediaTrackConstraints;
84
- expect(Object.keys(audioOpts)).toEqual(['noiseSuppression', 'echoCancellation']);
76
+ expect(Object.keys(audioOpts)).toEqual(['noiseSuppression', 'echoCancellation', 'deviceId']);
85
77
  expect(audioOpts.noiseSuppression).toEqual(true);
86
78
  expect(audioOpts.echoCancellation).toEqual(false);
87
79
  });
@@ -31,6 +31,7 @@ export function mergeDefaultOptions(
31
31
  clonedOptions.audio as Record<string, unknown>,
32
32
  audioDefaults as Record<string, unknown>,
33
33
  );
34
+ clonedOptions.audio.deviceId ??= 'default';
34
35
  if (audioProcessor) {
35
36
  clonedOptions.audio.processor = audioProcessor;
36
37
  }
@@ -40,6 +41,7 @@ export function mergeDefaultOptions(
40
41
  clonedOptions.video as Record<string, unknown>,
41
42
  videoDefaults as Record<string, unknown>,
42
43
  );
44
+ clonedOptions.video.deviceId ??= 'default';
43
45
  if (videoProcessor) {
44
46
  clonedOptions.video.processor = videoProcessor;
45
47
  }
@@ -77,8 +79,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
77
79
  }
78
80
  });
79
81
  constraints.video = videoOptions;
82
+ constraints.video.deviceId ??= 'default';
80
83
  } else {
81
- constraints.video = options.video;
84
+ constraints.video = options.video ? { deviceId: 'default' } : false;
82
85
  }
83
86
  } else {
84
87
  constraints.video = false;
@@ -87,8 +90,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
87
90
  if (options.audio) {
88
91
  if (typeof options.audio === 'object') {
89
92
  constraints.audio = options.audio;
93
+ constraints.audio.deviceId ??= 'default';
90
94
  } else {
91
- constraints.audio = true;
95
+ constraints.audio = { deviceId: 'default' };
92
96
  }
93
97
  } else {
94
98
  constraints.audio = false;