livekit-client 1.1.8 → 1.2.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.
Files changed (54) hide show
  1. package/README.md +1 -0
  2. package/dist/livekit-client.esm.mjs +677 -187
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/api/SignalClient.d.ts +4 -1
  7. package/dist/src/api/SignalClient.d.ts.map +1 -1
  8. package/dist/src/index.d.ts +4 -3
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/options.d.ts +1 -0
  11. package/dist/src/options.d.ts.map +1 -1
  12. package/dist/src/proto/livekit_models.d.ts +234 -0
  13. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  14. package/dist/src/proto/livekit_rtc.d.ts +944 -6
  15. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  16. package/dist/src/room/PCTransport.d.ts +9 -0
  17. package/dist/src/room/PCTransport.d.ts.map +1 -1
  18. package/dist/src/room/RTCEngine.d.ts +3 -2
  19. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  20. package/dist/src/room/Room.d.ts +3 -3
  21. package/dist/src/room/Room.d.ts.map +1 -1
  22. package/dist/src/room/events.d.ts +8 -1
  23. package/dist/src/room/events.d.ts.map +1 -1
  24. package/dist/src/room/participant/LocalParticipant.d.ts +3 -3
  25. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  26. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  27. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalTrack.d.ts +2 -0
  29. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  31. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/RemoteTrackPublication.d.ts +4 -1
  33. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  34. package/package.json +3 -1
  35. package/src/api/SignalClient.ts +21 -6
  36. package/src/index.ts +6 -2
  37. package/src/options.ts +1 -0
  38. package/src/proto/livekit_models.ts +179 -4
  39. package/src/proto/livekit_rtc.ts +14 -1
  40. package/src/room/PCTransport.ts +39 -0
  41. package/src/room/RTCEngine.ts +44 -18
  42. package/src/room/Room.ts +30 -24
  43. package/src/room/events.ts +7 -0
  44. package/src/room/participant/LocalParticipant.ts +70 -10
  45. package/src/room/participant/RemoteParticipant.ts +12 -10
  46. package/src/room/participant/publishUtils.ts +1 -1
  47. package/src/room/track/LocalAudioTrack.ts +16 -12
  48. package/src/room/track/LocalTrack.ts +41 -25
  49. package/src/room/track/LocalVideoTrack.ts +15 -11
  50. package/src/room/track/RemoteTrack.ts +1 -0
  51. package/src/room/track/RemoteTrackPublication.ts +37 -11
  52. package/dist/src/api/RequestQueue.d.ts +0 -13
  53. package/dist/src/api/RequestQueue.d.ts.map +0 -1
  54. package/src/api/RequestQueue.ts +0 -53
package/src/room/Room.ts CHANGED
@@ -5,6 +5,7 @@ import log from '../logger';
5
5
  import { RoomConnectOptions, RoomOptions } from '../options';
6
6
  import {
7
7
  DataPacket_Kind,
8
+ DisconnectReason,
8
9
  ParticipantInfo,
9
10
  ParticipantInfo_State,
10
11
  ParticipantPermission,
@@ -150,8 +151,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
150
151
  this.onTrackAdded(mediaTrack, stream, receiver);
151
152
  },
152
153
  )
