livekit-client 1.6.5 → 1.6.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/dist/livekit-client.esm.mjs +149 -130
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/room/PCTransport.d.ts.map +1 -1
  6. package/dist/src/room/RTCEngine.d.ts +10 -3
  7. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  8. package/dist/src/room/Room.d.ts.map +1 -1
  9. package/dist/src/room/events.d.ts +2 -1
  10. package/dist/src/room/events.d.ts.map +1 -1
  11. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  12. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  13. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -0
  14. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  15. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  16. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  17. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  18. package/dist/src/room/track/Track.d.ts +2 -1
  19. package/dist/src/room/track/Track.d.ts.map +1 -1
  20. package/dist/src/room/track/options.d.ts +2 -2
  21. package/dist/ts4.2/src/room/RTCEngine.d.ts +10 -3
  22. package/dist/ts4.2/src/room/events.d.ts +2 -1
  23. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -0
  24. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  25. package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
  26. package/dist/ts4.2/src/room/track/options.d.ts +2 -2
  27. package/package.json +13 -13
  28. package/src/room/PCTransport.ts +2 -0
  29. package/src/room/RTCEngine.ts +46 -51
  30. package/src/room/Room.ts +13 -3
  31. package/src/room/events.ts +2 -1
  32. package/src/room/participant/LocalParticipant.ts +20 -8
  33. package/src/room/participant/RemoteParticipant.ts +2 -3
  34. package/src/room/track/LocalVideoTrack.ts +62 -46
  35. package/src/room/track/RemoteTrackPublication.ts +3 -2
  36. package/src/room/track/RemoteVideoTrack.ts +0 -3
  37. package/src/room/track/Track.ts +2 -1
  38. package/src/room/track/options.ts +2 -2
@@ -118,8 +118,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
118
118
 
119
119
  private clientConfiguration?: ClientConfiguration;
120
120
 
121
- private connectedServerAddr?: string;
122
-
123
121
  private attemptingReconnect: boolean = false;
124
122
 
125
123
  private reconnectPolicy: ReconnectPolicy;
@@ -136,6 +134,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
136
134
 
137
135
  private closingLock: Mutex;
138
136
 
137
+ private shouldFailNext: boolean = false;
138
+
139
139
  constructor(private options: InternalRoomOptions) {
140
140
  super();
141
141
  this.client = new SignalClient();
@@ -247,7 +247,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
247
247
  });
248
248
  }
249
249
 
