livekit-client 2.10.0 → 2.11.1

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.
@@ -1,3 +1,4 @@
1
+ import { Mutex } from '@livekit/mutex';
1
2
  import {
2
3
  AddTrackRequest,
3
4
  BackupCodecPolicy,
@@ -27,10 +28,11 @@ import {
27
28
  UserPacket,
28
29
  protoInt64,
29
30
  } from '@livekit/protocol';
31
+ import { SignalConnectionState } from '../../api/SignalClient';
30
32
  import type { InternalRoomOptions } from '../../options';
31
33
  import { PCTransportState } from '../PCTransportManager';
32
34
  import type RTCEngine from '../RTCEngine';
33
- import { TextStreamWriter } from '../StreamWriter';
35
+ import { ByteStreamWriter, TextStreamWriter } from '../StreamWriter';
34
36
  import { defaultVideoCodec } from '../defaults';
35
37
  import {
36
38
  DeviceUnsupportedError,
@@ -53,6 +55,7 @@ import LocalTrack from '../track/LocalTrack';
53
55
  import LocalTrackPublication from '../track/LocalTrackPublication';
54
56
  import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
55
57
  import { Track } from '../track/Track';
58
+ import { createLocalTracks } from '../track/create';
56
59
  import type {
57
60
  AudioCaptureOptions,
58
61
  BackupVideoCodec,
@@ -63,8 +66,6 @@ import type {
63
66
  } from '../track/options';
64
67
  import { ScreenSharePresets, VideoPresets, isBackupCodec } from '../track/options';
65
68
  import {
66
- constraintsForOptions,
67
- extractProcessorsFromOptions,
68
69
  getLogContextFromTrack,
69
70
  getTrackSourceFromProto,
70
71
  mergeDefaultOptions,
@@ -72,6 +73,7 @@ import {
72
73
  screenCaptureToDisplayMediaStreamOptions,
73
74
  } from '../track/utils';
74
75
  import {
76
+ type ByteStreamInfo,
75
77
  type ChatMessage,
76
78
  type DataPublishOptions,
77
79
  type SendTextOptions,
@@ -104,7 +106,6 @@ import {
104
106
  computeTrackBackupEncodings,
105
107
  computeVideoEncodings,
106
108
  getDefaultDegradationPreference,
107
- mediaTrackToLocalTrack,
108
109
  } from './publishUtils';
109
110
 
110
111
  const STREAM_CHUNK_SIZE = 15_000;
@@ -528,9 +529,11 @@ export default class LocalParticipant extends Participant {
528
529
  ...this.logContext,
529
530
  ...getLogContextFromTrack(localTrack),
530
531
  });
532
+
531
533
  publishPromises.push(this.publishTrack(localTrack, publishOptions));
532
534
  }
533
535
  const publishedTracks = await Promise.all(publishPromises);
536
+
534
537
  // for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
535
538
  // revisit if we want to return an array of tracks instead for v2
536
539
  [track] = publishedTracks;
@@ -612,61 +615,37 @@ export default class LocalParticipant extends Participant {
612
615
  this.roomOptions?.videoCaptureDefaults,
613
616
  );
614
617
 
615
- const { audioProcessor, videoProcessor, optionsWithoutProcessor } =
616
- extractProcessorsFromOptions(mergedOptionsWithProcessors);
617
-
618
- const constraints = constraintsForOptions(optionsWithoutProcessor);
619
- let stream: MediaStream | undefined;
620
618
  try {
621
- stream = await navigator.mediaDevices.getUserMedia(constraints);
619
+ const tracks = await createLocalTracks(mergedOptionsWithProcessors, {
620
+ loggerName: this.roomOptions.loggerName,
621
+ loggerContextCb: () => this.logContext,
622
+ });
623
+ const localTracks = tracks.map((track) => {
624
+ if (isAudioTrack(track)) {
625
+ this.microphoneError = undefined;
626
+ track.setAudioContext(this.audioContext);
627
+ track.source = Track.Source.Microphone;
628
+ this.emit(ParticipantEvent.AudioStreamAcquired);
629
+ }
630
+ if (isVideoTrack(track)) {
631
+ this.cameraError = undefined;
632
+ track.source = Track.Source.Camera;
633
+ }
634
+ return track;
635
+ });
636
+ return localTracks;
622
637
  } catch (err) {
623
638
  if (err instanceof Error) {
624
- if (constraints.audio) {
639
+ if (options.audio) {
625
640
  this.microphoneError = err;
626
641
  }
627
- if (constraints.video) {
642
+ if (options.video) {
628
643
  this.cameraError = err;
629
644
  }
630
645
  }
631
646
 
632
647
  throw err;
633
648
  }
634
-
635
- if (constraints.audio) {
636
- this.microphoneError = undefined;
637
- this.emit(ParticipantEvent.AudioStreamAcquired);
638
- }
639
- if (constraints.video) {
640
- this.cameraError = undefined;
641
- }
642
-
643
- return Promise.all(
644
- stream.getTracks().map(async (mediaStreamTrack) => {
645
- const isAudio = mediaStreamTrack.kind === 'audio';
646
- let trackConstraints: MediaTrackConstraints | undefined;
647
- const conOrBool = isAudio ? constraints.audio : constraints.video;
648
- if (typeof conOrBool !== 'boolean') {
649
- trackConstraints = conOrBool;
650
- }
651
- const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints, {
652
- loggerName: this.roomOptions.loggerName,
653
- loggerContextCb: () => this.logContext,
654
- });
655
- if (track.kind === Track.Kind.Video) {
656
- track.source = Track.Source.Camera;
657
- } else if (track.kind === Track.Kind.Audio) {
658
- track.source = Track.Source.Microphone;
659
- track.setAudioContext(this.audioContext);
660
- }
661
- track.mediaStream = stream;
662
- if (isAudioTrack(track) && audioProcessor) {
663
- await track.setProcessor(audioProcessor);
664
- } else if (isVideoTrack(track) && videoProcessor) {
665
- await track.setProcessor(videoProcessor);
666
- }
667
- return track;
668
- }),
669
- );
670
649
  }
671
650
 
672
651
  /**
@@ -862,7 +841,47 @@ export default class LocalParticipant extends Participant {
862
841
  if (opts.source) {
863
842
  track.source = opts.source;
864
843
  }
865
- const publishPromise = this.publish(track, opts, isStereo);
844
+ const publishPromise = new Promise<LocalTrackPublication>(async (resolve, reject) => {
845
+ try {
846
+ if (this.engine.client.currentState !== SignalConnectionState.CONNECTED) {
847
+ this.log.debug('deferring track publication until signal is connected', {
848
+ ...this.logContext,
849
+ track: getLogContextFromTrack(track),
850
+ });
851
+ const onSignalConnected = async () => {
852
+ try {
853
+ const publication = await this.publish(track, opts, isStereo);
854
+ resolve(publication);
855
+ } catch (e) {
856
+ reject(e);
857
+ }
858
+ };
859
+ setTimeout(() => {
860
+ this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
861
+ reject(
862
+ new PublishTrackError(
863
+ 'publishing rejected as engine not connected within timeout',
864
+ 408,
865
+ ),
866
+ );
867
+ }, 15_000);
868
+ this.engine.once(EngineEvent.SignalConnected, onSignalConnected);
869
+ this.engine.on(EngineEvent.Closing, () => {
870
+ this.engine.off(EngineEvent.SignalConnected, onSignalConnected);
871
+ reject(new PublishTrackError('publishing rejected as engine closed', 499));
872
+ });
873
+ } else {
874
+ try {
875
+ const publication = await this.publish(track, opts, isStereo);
876
+ resolve(publication);
877
+ } catch (e) {
878
+ reject(e);
879
+ }
880
+ }
881
+ } catch (e) {
882
+ reject(e);
883
+ }
884
+ });
866
885
  this.pendingPublishPromises.set(track, publishPromise);
867
886
  try {
868
887
  const publication = await publishPromise;
@@ -1713,23 +1732,62 @@ export default class LocalParticipant extends Participant {
1713
1732
  onProgress?: (progress: number) => void;
1714
1733
  },
1715
1734
  ) {
1716
- const totalLength = file.size;
1717
- const header = new DataStream_Header({
1718
- totalLength: numberToBigInt(totalLength),
1719
- mimeType: options?.mimeType ?? file.type,
1735
+ const writer = await this.streamBytes({
1720
1736
  streamId,
1737
+ totalSize: file.size,
1738
+ name: file.name,
1739
+ mimeType: options?.mimeType ?? file.type,
1721
1740
  topic: options?.topic,
1722
- encryptionType: options?.encryptionType,
1741
+ destinationIdentities: options?.destinationIdentities,
1742
+ });
1743
+ const reader = file.stream().getReader();
1744
+ while (true) {
1745
+ const { done, value } = await reader.read();
1746
+ if (done) {
1747
+ break;
1748
+ }
1749
+ await writer.write(value);
1750
+ }
1751
+ await writer.close();
1752
+ return writer.info;
1753
+ }
1754
+
1755
+ async streamBytes(options?: {
1756
+ name?: string;
1757
+ topic?: string;
1758
+ attributes?: Record<string, string>;
1759
+ destinationIdentities?: Array<string>;
1760
+ streamId?: string;
1761
+ mimeType?: string;
1762
+ totalSize?: number;
1763
+ }) {
1764
+ const streamId = options?.streamId ?? crypto.randomUUID();
1765
+ const destinationIdentities = options?.destinationIdentities;
1766
+
1767
+ const info: ByteStreamInfo = {
1768
+ id: streamId,
1769
+ mimeType: options?.mimeType ?? 'application/octet-stream',
1770
+ topic: options?.topic ?? '',
1771
+ timestamp: Date.now(),
1772
+ attributes: options?.attributes,
1773
+ size: options?.totalSize,
1774
+ name: options?.name ?? 'unknown',
1775
+ };
1776
+
1777
+ const header = new DataStream_Header({
1778
+ totalLength: numberToBigInt(info.size ?? 0),
1779
+ mimeType: info.mimeType,
1780
+ streamId,
1781
+ topic: info.topic,
1723
1782
  timestamp: numberToBigInt(Date.now()),
1724
1783
  contentHeader: {
1725
1784
  case: 'byteHeader',
1726
1785
  value: new DataStream_ByteHeader({
1727
- name: file.name,
1786
+ name: info.name,
1728
1787
  }),
1729
1788
  },
1730
1789
  });
1731
1790
 
1732
- const destinationIdentities = options?.destinationIdentities;
1733
1791
  const packet = new DataPacket({
1734
1792
  destinationIdentities,
1735
1793
  value: {
@@ -1739,47 +1797,61 @@ export default class LocalParticipant extends Participant {
1739
1797
  });
1740
1798
 
1741
1799
  await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
1742
- function read(b: Blob): Promise<Uint8Array> {
1743
- return new Promise((resolve) => {
1744
- const fr = new FileReader();
1745
- fr.onload = () => {
1746
- resolve(new Uint8Array(fr.result as ArrayBuffer));
1747
- };
1748
- fr.readAsArrayBuffer(b);
1749
- });
1750
- }
1751
- const totalChunks = Math.ceil(totalLength / STREAM_CHUNK_SIZE);
1752
- for (let i = 0; i < totalChunks; i++) {
1753
- const chunkData = await read(
1754
- file.slice(i * STREAM_CHUNK_SIZE, Math.min((i + 1) * STREAM_CHUNK_SIZE, totalLength)),
1755
- );
1756
- await this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1757
- const chunk = new DataStream_Chunk({
1758
- content: chunkData,
1759
- streamId,
1760
- chunkIndex: numberToBigInt(i),
1761
- });
1762
- const chunkPacket = new DataPacket({
1763
- destinationIdentities,
1764
- value: {
1765
- case: 'streamChunk',
1766
- value: chunk,
1767
- },
1768
- });
1769
- await this.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
1770
- options?.onProgress?.((i + 1) / totalChunks);
1771
- }
1772
- const trailer = new DataStream_Trailer({
1773
- streamId,
1774
- });
1775
- const trailerPacket = new DataPacket({
1776
- destinationIdentities,
1777
- value: {
1778
- case: 'streamTrailer',
1779
- value: trailer,
1800
+
1801
+ let chunkId = 0;
1802
+ const writeMutex = new Mutex();
1803
+ const engine = this.engine;
1804
+ const log = this.log;
1805
+
1806
+ const writableStream = new WritableStream<Uint8Array>({
1807
+ async write(chunk) {
1808
+ const unlock = await writeMutex.lock();
1809
+
1810
+ let byteOffset = 0;
1811
+ try {
1812
+ while (byteOffset < chunk.byteLength) {
1813
+ const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
1814
+ await engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1815
+ const chunkPacket = new DataPacket({
1816
+ destinationIdentities,
1817
+ value: {
1818
+ case: 'streamChunk',
1819
+ value: new DataStream_Chunk({
1820
+ content: subChunk,
1821
+ streamId,
1822
+ chunkIndex: numberToBigInt(chunkId),
1823
+ }),
1824
+ },
1825
+ });
1826
+ await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
1827
+ chunkId += 1;
1828
+ byteOffset += subChunk.byteLength;
1829
+ }
1830
+ } finally {
1831
+ unlock();
1832
+ }
1833
+ },
1834
+ async close() {
1835
+ const trailer = new DataStream_Trailer({
1836
+ streamId,
1837
+ });
1838
+ const trailerPacket = new DataPacket({
1839
+ destinationIdentities,
1840
+ value: {
1841
+ case: 'streamTrailer',
1842
+ value: trailer,
1843
+ },
1844
+ });
1845
+ await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
1846
+ },
1847
+ abort(err) {
1848
+ log.error('Sink error:', err);
1780
1849
  },
1781
1850
  });
1782
- await this.engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
1851
+
1852
+ const byteWriter = new ByteStreamWriter(writableStream, info);
1853
+
1854
+ return byteWriter;
1783
1855
  }
1784
1856
 
1785
1857
  /**
@@ -2,6 +2,7 @@ import DeviceManager from '../DeviceManager';
2
2
  import { audioDefaults, videoDefaults } from '../defaults';
3
3
  import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
4
4
  import { mediaTrackToLocalTrack } from '../participant/publishUtils';
5
+ import type { LoggerOptions } from '../types';
5
6
  import { isAudioTrack, isSafari17, isVideoTrack, unwrapConstraint } from '../utils';
6
7
  import LocalAudioTrack from './LocalAudioTrack';
7
8
  import type LocalTrack from './LocalTrack';
@@ -29,72 +30,126 @@ import {
29
30
  */
30
31
  export async function createLocalTracks(
31
32
  options?: CreateLocalTracksOptions,
33
+ loggerOptions?: LoggerOptions,
32
34
  ): Promise<Array<LocalTrack>> {
33
35
  // set default options to true
34
- options ??= {};
35
- options.audio ??= { deviceId: 'default' };
36
- options.video ??= { deviceId: 'default' };
37
-
38
- const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(options);
39
- const opts = mergeDefaultOptions(options, audioDefaults, videoDefaults);
36
+ const internalOptions = { ...(options ?? {}) };
37
+ let attemptExactMatch = false;
38
+ let retryAudioOptions: AudioCaptureOptions | undefined | boolean = options?.audio;
39
+ let retryVideoOptions: VideoCaptureOptions | undefined | boolean = options?.video;
40
+ // if the user passes a device id as a string, we default to exact match
41
+ if (
42
+ internalOptions.audio &&
43
+ typeof internalOptions.audio === 'object' &&
44
+ typeof internalOptions.audio.deviceId === 'string'
45
+ ) {
46
+ const deviceId: string = internalOptions.audio.deviceId;
47
+ internalOptions.audio.deviceId = { exact: deviceId };
48
+ attemptExactMatch = true;
49
+ retryAudioOptions = {
50
+ ...internalOptions.audio,
51
+ deviceId: { ideal: deviceId },
52
+ };
53
+ }
54
+ if (
55
+ internalOptions.video &&
56
+ typeof internalOptions.video === 'object' &&
57
+ typeof internalOptions.video.deviceId === 'string'
58
+ ) {
59
+ const deviceId: string = internalOptions.video.deviceId;
60
+ internalOptions.video.deviceId = { exact: deviceId };
61
+ attemptExactMatch = true;
62
+ retryVideoOptions = {
63
+ ...internalOptions.video,
64
+ deviceId: { ideal: deviceId },
65
+ };
66
+ }
67
+ // TODO if internal options don't have device Id specified, set it to 'default'
68
+ if (
69
+ internalOptions.audio === true ||
70
+ (typeof internalOptions.audio === 'object' && !internalOptions.audio.deviceId)
71
+ ) {
72
+ internalOptions.audio = { deviceId: 'default' };
73
+ }
74
+ if (
75
+ internalOptions.video === true ||
76
+ (typeof internalOptions.video === 'object' && !internalOptions.video.deviceId)
77
+ ) {
78
+ internalOptions.video = { deviceId: 'default' };
79
+ }
80
+ const { audioProcessor, videoProcessor } = extractProcessorsFromOptions(internalOptions);
81
+ const opts = mergeDefaultOptions(internalOptions, audioDefaults, videoDefaults);
40
82
  const constraints = constraintsForOptions(opts);
41
83
 
42
84
  // Keep a reference to the promise on DeviceManager and await it in getLocalDevices()
43
85
  // works around iOS Safari Bug https://bugs.webkit.org/show_bug.cgi?id=179363
44
86
  const mediaPromise = navigator.mediaDevices.getUserMedia(constraints);
45
87
 
46
- if (options.audio) {
88
+ if (internalOptions.audio) {
47
89
  DeviceManager.userMediaPromiseMap.set('audioinput', mediaPromise);
48
90
  mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('audioinput'));
49
91
  }
50
- if (options.video) {
92
+ if (internalOptions.video) {
51
93
  DeviceManager.userMediaPromiseMap.set('videoinput', mediaPromise);
52
94
  mediaPromise.catch(() => DeviceManager.userMediaPromiseMap.delete('videoinput'));
53
95
  }
96
+ try {
97
+ const stream = await mediaPromise;
98
+ return await Promise.all(
99
+ stream.getTracks().map(async (mediaStreamTrack) => {
100
+ const isAudio = mediaStreamTrack.kind === 'audio';
101
+ let trackOptions = isAudio ? opts!.audio : opts!.video;
102
+ if (typeof trackOptions === 'boolean' || !trackOptions) {
103
+ trackOptions = {};
104
+ }
105
+ let trackConstraints: MediaTrackConstraints | undefined;
106
+ const conOrBool = isAudio ? constraints.audio : constraints.video;
107
+ if (typeof conOrBool !== 'boolean') {
108
+ trackConstraints = conOrBool;
109
+ }
54
110
 
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
- }
68
-
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
+ // update the constraints with the device id the user gave permissions to in the permission prompt
112
+ // otherwise each track restart (e.g. mute - unmute) will try to initialize the device again -> causing additional permission prompts
113
+ const newDeviceId = mediaStreamTrack.getSettings().deviceId;
114
+ if (
115
+ trackConstraints?.deviceId &&
116
+ unwrapConstraint(trackConstraints.deviceId) !== newDeviceId
117
+ ) {
118
+ trackConstraints.deviceId = newDeviceId;
119
+ } else if (!trackConstraints) {
120
+ trackConstraints = { deviceId: newDeviceId };
121
+ }
80
122
 
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;
123
+ const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints, loggerOptions);
124
+ if (track.kind === Track.Kind.Video) {
125
+ track.source = Track.Source.Camera;
126
+ } else if (track.kind === Track.Kind.Audio) {
127
+ track.source = Track.Source.Microphone;
128
+ }
129
+ track.mediaStream = stream;
88
130
 
89
- if (isAudioTrack(track) && audioProcessor) {
90
- await track.setProcessor(audioProcessor);
91
- } else if (isVideoTrack(track) && videoProcessor) {
92
- await track.setProcessor(videoProcessor);
93
- }
131
+ if (isAudioTrack(track) && audioProcessor) {
132
+ await track.setProcessor(audioProcessor);
133
+ } else if (isVideoTrack(track) && videoProcessor) {
134
+ await track.setProcessor(videoProcessor);
135
+ }
94
136
 
95
- return track;
96
- }),
97
- );
137
+ return track;
138
+ }),
139
+ );
140
+ } catch (e) {
141
+ if (!attemptExactMatch) {
142
+ throw e;
143
+ }
144
+ return createLocalTracks(
145
+ {
146
+ ...options,
147
+ audio: retryAudioOptions,
148
+ video: retryVideoOptions,
149
+ },
150
+ loggerOptions,
151
+ );
152
+ }
98
153
  }