153
- .on(EngineEvent.Disconnected, () => {
154
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
154
+ .on(EngineEvent.Disconnected, (reason?: DisconnectReason) => {
155
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, reason);
155
156
  })
156
157
  .on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
157
158
  .on(EngineEvent.DataPacketReceived, this.handleDataPacket)
@@ -357,7 +358,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
357
358
  /**
358
359
  * disconnects the room, emits [[RoomEvent.Disconnected]]
359
360
  */
360
- disconnect = (stopTracks = true) => {
361
+ disconnect = async (stopTracks = true) => {
362
+ log.info('disconnect from room', { identity: this.localParticipant.identity });
361
363
  if (this.state === ConnectionState.Connecting) {
362
364
  // try aborting pending connection attempt
363
365
  log.warn('abort connection attempt');
@@ -366,14 +368,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
366
368
  }
367
369
  // send leave
368
370
  if (this.engine?.client.isConnected) {
369
- this.engine.client.sendLeave();
371
+ await this.engine.client.sendLeave();
370
372
  }
371
373
  // close engine (also closes client)
372
374
  if (this.engine) {
373
375
  this.engine.close();
374
376
  }
375
377
 
376
- this.handleDisconnect(stopTracks);
378
+ this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
377
379
  /* @ts-ignore */
378
380
  this.engine = undefined;
379
381
  };
@@ -551,14 +553,15 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
551
553
  // at that time, ICE connectivity has not been established so the track is not
552
554
  // technically subscribed.
553
555
  // We'll defer these events until when the room is connected or eventually disconnected.
554
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
555
- setTimeout(() => {
556
+ if (this.connectFuture) {
557
+ this.connectFuture.promise.then(() => {
556
558
  this.onTrackAdded(mediaTrack, stream, receiver);
557
- }, 50);
559
+ });
558
560
  return;
559
561
  }
560
562
  if (this.state === ConnectionState.Disconnected) {
561
563
  log.warn('skipping incoming track after Room disconnected');
564
+ return;
562
565
  }
563
566
  const parts = unpackStreamId(stream.id);
564
567
  const participantId = parts[0];
@@ -644,7 +647,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
644
647
  );
645
648
  };
646
649
 
647
- private handleDisconnect(shouldStopTracks = true) {
650
+ private handleDisconnect(shouldStopTracks = true, reason?: DisconnectReason) {
651
+ if (this.state === ConnectionState.Disconnected) {
652
+ return;
653
+ }
648
654
  this.participants.forEach((p) => {
649
655
  p.tracks.forEach((pub) => {
650
656
  p.unpublishTrack(pub.trackSid);
@@ -660,6 +666,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
660
666
  pub.track?.stop();
661
667
  }
662
668
  });
669
+ this.localParticipant.tracks.clear();
670
+ this.localParticipant.videoTracks.clear();
671
+ this.localParticipant.audioTracks.clear();
663
672
 
664
673
  this.participants.clear();
665
674
  this.activeSpeakers = [];
@@ -672,7 +681,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
672
681
  navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
673
682
  }
674
683
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
675
- this.emit(RoomEvent.Disconnected);
684
+ this.emit(RoomEvent.Disconnected, reason);
676
685
  }
677
686
 
678
687
  private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
@@ -821,18 +830,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
821
830
  return;
822
831
  }
823
832
 
824
- pub._allowed = update.allowed;
825
- participant.emit(
826
- ParticipantEvent.TrackSubscriptionPermissionChanged,
827
- pub,
828
- pub.subscriptionStatus,
829
- );
830
- this.emitWhenConnected(
831
- RoomEvent.TrackSubscriptionPermissionChanged,
832
- pub,
833
- pub.subscriptionStatus,
834
- participant,
835
- );
833
+ pub.setAllowed(update.allowed);
836
834
  };
837
835
 
838
836
  private handleDataPacket = (userPacket: UserPacket, kind: DataPacket_Kind) => {
@@ -969,7 +967,15 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
969
967
  participant,
970
968
  );
971
969
  },
972
- );
970
+ )
971
+ .on(ParticipantEvent.TrackSubscriptionPermissionChanged, (pub, status) => {
972
+ this.emitWhenConnected(
973
+ RoomEvent.TrackSubscriptionPermissionChanged,
974
+ pub,
975
+ status,
976
+ participant,
977
+ );
978
+ });
973
979
 
974
980
  // update info at the end after callbacks have been set up