250
- removeTrack(sender: RTCRtpSender) {
250
+ /**
251
+ * Removes sender from PeerConnection, returning true if it was removed successfully
252
+ * and a negotiation is necessary
253
+ * @param sender
254
+ * @returns
255
+ */
256
+ removeTrack(sender: RTCRtpSender): boolean {
251
257
  if (sender.track && this.pendingTrackResolvers[sender.track.id]) {
252
258
  const { reject } = this.pendingTrackResolvers[sender.track.id];
253
259
  if (reject) {
@@ -257,9 +263,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
257
263
  }
258
264
  try {
259
265
  this.publisher?.pc.removeTrack(sender);
266
+ return true;
260
267
  } catch (e: unknown) {
261
268
  log.warn('failed to remove track', { error: e, method: 'removeTrack' });
262
269
  }
270
+ return false;
263
271
  }
264
272
 
265
273
  updateMuteStatus(trackSid: string, muted: boolean) {
@@ -270,8 +278,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
270
278
  return this.reliableDCSub?.readyState;
271
279
  }
272
280
 
273
- get connectedServerAddress(): string | undefined {
274
- return this.connectedServerAddr;
281
+ async getConnectedServerAddress(): Promise<string | undefined> {
282
+ if (this.primaryPC === undefined) {
283
+ return undefined;
284
+ }
285
+ return getConnectedAddress(this.primaryPC);
275
286
  }
276
287
 
277
288
  private configure(joinResponse: JoinResponse) {
@@ -317,11 +328,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
317
328
  primaryPC.onconnectionstatechange = async () => {
318
329
  log.debug(`primary PC state changed ${primaryPC.connectionState}`);
319
330
  if (primaryPC.connectionState === 'connected') {
320
- try {
321
- this.connectedServerAddr = await getConnectedAddress(primaryPC);
322
- } catch (e) {
323
- log.warn('could not get connected server address', { error: e });
324
- }
325
331
  const shouldEmit = this.pcState === PCState.New;
326
332
  this.pcState = PCState.Connected;
327
333
  if (shouldEmit) {
@@ -334,7 +340,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
334
340
 
335
341
  this.handleDisconnect(
336
342
  'primary peerconnection',
337
- false,
338
343
  subscriberPrimary
339
344
  ? ReconnectReason.REASON_SUBSCRIBER_FAILED
340
345
  : ReconnectReason.REASON_PUBLISHER_FAILED,
@@ -348,7 +353,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
348
353
  if (secondaryPC.connectionState === 'failed') {
349
354
  this.handleDisconnect(
350
355
  'secondary peerconnection',
351
- false,
352
356
  subscriberPrimary
353
357
  ? ReconnectReason.REASON_PUBLISHER_FAILED
354
358
  : ReconnectReason.REASON_SUBSCRIBER_FAILED,
@@ -419,7 +423,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
419
423
  };
420
424
 
421
425
  this.client.onClose = () => {
422
- this.handleDisconnect('signal', false, ReconnectReason.REASON_SIGNAL_DISCONNECTED);
426
+ this.handleDisconnect('signal', ReconnectReason.REASON_SIGNAL_DISCONNECTED);
423
427
  };
424
428
 
425
429
  this.client.onLeave = (leave?: LeaveRequest) => {
@@ -690,11 +694,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
690
694
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
691
695
  // continues to work, we can reconnect to websocket to continue the session
692
696
  // after a number of retries, we'll close and give up permanently
693
- private handleDisconnect = (
694
- connection: string,
695
- signalEvents: boolean = false,
696
- disconnectReason?: ReconnectReason,
697
- ) => {
697
+ private handleDisconnect = (connection: string, disconnectReason?: ReconnectReason) => {
698
698
  if (this._isClosed) {
699
699
  return;
700
700
  }
@@ -731,12 +731,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
731
731
 
732
732
  this.clearReconnectTimeout();
733
733
  this.reconnectTimeout = CriticalTimers.setTimeout(
734
- () => this.attemptReconnect(signalEvents, disconnectReason),
734
+ () => this.attemptReconnect(disconnectReason),
735
735
  delay,
736
736
  );
737
737
  };
738
738
 
739
- private async attemptReconnect(signalEvents: boolean = false, reason?: ReconnectReason) {
739
+ private async attemptReconnect(reason?: ReconnectReason) {
740
740
  if (this._isClosed) {
741
741
  return;
742
742
  }
@@ -756,35 +756,26 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
756
756
  try {
757
757
  this.attemptingReconnect = true;
758
758
  if (this.fullReconnectOnNext) {
759
- await this.restartConnection(signalEvents);
759
+ await this.restartConnection();
760
760
  } else {
761
- await this.resumeConnection(signalEvents, reason);
761
+ await this.resumeConnection(reason);
762
762
  }
763
763
  this.clearPendingReconnect();
764
764
  this.fullReconnectOnNext = false;
765
765
  } catch (e) {
766
766
  this.reconnectAttempts += 1;
767
- let reconnectRequired = false;
768
767
  let recoverable = true;
769
- let requireSignalEvents = false;
770
768
  if (e instanceof UnexpectedConnectionState) {
771
769
  log.debug('received unrecoverable error', { error: e });
772
770
  // unrecoverable
773
771
  recoverable = false;
774
772
  } else if (!(e instanceof SignalReconnectError)) {
775
773
  // cannot resume
776
- reconnectRequired = true;
777
- }
778
-
779
- // when we flip from resume to reconnect
780
- // we need to fire the right reconnecting events
781
- if (reconnectRequired && !this.fullReconnectOnNext) {
782
774
  this.fullReconnectOnNext = true;
783
- requireSignalEvents = true;
784
775
  }
785
776
 
786
777
  if (recoverable) {
787
- this.handleDisconnect('reconnect', requireSignalEvents, ReconnectReason.REASON_UNKOWN);
778
+ this.handleDisconnect('reconnect', ReconnectReason.REASON_UNKOWN);
788
779
  } else {
789
780
  log.info(
790
781
  `could not recover connection after ${this.reconnectAttempts} attempts, ${
@@ -810,16 +801,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
810
801
  return null;
811
802
  }
812
803
 
813
- private async restartConnection(emitRestarting: boolean = false) {
804
+ private async restartConnection() {
814
805
  if (!this.url || !this.token) {
815
806
  // permanent failure, don't attempt reconnection
816
807
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
817
808
  }
818
809
 
819
810
  log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
820
- if (emitRestarting || this.reconnectAttempts === 0) {
821
- this.emit(EngineEvent.Restarting);
822
- }
811
+ this.emit(EngineEvent.Restarting);
823
812
 
824
813
  if (this.client.isConnected) {
825
814
  await this.client.sendLeave();
@@ -842,6 +831,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
842
831
  throw new SignalReconnectError();
843
832
  }
844
833
 
834
+ if (this.shouldFailNext) {
835
+ this.shouldFailNext = false;
836
+ throw new Error('simulated failure');
837
+ }
838
+
845
839
  await this.waitForPCConnected();
846
840
  this.client.setReconnected();
847
841
 
@@ -849,10 +843,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
849
843
  this.emit(EngineEvent.Restarted, joinResponse);
850
844
  }
851
845
 
852
- private async resumeConnection(
853
- emitResuming: boolean = false,
854
- reason?: ReconnectReason,
855
- ): Promise<void> {
846
+ private async resumeConnection(reason?: ReconnectReason): Promise<void> {
856
847
  if (!this.url || !this.token) {
857
848
  // permanent failure, don't attempt reconnection
858
849
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -863,9 +854,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
863
854
  }
864
855
 
865
856
  log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`);
866
- if (emitResuming || this.reconnectAttempts === 0) {
867
- this.emit(EngineEvent.Resuming);
868
- }
857
+ this.emit(EngineEvent.Resuming);
869
858
 
870
859
  try {
871
860
  const res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
@@ -883,6 +872,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
883
872
  }
884
873
  this.emit(EngineEvent.SignalResumed);
885
874
 
875
+ if (this.shouldFailNext) {
876
+ this.shouldFailNext = false;
877
+ throw new Error('simulated failure');
878
+ }
879
+
886
880
  this.subscriber.restartingIce = true;
887
881
 
888
882
  // only restart publisher if it's needed
@@ -921,11 +915,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
921
915
  this.primaryPC?.connectionState === 'connected'
922
916
  ) {
923
917
  this.pcState = PCState.Connected;
924
- try {
925
- this.connectedServerAddr = await getConnectedAddress(this.primaryPC);
926
- } catch (e) {
927
- log.warn('could not get connected server address', { error: e });
928
- }
929
918
  }
930
919
  if (this.pcState === PCState.Connected) {
931
920
  return;
@@ -1022,7 +1011,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1022
1011
 
1023
1012
  const negotiationTimeout = setTimeout(() => {
1024
1013
  reject('negotiation timed out');
1025
- this.handleDisconnect('negotiation', false, ReconnectReason.REASON_SIGNAL_DISCONNECTED);
1014
+ this.handleDisconnect('negotiation', ReconnectReason.REASON_SIGNAL_DISCONNECTED);
1026
1015
  }, this.peerConnectionTimeout);
1027
1016
 
1028
1017
  const cleanup = () => {
@@ -1043,7 +1032,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1043
1032
  if (e instanceof NegotiationError) {
1044
1033
  this.fullReconnectOnNext = true;
1045
1034
  }
1046
- this.handleDisconnect('negotiation', false, ReconnectReason.REASON_UNKOWN);
1035
+ this.handleDisconnect('negotiation', ReconnectReason.REASON_UNKOWN);
1047
1036
  });
1048
1037
  });
1049
1038
  }
@@ -1066,6 +1055,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1066
1055
  }
1067
1056
  }
1068
1057
 
1058
+ /* @internal */
1059
+ failNext() {
1060
+ // debugging method to fail the next reconnect/resume attempt
1061
+ this.shouldFailNext = true;
1062
+ }
1063
+
1069
1064
  private clearReconnectTimeout() {
1070
1065
  if (this.reconnectTimeout) {
1071
1066
  CriticalTimers.clearTimeout(this.reconnectTimeout);
@@ -1081,7 +1076,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1081
1076
  // in case the engine is currently reconnecting, attempt a reconnect immediately after the browser state has changed to 'onLine'
1082
1077
  if (this.client.isReconnecting) {
1083
1078
  this.clearReconnectTimeout();
1084
- this.attemptReconnect(true, ReconnectReason.REASON_SIGNAL_DISCONNECTED);
1079
+ this.attemptReconnect(ReconnectReason.REASON_SIGNAL_DISCONNECTED);
1085
1080
  }
1086
1081
  };
1087
1082
 
package/src/room/Room.ts CHANGED
@@ -56,8 +56,8 @@ import type { AdaptiveStreamSettings } from './track/types';
56
56
  import { getNewAudioContext } from './track/utils';
57
57
  import type { SimulationOptions } from './types';
58
58
  import {
59
- Future,
60
59
  createDummyVideoStreamTrack,
60
+ Future,
61
61
  getEmptyAudioStreamTrack,
62
62
  isWeb,
63
63
  Mutex,
@@ -516,6 +516,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
516
516
  },
517
517
  });
518
518
  break;
519
+ case 'resume-reconnect':
520
+ this.engine.failNext();
521
+ await this.engine.client.close();
522
+ if (this.engine.client.onClose) {
523
+ this.engine.client.onClose('simulate resume-reconnect');
524
+ }
525
+ break;
519
526
  case 'force-tcp':
520
527
  case 'force-tls':
521
528
  req = SimulateScenario.fromPartial({
@@ -786,6 +793,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
786
793
  });
787
794
  await track.restartTrack();
788
795
  }
796
+ log.debug('publishing new track', {
797
+ track: pub.trackSid,
798
+ });
789
799
  await this.localParticipant.publishTrack(track, pub.options);
790
800
  }
791
801
  }),
@@ -886,7 +896,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
886
896
  participant.tracks.forEach((publication) => {
887
897
  participant.unpublishTrack(publication.trackSid, true);
888
898
  });
889
- this.emitWhenConnected(RoomEvent.ParticipantDisconnected, participant);
899
+ this.emit(RoomEvent.ParticipantDisconnected, participant);
890
900
  }
891
901
 
892
902
  // updates are sent only when there's a change to speaker ordering
@@ -1114,7 +1124,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1114
1124
  },
1115
1125
  )
1116
1126
  .on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
1117
- this.emitWhenConnected(RoomEvent.TrackUnpublished, publication, participant);
1127
+ this.emit(RoomEvent.TrackUnpublished, publication, participant);
1118
1128
  })
