livekit-client 1.6.5 → 1.6.7

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 (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