99
154
 
100
155
  /**
@@ -33,7 +33,7 @@ export function mergeDefaultOptions(
33
33
  clonedOptions.audio as Record<string, unknown>,
34
34
  audioDefaults as Record<string, unknown>,
35
35
  );
36
- clonedOptions.audio.deviceId ??= 'default';
36
+ clonedOptions.audio.deviceId ??= { ideal: 'default' };
37
37
  if (audioProcessor || defaultAudioProcessor) {
38
38
  clonedOptions.audio.processor = audioProcessor ?? defaultAudioProcessor;
39
39
  }
@@ -43,7 +43,7 @@ export function mergeDefaultOptions(
43
43
  clonedOptions.video as Record<string, unknown>,
44
44
  videoDefaults as Record<string, unknown>,
45
45
  );
46
- clonedOptions.video.deviceId ??= 'default';
46
+ clonedOptions.video.deviceId ??= { ideal: 'default' };
47
47
  if (videoProcessor || defaultVideoProcessor) {
48
48
  clonedOptions.video.processor = videoProcessor ?? defaultVideoProcessor;
49
49
  }
@@ -81,9 +81,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
81
81
  }
82
82
  });
83
83
  constraints.video = videoOptions;
84
- constraints.video.deviceId ??= 'default';
84
+ constraints.video.deviceId ??= { ideal: 'default' };
85
85
  } else {
86
- constraints.video = options.video ? { deviceId: 'default' } : false;
86
+ constraints.video = options.video ? { deviceId: { ideal: 'default' } } : false;
87
87
  }
88
88
  } else {
89
89
  constraints.video = false;
@@ -92,9 +92,9 @@ export function constraintsForOptions(options: CreateLocalTracksOptions): MediaS
92
92
  if (options.audio) {
93
93
  if (typeof options.audio === 'object') {
94
94
  constraints.audio = options.audio;
95
- constraints.audio.deviceId ??= 'default';
95
+ constraints.audio.deviceId ??= { ideal: 'default' };
96
96
  } else {
97
- constraints.audio = { deviceId: 'default' };
97
+ constraints.audio = { deviceId: { ideal: 'default' } };
98
98
  }
99
99
  } else {
100
100
  constraints.audio = false;