1119
1129
  .on(
1120
1130
  ParticipantEvent.TrackUnsubscribed,
@@ -252,7 +252,8 @@ export enum RoomEvent {
252
252
  SignalConnected = 'signalConnected',
253
253
 
254
254
  /**
255
- * Recording of a room has started/stopped.
255
+ * Recording of a room has started/stopped. Room.isRecording will be updated too.
256
+ * args: (isRecording: boolean)
256
257
  */
257
258
  RecordingStatusChanged = 'recordingStatusChanged',
258
259
  }
@@ -439,7 +439,13 @@ export default class LocalParticipant extends Participant {
439
439
  `Opus DTX will be disabled for stereo tracks by default. Enable them explicitly to make it work.`,
440
440
  );
441
441
  }
442
+ if (options.red === undefined) {
443
+ log.info(
444
+ `Opus RED will be disabled for stereo tracks by default. Enable them explicitly to make it work.`,
445
+ );
446
+ }
442
447
  options.dtx ??= false;
448
+ options.red ??= false;
443
449
  }
444
450
  const opts: TrackPublishOptions = {
445
451
  ...this.roomOptions.publishDefaults,
@@ -721,17 +727,24 @@ export default class LocalParticipant extends Participant {
721
727
  track.stop();
722
728
  }
723
729
 
730
+ let negotiationNeeded = false;
731
+ const trackSender = track.sender;
732
+ track.sender = undefined;
724
733
  if (
725
734
  this.engine.publisher &&
726
735
  this.engine.publisher.pc.connectionState !== 'closed' &&
727
- track.sender
736
+ trackSender
728
737
  ) {
729
738
  try {
730
- this.engine.removeTrack(track.sender);
739
+ if (this.engine.removeTrack(trackSender)) {
740
+ negotiationNeeded = true;
741
+ }
731
742
  if (track instanceof LocalVideoTrack) {
732
743
  for (const [, trackInfo] of track.simulcastCodecs) {
733
744
  if (trackInfo.sender) {
734
- this.engine.removeTrack(trackInfo.sender);
745
+ if (this.engine.removeTrack(trackInfo.sender)) {
746
+ negotiationNeeded = true;
747
+ }
735
748
  trackInfo.sender = undefined;
736
749
  }
737
750
  }
@@ -739,13 +752,9 @@ export default class LocalParticipant extends Participant {
739
752
  }
740
753
  } catch (e) {
741
754
  log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
742
- } finally {
743
- await this.engine.negotiate();
744
755
  }
745
756
  }
746
757
 
747
- track.sender = undefined;
748
-
749
758
  // remove from our maps
750
759
  this.tracks.delete(publication.trackSid);
751
760
  switch (publication.kind) {
@@ -762,6 +771,9 @@ export default class LocalParticipant extends Participant {
762
771
  this.emit(ParticipantEvent.LocalTrackUnpublished, publication);
763
772
  publication.setTrack(undefined);
764
773
 
774
+ if (negotiationNeeded) {
775
+ await this.engine.negotiate();
776
+ }
765
777
  return publication;
766
778
  }
767
779
 
@@ -957,7 +969,7 @@ export default class LocalParticipant extends Participant {
957
969
  }
958
970
  }
959
971
  } else if (update.subscribedQualities.length > 0) {
960
- pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
972
+ await pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
961
973
  }
962
974
  };