975
981
  if (info) {
@@ -1090,7 +1096,7 @@ export default Room;
1090
1096
  export type RoomEventCallbacks = {
1091
1097
  reconnecting: () => void;
1092
1098
  reconnected: () => void;
1093
- disconnected: () => void;
1099
+ disconnected: (reason?: DisconnectReason) => void;
1094
1100
  /** @deprecated stateChanged has been renamed to connectionStateChanged */
1095
1101
  stateChanged: (state: ConnectionState) => void;
1096
1102
  connectionStateChanged: (state: ConnectionState) => void;
@@ -402,6 +402,8 @@ export enum TrackEvent {
402
402
  Muted = 'muted',
403
403
  Unmuted = 'unmuted',
404
404
  Ended = 'ended',
405
+ Subscribed = 'subscribed',
406
+ Unsubscribed = 'unsubscribed',
405
407
  /** @internal */
406
408
  UpdateSettings = 'updateSettings',
407
409
  /** @internal */
@@ -433,4 +435,9 @@ export enum TrackEvent {
433
435
  * Only fires on LocalTracks
434
436
  */
435
437
  UpstreamResumed = 'upstreamResumed',
438
+ /**
439
+ * @internal
440
+ * Fires on RemoteTrackPublication
441
+ */
442
+ SubscriptionPermissionChanged = 'subscriptionPermissionChanged',
436
443
  }
@@ -32,7 +32,7 @@ import {
32
32
  } from '../track/options';
33
33
  import { Track } from '../track/Track';
34
34
  import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
35
- import { isFireFox } from '../utils';
35
+ import { isFireFox, isWeb } from '../utils';
36
36
  import Participant from './Participant';
37
37
  import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
38
38
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
@@ -125,8 +125,9 @@ export default class LocalParticipant extends Participant {
125
125
  setCameraEnabled(
126
126
  enabled: boolean,
127
127
  options?: VideoCaptureOptions,
128
+ publishOptions?: TrackPublishOptions,
128
129
  ): Promise<LocalTrackPublication | undefined> {
129
- return this.setTrackEnabled(Track.Source.Camera, enabled, options);
130
+ return this.setTrackEnabled(Track.Source.Camera, enabled, options, publishOptions);
130
131
  }
131
132
 
132
133
  /**
@@ -138,8 +139,9 @@ export default class LocalParticipant extends Participant {
138
139
  setMicrophoneEnabled(
139
140
  enabled: boolean,
140
141
  options?: AudioCaptureOptions,
142
+ publishOptions?: TrackPublishOptions,
141
143
  ): Promise<LocalTrackPublication | undefined> {
142
- return this.setTrackEnabled(Track.Source.Microphone, enabled, options);
144
+ return this.setTrackEnabled(Track.Source.Microphone, enabled, options, publishOptions);
143
145
  }
144
146
 
145
147
  /**
@@ -149,8 +151,9 @@ export default class LocalParticipant extends Participant {
149
151
  setScreenShareEnabled(
150
152
  enabled: boolean,
151
153
  options?: ScreenShareCaptureOptions,
154
+ publishOptions?: TrackPublishOptions,
152
155
  ): Promise<LocalTrackPublication | undefined> {
153
- return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options);
156
+ return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options, publishOptions);
154
157
  }
155
158
 
156
159
  /** @internal */
@@ -172,21 +175,25 @@ export default class LocalParticipant extends Participant {
172
175
  source: Extract<Track.Source, Track.Source.Camera>,
173
176
  enabled: boolean,
174
177
  options?: VideoCaptureOptions,
178
+ publishOptions?: TrackPublishOptions,
175
179
  ): Promise<LocalTrackPublication | undefined>;
176
180
  private async setTrackEnabled(
177
181
  source: Extract<Track.Source, Track.Source.Microphone>,
178
182
  enabled: boolean,
179
183
  options?: AudioCaptureOptions,
184
+ publishOptions?: TrackPublishOptions,
180
185
  ): Promise<LocalTrackPublication | undefined>;
181
186
  private async setTrackEnabled(
182
187
  source: Extract<Track.Source, Track.Source.ScreenShare>,
183
188
  enabled: boolean,
184
189
  options?: ScreenShareCaptureOptions,
190
+ publishOptions?: TrackPublishOptions,
185
191
  ): Promise<LocalTrackPublication | undefined>;
186
192
  private async setTrackEnabled(
187
193
  source: Track.Source,
188
194
  enabled: true,
189
195
  options?: VideoCaptureOptions | AudioCaptureOptions | ScreenShareCaptureOptions,
196
+ publishOptions?: TrackPublishOptions,
190
197
  ) {
191
198
  log.debug('setTrackEnabled', { source, enabled });
192
199
  let track = this.getTrack(source);
@@ -224,7 +231,7 @@ export default class LocalParticipant extends Participant {
224
231
  }
225
232
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
226
233
  for (const localTrack of localTracks) {
227
- publishPromises.push(this.publishTrack(localTrack));
234
+ publishPromises.push(this.publishTrack(localTrack, publishOptions));
228
235
  }
229
236
  const publishedTracks = await Promise.all(publishPromises);
230
237
  // for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
@@ -544,6 +551,14 @@ export default class LocalParticipant extends Participant {
544
551
  track.codec = opts.videoCodec;
545
552
  }
546
553
 
554
+ if (track.codec === 'av1' && encodings && encodings[0]?.maxBitrate) {
555
+ this.engine.publisher.setTrackCodecBitrate(
556
+ req.cid,
557
+ track.codec,
558
+ encodings[0].maxBitrate / 1000,
559
+ );
560
+ }
561
+
547
562
  this.engine.negotiate();
548
563
 
549
564
  // store RTPSender
@@ -642,6 +657,13 @@ export default class LocalParticipant extends Participant {
642
657
  this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
643
658
  track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
644
659
 
660
+ if (videoCodec === 'av1' && encodings[0]?.maxBitrate) {
661
+ this.engine.publisher.setTrackCodecBitrate(
662
+ req.cid,
663
+ videoCodec,
664
+ encodings[0].maxBitrate / 1000,
665
+ );
666
+ }
645
667
  this.engine.negotiate();
646
668
  log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
647
669
  }
@@ -894,11 +916,49 @@ export default class LocalParticipant extends Participant {
894
916
  this.unpublishTrack(track.track!);
895
917
  };
896
918
 
897
- private handleTrackEnded = (track: LocalTrack) => {
898
- log.debug('unpublishing local track due to TrackEnded', {
899
- track: track.sid,
900
- });
901
- this.unpublishTrack(track);
919
+ private handleTrackEnded = async (track: LocalTrack) => {
920
+ if (
921
+ track.source === Track.Source.ScreenShare ||
922
+ track.source === Track.Source.ScreenShareAudio
923
+ ) {
924
+ log.debug('unpublishing local track due to TrackEnded', {
925
+ track: track.sid,
926
+ });
927
+ this.unpublishTrack(track);
928
+ } else if (track.isUserProvided) {
929
+ await track.pauseUpstream();
930
+ } else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
931
+ try {
932
+ if (isWeb()) {
933
+ try {
934
+ const currentPermissions = await navigator?.permissions.query({
935
+ // the permission query for camera and microphone currently not supported in Safari and Firefox
936
+ // @ts-ignore
937
+ name: track.source === Track.Source.Camera ? 'camera' : 'microphone',
938
+ });
939
+ if (currentPermissions && currentPermissions.state === 'denied') {
940
+ log.warn(`user has revoked access to ${track.source}`);
941
+
942
+ // detect granted change after permissions were denied to try and resume then
943
+ currentPermissions.onchange = () => {
944
+ if (currentPermissions.state !== 'denied') {
945
+ track.restartTrack();
946
+ currentPermissions.onchange = null;
947
+ }
948
+ };
949
+ throw new Error('GetUserMedia Permission denied');
950
+ }
951
+ } catch (e: any) {
952
+ // permissions query fails for firefox, we continue and try to restart the track
953
+ }
954
+ }
955
+ log.debug('track ended, attempting to use a different device');
956
+ await track.restartTrack();
957
+ } catch (e) {
958
+ log.warn(`could not restart track, pausing upstream instead`);
959
+ await track.pauseUpstream();
960
+ }
961
+ }
902
962
  };
903
963
 
904
964
  private getPublicationForTrack(
@@ -7,6 +7,7 @@ import RemoteAudioTrack from '../track/RemoteAudioTrack';
7
7
  import RemoteTrackPublication from '../track/RemoteTrackPublication';
8
8
  import RemoteVideoTrack from '../track/RemoteVideoTrack';
9
9
  import { Track } from '../track/Track';
10
+ import { TrackPublication } from '../track/TrackPublication';
10
11
  import { AdaptiveStreamSettings, RemoteTrack } from '../track/types';
11
12
  import Participant, { ParticipantEventCallbacks } from './Participant';
12
13
 
@@ -49,8 +50,17 @@ export default class RemoteParticipant extends Participant {
49
50
  });
50
51
  this.signalClient.sendUpdateSubscription(sub);
51
52
  });
52
- publication.on(TrackEvent.Ended, (track: RemoteTrack) => {
53
- this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
53
+ publication.on(
54
+ TrackEvent.SubscriptionPermissionChanged,
55
+ (status: TrackPublication.SubscriptionStatus) => {
56
+ this.emit(ParticipantEvent.TrackSubscriptionPermissionChanged, publication, status);
57
+ },
58
+ );
59
+ publication.on(TrackEvent.Subscribed, (track: RemoteTrack) => {
60
+ this.emit(ParticipantEvent.TrackSubscribed, track, publication);
61
+ });
62
+ publication.on(TrackEvent.Unsubscribed, (previousTrack: RemoteTrack) => {
63
+ this.emit(ParticipantEvent.TrackUnsubscribed, previousTrack, publication);
54
64
  });
55
65
  }
56
66
 
@@ -156,8 +166,6 @@ export default class RemoteParticipant extends Participant {
156
166
  track.start();
157
167
 
158
168
  publication.setTrack(track);
159
- // subscription means participant has permissions to subscribe
160
- publication._allowed = true;
161
169
  // set participant volume on new microphone tracks
162
170
  if (
163
171
  this.volume !== undefined &&
@@ -166,7 +174,6 @@ export default class RemoteParticipant extends Participant {
166
174
  ) {
167
175
  track.setVolume(this.volume);
168
176
  }
169
- this.emit(ParticipantEvent.TrackSubscribed, track, publication);
170
177
 
171
178
  return publication;
172
179
  }
@@ -251,13 +258,8 @@ export default class RemoteParticipant extends Participant {
251
258
  // also send unsubscribe, if track is actively subscribed
252
259
  const { track } = publication;
253
260
  if (track) {
254
- const { isSubscribed } = publication;
255
261
  track.stop();
256
262
  publication.setTrack(undefined);
257
- // always send unsubscribed, since apps may rely on this
258
- if (isSubscribed) {
259
- this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
260
- }
261
263
  }
262
264
  if (sendUnpublish) {
263
265
  this.emit(ParticipantEvent.TrackUnpublished, publication);
@@ -106,7 +106,7 @@ export function computeVideoEncodings(
106
106
  encodings.push({
107
107
  rid: videoRids[2 - i],
108
108
  scaleResolutionDownBy: 2 ** i,
109
- maxBitrate: videoEncoding ? videoEncoding.maxBitrate / 2 ** i : 0,
109
+ maxBitrate: videoEncoding ? videoEncoding.maxBitrate / 3 ** i : 0,
110
110
  /* @ts-ignore */
111
111
  maxFramerate: original.encoding.maxFramerate,
112
112
  /* @ts-ignore */
@@ -35,22 +35,26 @@ export default class LocalAudioTrack extends LocalTrack {
35
35
  }
36
36
 
37
37
  async mute(): Promise<LocalAudioTrack> {
38
- // disabled special handling as it will cause BT headsets to switch communication modes
39
- if (this.source === Track.Source.Microphone && this.stopOnMute) {
40
- log.debug('stopping mic track');
41
- // also stop the track, so that microphone indicator is turned off
42
- this._mediaStreamTrack.stop();
43
- }
44
- await super.mute();
38
+ await this.muteQueue.run(async () => {
39
+ // disabled special handling as it will cause BT headsets to switch communication modes
40
+ if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
41
+ log.debug('stopping mic track');
42
+ // also stop the track, so that microphone indicator is turned off
43
+ this._mediaStreamTrack.stop();
44
+ }
45
+ await super.mute();
46
+ });
45
47
  return this;
46
48
  }
47
49
 
48
50
  async unmute(): Promise<LocalAudioTrack> {
49
- if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
50
- log.debug('reacquiring mic track');
51
- await this.restartTrack();
52
- }
53
- await super.unmute();
51
+ await this.muteQueue.run(async () => {
52
+ if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
53
+ log.debug('reacquiring mic track');
54
+ await this.restartTrack();
55
+ }
56
+ await super.unmute();
57
+ });
54
58
  return this;
55
59
  }
56
60
 
@@ -1,9 +1,10 @@
1
+ import Queue from 'async-await-queue';
1
2
  import log from '../../logger';
2
3
  import DeviceManager from '../DeviceManager';
3
4
  import { TrackInvalidError } from '../errors';
4
5
  import { TrackEvent } from '../events';
5
- import { VideoCodec } from './options';
6
6
  import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile } from '../utils';
7
+ import { VideoCodec } from './options';
7
8
  import { attachToElement, detachTrack, Track } from './Track';
8
9
 
9
10
  export default class LocalTrack extends Track {
@@ -21,6 +22,8 @@ export default class LocalTrack extends Track {
21
22
 
22
23
  protected providedByUser: boolean;
23
24
 
25
+ protected muteQueue: Queue;
26
+
24
27
  protected constructor(
25
28
  mediaTrack: MediaStreamTrack,
26
29
  kind: Track.Kind,
@@ -33,6 +36,7 @@ export default class LocalTrack extends Track {
33
36
  this.reacquireTrack = false;
34
37
  this.wasMuted = false;
35
38
  this.providedByUser = userProvidedTrack;
39
+ this.muteQueue = new Queue();
36
40
  }
37
41
 
38
42
  get id(): string {
@@ -101,7 +105,9 @@ export default class LocalTrack extends Track {
101
105
  // on Safari, the old audio track must be stopped before attempting to acquire
102
106
  // the new track, otherwise the new track will stop with
103
107
  // 'A MediaStreamTrack ended due to a capture failure`
104
- this._mediaStreamTrack.stop();
108
+ if (!this.providedByUser) {
109
+ this._mediaStreamTrack.stop();
110
+ }
105
111
 
106
112
  track.addEventListener('ended', this.handleEnded);
107
113
  log.debug('replace MediaStreamTrack');
@@ -111,6 +117,8 @@ export default class LocalTrack extends Track {
111
117
  }
112
118
  this._mediaStreamTrack = track;
113
119
 
120
+ await this.resumeUpstream();
121
+
114
122
  this.attachedElements.forEach((el) => {
115
123
  attachToElement(track, el);
116
124
  });
@@ -160,6 +168,8 @@ export default class LocalTrack extends Track {
160
168
 
161
169
  this._mediaStreamTrack = newTrack;
162
170
 
171
+ await this.resumeUpstream();
172
+
163
173
  this.attachedElements.forEach((el) => {
164
174
  attachToElement(newTrack, el);
165
175
  });
@@ -170,6 +180,7 @@ export default class LocalTrack extends Track {
170
180
  }
171
181
 
172
182
  protected setTrackMuted(muted: boolean) {
183
+ log.debug(`setting ${this.kind} track ${muted ? 'muted' : 'unmuted'}`);
173
184
  if (this.isMuted === muted) {
174
185
  return;
175
186
  }
@@ -215,31 +226,36 @@ export default class LocalTrack extends Track {
215
226
  };
216
227
 
217
228
  async pauseUpstream() {
218
- if (this._isUpstreamPaused === true) {
219
- return;
220
- }
221
- if (!this.sender) {
222
- log.warn('unable to pause upstream for an unpublished track');
223
- return;
224
- }
225
- this._isUpstreamPaused = true;
226
- this.emit(TrackEvent.UpstreamPaused, this);
227
- const emptyTrack =
228
- this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
229
- await this.sender.replaceTrack(emptyTrack);
229
+ this.muteQueue.run(async () => {
230
+ if (this._isUpstreamPaused === true) {
231
+ return;
232
+ }
233
+ if (!this.sender) {
234
+ log.warn('unable to pause upstream for an unpublished track');
235
+ return;
236
+ }
237
+
238
+ this._isUpstreamPaused = true;
239
+ this.emit(TrackEvent.UpstreamPaused, this);
240
+ const emptyTrack =
241
+ this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
242
+ await this.sender.replaceTrack(emptyTrack);
243
+ });
230
244
  }
231
245
 
232
246
  async resumeUpstream() {
233
- if (this._isUpstreamPaused === false) {
234
- return;
235
- }
236
- if (!this.sender) {
237
- log.warn('unable to resume upstream for an unpublished track');
238
- return;
239
- }
240
- this._isUpstreamPaused = false;
241
- this.emit(TrackEvent.UpstreamResumed, this);
242
-
243
- await this.sender.replaceTrack(this._mediaStreamTrack);
247
+ this.muteQueue.run(async () => {
248
+ if (this._isUpstreamPaused === false) {
249
+ return;
250
+ }
251
+ if (!this.sender) {
252
+ log.warn('unable to resume upstream for an unpublished track');
253
+ return;
254
+ }
255
+ this._isUpstreamPaused = false;
256
+ this.emit(TrackEvent.UpstreamResumed, this);
257
+
258
+ await this.sender.replaceTrack(this._mediaStreamTrack);
259
+ });
244
260
  }
245
261
  }
@@ -86,21 +86,25 @@ export default class LocalVideoTrack extends LocalTrack {
86
86
  }
87
87
 
88
88
  async mute(): Promise<LocalVideoTrack> {
89
- if (this.source === Track.Source.Camera) {
90
- log.debug('stopping camera track');
91
- // also stop the track, so that camera indicator is turned off
92
- this._mediaStreamTrack.stop();
93
- }
94
- await super.mute();
89
+ await this.muteQueue.run(async () => {
90
+ if (this.source === Track.Source.Camera && !this.isUserProvided) {
91
+ log.debug('stopping camera track');
92
+ // also stop the track, so that camera indicator is turned off
93
+ this._mediaStreamTrack.stop();
94
+ }
95
+ await super.mute();
96
+ });
95
97
  return this;
96
98
  }
97
99
 
98
100
  async unmute(): Promise<LocalVideoTrack> {
99
- if (this.source === Track.Source.Camera && !this.isUserProvided) {
100
- log.debug('reacquiring camera track');
101
- await this.restartTrack();
102
- }
103
- await super.unmute();
101
+ await this.muteQueue.run(async () => {
102
+ if (this.source === Track.Source.Camera && !this.isUserProvided) {
103
+ log.debug('reacquiring camera track');
104
+ await this.restartTrack();
105
+ }
106
+ await super.unmute();
107
+ });
104
108
  return this;
105
109
  }
106
110
 
@@ -21,6 +21,7 @@ export default abstract class RemoteTrack extends Track {
21
21
  setMuted(muted: boolean) {
22
22
  if (this.isMuted !== muted) {
23
23
  this.isMuted = muted;
24
+ this._mediaStreamTrack.enabled = !muted;
24
25
  this.emit(muted ? TrackEvent.Muted : TrackEvent.Unmuted, this);
25
26
  }
26
27
  }