963
975
 
@@ -236,8 +236,7 @@ export default class RemoteParticipant extends Participant {
236
236
  }
237
237
  publication = new RemoteTrackPublication(
238
238
  kind,
239
- ti.sid,
240
- ti.name,
239
+ ti,
241
240
  this.signalClient.connectOptions?.autoSubscribe,
242
241
  );
243
242
  publication.updateInfo(ti);
@@ -246,7 +245,7 @@ export default class RemoteParticipant extends Participant {
246
245
  (publishedTrack) => publishedTrack.source === publication?.source,
247
246
  );
248
247
  if (existingTrackOfSource && publication.source !== Track.Source.Unknown) {
249
- log.warn(
248
+ log.debug(
250
249
  `received a second track publication for ${this.identity} with the same source: ${publication.source}`,
251
250
  {
252
251
  oldTrack: existingTrackOfSource,
@@ -3,7 +3,7 @@ import log from '../../logger';
3
3
  import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
4
4
  import type { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc';
5
5
  import { computeBitrate, monitorFrequency, VideoSenderStats } from '../stats';
6
- import { isFireFox, isMobile, isWeb } from '../utils';
6
+ import { isFireFox, isMobile, isWeb, Mutex } from '../utils';
7
7
  import LocalTrack from './LocalTrack';
8
8
  import type { VideoCaptureOptions, VideoCodec } from './options';
9
9
  import { Track } from './Track';
@@ -39,6 +39,12 @@ export default class LocalVideoTrack extends LocalTrack {
39
39
 
40
40
  private subscribedCodecs?: SubscribedCodec[];
41
41
 
42
+ // prevents concurrent manipulations to track sender
43
+ // if multiple get/setParameter are called concurrently, certain timing of events
44
+ // could lead to the browser throwing an exception in `setParameter`, due to
45
+ // a missing `getParameter` call.
46
+ private senderLock: Mutex;
47
+
42
48
  /**
43
49
  *
44
50
  * @param mediaTrack
@@ -51,6 +57,7 @@ export default class LocalVideoTrack extends LocalTrack {
51
57
  userProvidedTrack = true,
52
58
  ) {
53
59
  super(mediaTrack, Track.Kind.Video, constraints, userProvidedTrack);
60
+ this.senderLock = new Mutex();
54
61
  }
55
62
 
56
63
  get isSimulcast(): boolean {
@@ -257,6 +264,7 @@ export default class LocalVideoTrack extends LocalTrack {
257
264
  simulcastCodecInfo.sender,
258
265
  simulcastCodecInfo.encodings!,
259
266
  codec.qualities,
267
+ this.senderLock,
260
268
  );
261
269
  }
262
270
  }
@@ -274,7 +282,7 @@ export default class LocalVideoTrack extends LocalTrack {
274
282
  return;
275
283
  }
276
284
 
277
- await setPublishingLayersForSender(this.sender, this.encodings, qualities);
285
+ await setPublishingLayersForSender(this.sender, this.encodings, qualities, this.senderLock);
278
286
  }
279
287
 
280
288
  protected monitorSender = async () => {
@@ -317,58 +325,66 @@ async function setPublishingLayersForSender(
317
325
  sender: RTCRtpSender,
318
326
  senderEncodings: RTCRtpEncodingParameters[],
319
327
  qualities: SubscribedQuality[],
328
+ senderLock: Mutex,
320
329
  ) {
330
+ const unlock = await senderLock.lock();
321
331
  log.debug('setPublishingLayersForSender', { sender, qualities, senderEncodings });
322
- const params = sender.getParameters();
323
- const { encodings } = params;
324
- if (!encodings) {
325
- return;
326
- }
327
-
328
- if (encodings.length !== senderEncodings.length) {
329
- log.warn('cannot set publishing layers, encodings mismatch');
330
- return;
331
- }
332
-
333
- let hasChanged = false;
334
- encodings.forEach((encoding, idx) => {
335
- let rid = encoding.rid ?? '';
336
- if (rid === '') {
337
- rid = 'q';
332
+ try {
333
+ const params = sender.getParameters();
334
+ const { encodings } = params;
335
+ if (!encodings) {
336
+ return;
338
337
  }
339
- const quality = videoQualityForRid(rid);
340
- const subscribedQuality = qualities.find((q) => q.quality === quality);
341
- if (!subscribedQuality) {
338
+
339
+ if (encodings.length !== senderEncodings.length) {
340
+ log.warn('cannot set publishing layers, encodings mismatch');
342
341
  return;
343
342
  }
344
- if (encoding.active !== subscribedQuality.enabled) {
345
- hasChanged = true;
346
- encoding.active = subscribedQuality.enabled;
347
- log.debug(
348
- `setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}`,
349
- );
350
-
351
- // FireFox does not support setting encoding.active to false, so we
352
- // have a workaround of lowering its bitrate and resolution to the min.
353
- if (isFireFox()) {
354
- if (subscribedQuality.enabled) {
355
- encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
356
- encoding.maxBitrate = senderEncodings[idx].maxBitrate;
357
- /* @ts-ignore */
358
- encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
359
- } else {
360
- encoding.scaleResolutionDownBy = 4;
361
- encoding.maxBitrate = 10;
362
- /* @ts-ignore */
363
- encoding.maxFrameRate = 2;
343
+
344
+ let hasChanged = false;
345
+ encodings.forEach((encoding, idx) => {
346
+ let rid = encoding.rid ?? '';
347
+ if (rid === '') {
348
+ rid = 'q';
349
+ }
350
+ const quality = videoQualityForRid(rid);
351
+ const subscribedQuality = qualities.find((q) => q.quality === quality);
352
+ if (!subscribedQuality) {
353
+ return;
354
+ }
355
+ if (encoding.active !== subscribedQuality.enabled) {
356
+ hasChanged = true;
357
+ encoding.active = subscribedQuality.enabled;
358
+ log.debug(
359
+ `setting layer ${subscribedQuality.quality} to ${
360
+ encoding.active ? 'enabled' : 'disabled'
361
+ }`,
362
+ );
363
+
364
+ // FireFox does not support setting encoding.active to false, so we
365
+ // have a workaround of lowering its bitrate and resolution to the min.
366
+ if (isFireFox()) {
367
+ if (subscribedQuality.enabled) {
368
+ encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
369
+ encoding.maxBitrate = senderEncodings[idx].maxBitrate;
370
+ /* @ts-ignore */
371
+ encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
372
+ } else {
373
+ encoding.scaleResolutionDownBy = 4;
374
+ encoding.maxBitrate = 10;
375
+ /* @ts-ignore */
376
+ encoding.maxFrameRate = 2;
377
+ }
364
378
  }
365
379
  }
366
- }
367
- });
380
+ });
368
381
 
369
- if (hasChanged) {
370
- params.encodings = encodings;
371
- await sender.setParameters(params);
382
+ if (hasChanged) {
383
+ params.encodings = encodings;
384
+ await sender.setParameters(params);
385
+ }
386
+ } finally {
387
+ unlock();
372
388
  }
373
389
  }
374
390
 
@@ -24,9 +24,10 @@ export default class RemoteTrackPublication extends TrackPublication {
24
24
 
25
25
  protected fps?: number;
26
26
 
27
- constructor(kind: Track.Kind, id: string, name: string, autoSubscribe: boolean | undefined) {
28
- super(kind, id, name);
27
+ constructor(kind: Track.Kind, ti: TrackInfo, autoSubscribe: boolean | undefined) {
28
+ super(kind, ti.sid, ti.name);
29
29
  this.subscribed = autoSubscribe;
30
+ this.updateInfo(ti);
30
31
  }
31
32
 
32
33
  /**
@@ -31,9 +31,6 @@ export default class RemoteVideoTrack extends RemoteTrack {
31
31
  ) {
32
32
  super(mediaTrack, sid, Track.Kind.Video, receiver);
33
33
  this.adaptiveStreamSettings = adaptiveStreamSettings;
34
- if (this.isAdaptiveStream) {
35
- this.streamState = Track.StreamState.Paused;
36
- }
37
34
  }
38
35
 
39
36
  get isAdaptiveStream(): boolean {
@@ -32,7 +32,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
32
32
  mediaStream?: MediaStream;
33
33
 
34
34
  /**
35
- * indicates current state of stream
35
+ * indicates current state of stream, it'll indicate `paused` if the track
36
+ * has been paused by congestion controller
36
37
  */
37
38
  streamState: Track.StreamState = Track.StreamState.Active;
38
39
 
@@ -28,12 +28,12 @@ export interface TrackPublishDefaults {
28
28
  audioBitrate?: number;
29
29
 
30
30
  /**
31
- * dtx (Discontinuous Transmission of audio), defaults to true
31
+ * dtx (Discontinuous Transmission of audio), enabled by default for mono tracks.
32
32
  */
33
33
  dtx?: boolean;
34
34
 
35
35
  /**
36
- * red (Redundant Audio Data), defaults to true
36
+ * red (Redundant Audio Data), enabled by default for mono tracks.
37
37
  */
38
38
  red?: boolean;
39
39