@stream-io/video-client 1.5.0 → 1.6.0-0

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 (74) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/dist/index.browser.es.js +1986 -1482
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1983 -1478
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1986 -1482
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +93 -9
  9. package/dist/src/StreamSfuClient.d.ts +73 -56
  10. package/dist/src/StreamVideoClient.d.ts +2 -2
  11. package/dist/src/coordinator/connection/client.d.ts +3 -4
  12. package/dist/src/coordinator/connection/types.d.ts +5 -1
  13. package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
  14. package/dist/src/devices/MicrophoneManager.d.ts +1 -1
  15. package/dist/src/events/callEventHandlers.d.ts +1 -3
  16. package/dist/src/events/internal.d.ts +4 -0
  17. package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
  18. package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
  19. package/dist/src/helpers/ensureExhausted.d.ts +1 -0
  20. package/dist/src/helpers/withResolvers.d.ts +14 -0
  21. package/dist/src/logger.d.ts +1 -0
  22. package/dist/src/rpc/createClient.d.ts +2 -0
  23. package/dist/src/rpc/index.d.ts +1 -0
  24. package/dist/src/rpc/retryable.d.ts +23 -0
  25. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  26. package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
  27. package/dist/src/rtc/Publisher.d.ts +24 -25
  28. package/dist/src/rtc/Subscriber.d.ts +12 -11
  29. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -0
  30. package/dist/src/rtc/helpers/tracks.d.ts +3 -3
  31. package/dist/src/rtc/signal.d.ts +1 -1
  32. package/dist/src/store/CallState.d.ts +46 -2
  33. package/package.json +3 -3
  34. package/src/Call.ts +628 -566
  35. package/src/StreamSfuClient.ts +276 -246
  36. package/src/StreamVideoClient.ts +15 -16
  37. package/src/coordinator/connection/client.ts +25 -8
  38. package/src/coordinator/connection/connection.ts +1 -0
  39. package/src/coordinator/connection/types.ts +6 -0
  40. package/src/devices/CameraManager.ts +1 -1
  41. package/src/devices/InputMediaDeviceManager.ts +12 -3
  42. package/src/devices/MicrophoneManager.ts +3 -3
  43. package/src/devices/devices.ts +1 -1
  44. package/src/events/__tests__/mutes.test.ts +10 -13
  45. package/src/events/__tests__/participant.test.ts +75 -0
  46. package/src/events/callEventHandlers.ts +4 -7
  47. package/src/events/internal.ts +20 -3
  48. package/src/events/mutes.ts +5 -3
  49. package/src/events/participant.ts +48 -15
  50. package/src/gen/video/sfu/event/events.ts +451 -8
  51. package/src/gen/video/sfu/models/models.ts +211 -204
  52. package/src/helpers/ensureExhausted.ts +5 -0
  53. package/src/helpers/withResolvers.ts +43 -0
  54. package/src/logger.ts +3 -1
  55. package/src/rpc/__tests__/retryable.test.ts +72 -0
  56. package/src/rpc/createClient.ts +21 -0
  57. package/src/rpc/index.ts +1 -0
  58. package/src/rpc/retryable.ts +57 -0
  59. package/src/rtc/Dispatcher.ts +6 -2
  60. package/src/rtc/IceTrickleBuffer.ts +2 -2
  61. package/src/rtc/Publisher.ts +127 -163
  62. package/src/rtc/Subscriber.ts +92 -155
  63. package/src/rtc/__tests__/Publisher.test.ts +18 -95
  64. package/src/rtc/__tests__/Subscriber.test.ts +63 -99
  65. package/src/rtc/__tests__/videoLayers.test.ts +2 -2
  66. package/src/rtc/helpers/rtcConfiguration.ts +11 -0
  67. package/src/rtc/helpers/tracks.ts +27 -7
  68. package/src/rtc/signal.ts +3 -3
  69. package/src/rtc/videoLayers.ts +1 -10
  70. package/src/stats/SfuStatsReporter.ts +1 -0
  71. package/src/store/CallState.ts +109 -2
  72. package/src/store/__tests__/CallState.test.ts +48 -37
  73. package/dist/src/rtc/flows/join.d.ts +0 -20
  74. package/src/rtc/flows/join.ts +0 -65
@@ -26,8 +26,6 @@ import { getOSInfo } from '../client-details';
26
26
  import { VideoLayerSetting } from '../gen/video/sfu/event/events';
27
27
  import { TargetResolutionResponse } from '../gen/shims';
28
28
 
29
- const logger: Logger = getLogger(['Publisher']);
30
-
31
29
  export type PublisherConstructorOpts = {
32
30
  sfuClient: StreamSfuClient;
33
31
  state: CallState;
@@ -35,15 +33,17 @@ export type PublisherConstructorOpts = {
35
33
  connectionConfig?: RTCConfiguration;
36
34
  isDtxEnabled: boolean;
37
35
  isRedEnabled: boolean;
38
- iceRestartDelay?: number;
39
36
  onUnrecoverableError?: () => void;
37
+ logTag: string;
40
38
  };
41
39
 
42
40
  /**
43
41
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
42
+ *
44
43
  * @internal
45
44
  */
46
45
  export class Publisher {
46
+ private readonly logger: Logger;
47
47
  private pc: RTCPeerConnection;
48
48
  private readonly state: CallState;
49
49
 
@@ -67,9 +67,9 @@ export class Publisher {
67
67
  * This is needed because some browsers (Firefox) don't reliably report
68
68
  * trackId and `mid` parameters.
69
69
  *
70
- * @private
70
+ * @internal
71
71
  */
72
- private transceiverInitOrder: TrackType[] = [];
72
+ private readonly transceiverInitOrder: TrackType[] = [];
73
73
 
74
74
  private readonly trackKindMapping: {
75
75
  [key in TrackType]: 'video' | 'audio' | undefined;
@@ -97,9 +97,7 @@ export class Publisher {
97
97
  private readonly unsubscribeOnIceRestart: () => void;
98
98
  private readonly onUnrecoverableError?: () => void;
99
99
 
100
- private readonly iceRestartDelay: number;
101
100
  private isIceRestarting = false;
102
- private iceRestartTimeout?: NodeJS.Timeout;
103
101
 
104
102
  // workaround for the lack of RTCPeerConnection.getConfiguration() method in react-native-webrtc
105
103
  private _connectionConfiguration: RTCConfiguration | undefined;
@@ -130,6 +128,7 @@ export class Publisher {
130
128
  * @param isRedEnabled whether RED is enabled.
131
129
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
132
130
  * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
131
+ * @param logTag the log tag to use.
133
132
  */
134
133
  constructor({
135
134
  connectionConfig,
@@ -138,21 +137,21 @@ export class Publisher {
138
137
  state,
139
138
  isDtxEnabled,
140
139
  isRedEnabled,
141
- iceRestartDelay = 2500,
142
140
  onUnrecoverableError,
141
+ logTag,
143
142
  }: PublisherConstructorOpts) {
143
+ this.logger = getLogger(['Publisher', logTag]);
144
144
  this.pc = this.createPeerConnection(connectionConfig);
145
145
  this.sfuClient = sfuClient;
146
146
  this.state = state;
147
147
  this.isDtxEnabled = isDtxEnabled;
148
148
  this.isRedEnabled = isRedEnabled;
149
- this.iceRestartDelay = iceRestartDelay;
150
149
  this.onUnrecoverableError = onUnrecoverableError;
151
150
 
152
151
  this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
153
152
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
154
153
  this.restartIce().catch((err) => {
155
- logger('warn', `ICERestart failed`, err);
154
+ this.logger('warn', `ICERestart failed`, err);
156
155
  this.onUnrecoverableError?.();
157
156
  });
158
157
  });
@@ -180,7 +179,7 @@ export class Publisher {
180
179
  /**
181
180
  * Closes the publisher PeerConnection and cleans up the resources.
182
181
  */
183
- close = ({ stopTracks = true } = {}) => {
182
+ close = ({ stopTracks }: { stopTracks: boolean }) => {
184
183
  if (stopTracks) {
185
184
  this.stopPublishing();
186
185
  Object.keys(this.transceiverRegistry).forEach((trackType) => {
@@ -193,10 +192,33 @@ export class Publisher {
193
192
  });
194
193
  }
195
194
 
196
- clearTimeout(this.iceRestartTimeout);
195
+ this.detachEventHandlers();
196
+ this.pc.close();
197
+ };
198
+
199
+ /**
200
+ * Detaches the event handlers from the `RTCPeerConnection`.
201
+ * This is useful when we want to replace the `RTCPeerConnection`
202
+ * instance with a new one (in case of migration).
203
+ */
204
+ detachEventHandlers = () => {
197
205
  this.unsubscribeOnIceRestart();
206
+
207
+ this.pc.removeEventListener('icecandidate', this.onIceCandidate);
198
208
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
199
- this.pc.close();
209
+ this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
210
+ this.pc.removeEventListener(
211
+ 'iceconnectionstatechange',
212
+ this.onIceConnectionStateChange,
213
+ );
214
+ this.pc.removeEventListener(
215
+ 'icegatheringstatechange',
216
+ this.onIceGatheringStateChange,
217
+ );
218
+ this.pc.removeEventListener(
219
+ 'signalingstatechange',
220
+ this.onSignalingStateChange,
221
+ );
200
222
  };
201
223
 
202
224
  /**
@@ -234,7 +256,7 @@ export class Publisher {
234
256
  * Once the track has ended, it will notify the SFU and update the state.
235
257
  */
236
258
  const handleTrackEnded = async () => {
237
- logger(
259
+ this.logger(
238
260
  'info',
239
261
  `Track ${TrackType[trackType]} has ended, notifying the SFU`,
240
262
  );
@@ -262,23 +284,17 @@ export class Publisher {
262
284
  : undefined;
263
285
 
264
286
  let preferredCodec = opts.preferredCodec;
265
- if (!preferredCodec && trackType === TrackType.VIDEO) {
266
- if (isReactNative()) {
267
- const osName = getOSInfo()?.name.toLowerCase();
268
- if (osName === 'ipados') {
269
- // in ipads it was noticed that if vp8 codec is used
270
- // then the bytes sent is 0 in the outbound-rtp
271
- // so we are forcing h264 codec for ipads
272
- preferredCodec = 'H264';
273
- } else if (osName === 'android') {
274
- preferredCodec = 'VP8';
275
- }
287
+ if (!preferredCodec && trackType === TrackType.VIDEO && isReactNative()) {
288
+ const osName = getOSInfo()?.name.toLowerCase();
289
+ if (osName === 'ipados') {
290
+ // in ipads it was noticed that if vp8 codec is used
291
+ // then the bytes sent is 0 in the outbound-rtp
292
+ // so we are forcing h264 codec for ipads
293
+ preferredCodec = 'H264';
294
+ } else if (osName === 'android') {
295
+ preferredCodec = 'VP8';
276
296
  }
277
297
  }
278
- const codecPreferences = this.getCodecPreferences(
279
- trackType,
280
- preferredCodec,
281
- );
282
298
 
283
299
  // listen for 'ended' event on the track as it might be ended abruptly
284
300
  // by an external factor as permission revokes, device disconnected, etc.
@@ -297,13 +313,17 @@ export class Publisher {
297
313
  sendEncodings: videoEncodings,
298
314
  });
299
315
 
300
- logger('debug', `Added ${TrackType[trackType]} transceiver`);
316
+ this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
301
317
  this.transceiverInitOrder.push(trackType);
302
318
  this.transceiverRegistry[trackType] = transceiver;
303
319
  this.publishOptionsPerTrackType.set(trackType, opts);
304
320
 
305
- if ('setCodecPreferences' in transceiver && codecPreferences) {
306
- logger(
321
+ const codecPreferences =
322
+ 'setCodecPreferences' in transceiver
323
+ ? this.getCodecPreferences(trackType, preferredCodec)
324
+ : undefined;
325
+ if (codecPreferences) {
326
+ this.logger(
307
327
  'info',
308
328
  `Setting ${TrackType[trackType]} codec preferences`,
309
329
  codecPreferences,
@@ -311,7 +331,7 @@ export class Publisher {
311
331
  try {
312
332
  transceiver.setCodecPreferences(codecPreferences);
313
333
  } catch (err) {
314
- logger('warn', `Couldn't set codec preferences`, err);
334
+ this.logger('warn', `Couldn't set codec preferences`, err);
315
335
  }
316
336
  }
317
337
  } else {
@@ -364,30 +384,10 @@ export class Publisher {
364
384
  * @param trackType the track type to check.
365
385
  */
366
386
  isPublishing = (trackType: TrackType): boolean => {
367
- const transceiverForTrackType = this.transceiverRegistry[trackType];
368
- if (transceiverForTrackType && transceiverForTrackType.sender) {
369
- const sender = transceiverForTrackType.sender;
370
- return (
371
- !!sender.track &&
372
- sender.track.readyState === 'live' &&
373
- sender.track.enabled
374
- );
375
- }
376
- return false;
377
- };
378
-
379
- /**
380
- * Returns true if the given track type is currently live
381
- *
382
- * @param trackType the track type to check.
383
- */
384
- isLive = (trackType: TrackType): boolean => {
385
- const transceiverForTrackType = this.transceiverRegistry[trackType];
386
- if (transceiverForTrackType && transceiverForTrackType.sender) {
387
- const sender = transceiverForTrackType.sender;
388
- return !!sender.track && sender.track.readyState === 'live';
389
- }
390
- return false;
387
+ const transceiver = this.transceiverRegistry[trackType];
388
+ if (!transceiver || !transceiver.sender) return false;
389
+ const track = transceiver.sender.track;
390
+ return !!track && track.readyState === 'live' && track.enabled;
391
391
  };
392
392
 
393
393
  private notifyTrackMuteStateChanged = async (
@@ -399,6 +399,7 @@ export class Publisher {
399
399
 
400
400
  const audioOrVideoOrScreenShareStream =
401
401
  trackTypeToParticipantStreamKey(trackType);
402
+ if (!audioOrVideoOrScreenShareStream) return;
402
403
  if (isMuted) {
403
404
  this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
404
405
  publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
@@ -419,8 +420,8 @@ export class Publisher {
419
420
  /**
420
421
  * Stops publishing all tracks and stop all tracks.
421
422
  */
422
- stopPublishing = () => {
423
- logger('debug', 'Stopping publishing all tracks');
423
+ private stopPublishing = () => {
424
+ this.logger('debug', 'Stopping publishing all tracks');
424
425
  this.pc.getSenders().forEach((s) => {
425
426
  s.track?.stop();
426
427
  if (this.pc.signalingState !== 'closed') {
@@ -430,7 +431,7 @@ export class Publisher {
430
431
  };
431
432
 
432
433
  updateVideoPublishQuality = async (enabledLayers: VideoLayerSetting[]) => {
433
- logger(
434
+ this.logger(
434
435
  'info',
435
436
  'Update publish quality, requested layers by SFU:',
436
437
  enabledLayers,
@@ -438,13 +439,13 @@ export class Publisher {
438
439
 
439
440
  const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
440
441
  if (!videoSender) {
441
- logger('warn', 'Update publish quality, no video sender found.');
442
+ this.logger('warn', 'Update publish quality, no video sender found.');
442
443
  return;
443
444
  }
444
445
 
445
446
  const params = videoSender.getParameters();
446
447
  if (params.encodings.length === 0) {
447
- logger(
448
+ this.logger(
448
449
  'warn',
449
450
  'Update publish quality, No suitable video encoding quality found',
450
451
  );
@@ -469,7 +470,7 @@ export class Publisher {
469
470
  layer.scaleResolutionDownBy >= 1 &&
470
471
  layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy
471
472
  ) {
472
- logger(
473
+ this.logger(
473
474
  'debug',
474
475
  '[dynascale]: setting scaleResolutionDownBy from server',
475
476
  'layer',
@@ -482,7 +483,7 @@ export class Publisher {
482
483
  }
483
484
 
484
485
  if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
485
- logger(
486
+ this.logger(
486
487
  'debug',
487
488
  '[dynascale] setting max-bitrate from the server',
488
489
  'layer',
@@ -498,7 +499,7 @@ export class Publisher {
498
499
  layer.maxFramerate > 0 &&
499
500
  layer.maxFramerate !== enc.maxFramerate
500
501
  ) {
501
- logger(
502
+ this.logger(
502
503
  'debug',
503
504
  '[dynascale]: setting maxFramerate from server',
504
505
  'layer',
@@ -516,9 +517,13 @@ export class Publisher {
516
517
  const activeLayers = params.encodings.filter((e) => e.active);
517
518
  if (changed) {
518
519
  await videoSender.setParameters(params);
519
- logger('info', `Update publish quality, enabled rids: `, activeLayers);
520
+ this.logger(
521
+ 'info',
522
+ `Update publish quality, enabled rids: `,
523
+ activeLayers,
524
+ );
520
525
  } else {
521
- logger('info', `Update publish quality, no change: `, activeLayers);
526
+ this.logger('info', `Update publish quality, no change: `, activeLayers);
522
527
  }
523
528
  };
524
529
 
@@ -552,7 +557,7 @@ export class Publisher {
552
557
  private onIceCandidate = (e: RTCPeerConnectionIceEvent) => {
553
558
  const { candidate } = e;
554
559
  if (!candidate) {
555
- logger('debug', 'null ice candidate');
560
+ this.logger('debug', 'null ice candidate');
556
561
  return;
557
562
  }
558
563
  this.sfuClient
@@ -561,7 +566,7 @@ export class Publisher {
561
566
  peerType: PeerType.PUBLISHER_UNSPECIFIED,
562
567
  })
563
568
  .catch((err) => {
564
- logger('warn', `ICETrickle failed`, err);
569
+ this.logger('warn', `ICETrickle failed`, err);
565
570
  });
566
571
  };
567
572
 
@@ -574,44 +579,24 @@ export class Publisher {
574
579
  this.sfuClient = sfuClient;
575
580
  };
576
581
 
577
- /**
578
- * Performs a migration of this publisher instance to a new SFU.
579
- *
580
- * Initiates a new `iceRestart` offer/answer exchange with the new SFU.
581
- *
582
- * @param sfuClient the new SFU client to migrate to.
583
- * @param connectionConfig the new connection configuration to use.
584
- */
585
- migrateTo = async (
586
- sfuClient: StreamSfuClient,
587
- connectionConfig?: RTCConfiguration,
588
- ) => {
589
- this.sfuClient = sfuClient;
590
- this.pc.setConfiguration(connectionConfig);
591
- this._connectionConfiguration = connectionConfig;
592
-
593
- const shouldRestartIce = this.pc.iceConnectionState === 'connected';
594
- if (shouldRestartIce) {
595
- // negotiate only if there are tracks to publish
596
- await this.negotiate({ iceRestart: true });
597
- }
598
- };
599
-
600
582
  /**
601
583
  * Restarts the ICE connection and renegotiates with the SFU.
602
584
  */
603
585
  restartIce = async () => {
604
- logger('debug', 'Restarting ICE connection');
586
+ this.logger('debug', 'Restarting ICE connection');
605
587
  const signalingState = this.pc.signalingState;
606
588
  if (this.isIceRestarting || signalingState === 'have-local-offer') {
607
- logger('debug', 'ICE restart is already in progress');
589
+ this.logger('debug', 'ICE restart is already in progress');
608
590
  return;
609
591
  }
610
592
  await this.negotiate({ iceRestart: true });
611
593
  };
612
594
 
613
595
  private onNegotiationNeeded = () => {
614
- this.negotiate().catch((err) => logger('warn', `Negotiation failed.`, err));
596
+ this.negotiate().catch((err) => {
597
+ this.logger('warn', `Negotiation failed.`, err);
598
+ this.onUnrecoverableError?.();
599
+ });
615
600
  };
616
601
 
617
602
  /**
@@ -625,28 +610,15 @@ export class Publisher {
625
610
  const offer = await this.pc.createOffer(options);
626
611
  let sdp = this.mungeCodecs(offer.sdp);
627
612
  if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
628
- const transceiver =
629
- this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
630
- if (transceiver && transceiver.sender.track) {
631
- const mid =
632
- transceiver.mid ??
633
- this.extractMid(
634
- sdp,
635
- transceiver.sender.track,
636
- TrackType.SCREEN_SHARE_AUDIO,
637
- );
638
- sdp = enableHighQualityAudio(sdp, mid);
639
- }
613
+ sdp = this.enableHighQualityAudio(sdp);
640
614
  }
641
615
 
642
616
  // set the munged SDP back to the offer
643
617
  offer.sdp = sdp;
644
618
 
645
- const trackInfos = this.getCurrentTrackInfos(offer.sdp);
619
+ const trackInfos = this.getAnnouncedTracks(offer.sdp);
646
620
  if (trackInfos.length === 0) {
647
- throw new Error(
648
- `Can't initiate negotiation without announcing any tracks`,
649
- );
621
+ throw new Error(`Can't negotiate without announcing any tracks`);
650
622
  }
651
623
 
652
624
  await this.pc.setLocalDescription(offer);
@@ -656,32 +628,36 @@ export class Publisher {
656
628
  tracks: trackInfos,
657
629
  });
658
630
 
631
+ const { sdp: remoteSdp, error } = response;
659
632
  try {
660
- await this.pc.setRemoteDescription({
661
- type: 'answer',
662
- sdp: response.sdp,
663
- });
633
+ await this.pc.setRemoteDescription({ type: 'answer', sdp: remoteSdp });
664
634
  } catch (e) {
665
- logger('error', `setRemoteDescription error`, {
666
- sdp: response.sdp,
667
- error: e,
668
- });
635
+ this.logger('error', `setRemoteDescription error`, remoteSdp, error, e);
636
+ throw e;
637
+ } finally {
638
+ this.isIceRestarting = false;
669
639
  }
670
640
 
671
- this.isIceRestarting = false;
672
-
673
641
  this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(
674
642
  async (candidate) => {
675
643
  try {
676
644
  const iceCandidate = JSON.parse(candidate.iceCandidate);
677
645
  await this.pc.addIceCandidate(iceCandidate);
678
646
  } catch (e) {
679
- logger('warn', `ICE candidate error`, [e, candidate]);
647
+ this.logger('warn', `ICE candidate error`, e, candidate);
680
648
  }
681
649
  },
682
650
  );
683
651
  };
684
652
 
653
+ private enableHighQualityAudio = (sdp: string) => {
654
+ const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
655
+ if (!transceiver) return sdp;
656
+
657
+ const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
658
+ return enableHighQualityAudio(sdp, mid);
659
+ };
660
+
685
661
  private mungeCodecs = (sdp?: string) => {
686
662
  if (sdp) {
687
663
  sdp = toggleDtx(sdp, this.isDtxEnabled);
@@ -690,20 +666,23 @@ export class Publisher {
690
666
  };
691
667
 
692
668
  private extractMid = (
669
+ transceiver: RTCRtpTransceiver,
693
670
  sdp: string | undefined,
694
- track: MediaStreamTrack,
695
671
  trackType: TrackType,
696
672
  ): string => {
673
+ if (transceiver.mid) return transceiver.mid;
674
+
697
675
  if (!sdp) {
698
- logger('warn', 'No SDP found. Returning empty mid');
676
+ this.logger('warn', 'No SDP found. Returning empty mid');
699
677
  return '';
700
678
  }
701
679
 
702
- logger(
680
+ this.logger(
703
681
  'debug',
704
682
  `No 'mid' found for track. Trying to find it from the Offer SDP`,
705
683
  );
706
684
 
685
+ const track = transceiver.sender.track!;
707
686
  const parsedSdp = SDP.parse(sdp);
708
687
  const media = parsedSdp.media.find((m) => {
709
688
  return (
@@ -713,9 +692,9 @@ export class Publisher {
713
692
  );
714
693
  });
715
694
  if (typeof media?.mid === 'undefined') {
716
- logger(
695
+ this.logger(
717
696
  'debug',
718
- `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find a heuristic mid`,
697
+ `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`,
719
698
  );
720
699
 
721
700
  const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
@@ -723,13 +702,19 @@ export class Publisher {
723
702
  return String(heuristicMid);
724
703
  }
725
704
 
726
- logger('debug', 'No heuristic mid found. Returning empty mid');
705
+ this.logger('debug', 'No heuristic mid found. Returning empty mid');
727
706
  return '';
728
707
  }
729
708
  return String(media.mid);
730
709
  };
731
710
 
732
- getCurrentTrackInfos = (sdp?: string) => {
711
+ /**
712
+ * Returns a list of tracks that are currently being published.
713
+ *
714
+ * @internal
715
+ * @param sdp an optional SDP to extract the `mid` from.
716
+ */
717
+ getAnnouncedTracks = (sdp?: string): TrackInfo[] => {
733
718
  sdp = sdp || this.pc.localDescription?.sdp;
734
719
 
735
720
  const { settings } = this.state;
@@ -747,7 +732,8 @@ export class Publisher {
747
732
  );
748
733
  const track = transceiver.sender.track!;
749
734
  let optimalLayers: OptimalVideoLayer[];
750
- if (track.readyState === 'live') {
735
+ const isTrackLive = track.readyState === 'live';
736
+ if (isTrackLive) {
751
737
  const publishOpts = this.publishOptionsPerTrackType.get(trackType);
752
738
  optimalLayers =
753
739
  trackType === TrackType.VIDEO
@@ -762,7 +748,7 @@ export class Publisher {
762
748
  } else {
763
749
  // we report the last known optimal layers for ended tracks
764
750
  optimalLayers = this.trackLayersCache[trackType] || [];
765
- logger(
751
+ this.logger(
766
752
  'debug',
767
753
  `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`,
768
754
  optimalLayers,
@@ -792,11 +778,12 @@ export class Publisher {
792
778
  trackId: track.id,
793
779
  layers: layers,
794
780
  trackType,
795
- mid: transceiver.mid ?? this.extractMid(sdp, track, trackType),
781
+ mid: this.extractMid(transceiver, sdp, trackType),
796
782
 
797
783
  stereo: isStereo,
798
784
  dtx: isAudioTrack && this.isDtxEnabled,
799
785
  red: isAudioTrack && this.isRedEnabled,
786
+ muted: !isTrackLive,
800
787
  };
801
788
  });
802
789
  };
@@ -808,53 +795,30 @@ export class Publisher {
808
795
  const iceState = this.pc.iceConnectionState;
809
796
  const logLevel =
810
797
  iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
811
- logger(logLevel, `ICE Candidate error`, errorMessage);
798
+ this.logger(logLevel, `ICE Candidate error`, errorMessage);
812
799
  };
813
800
 
814
801
  private onIceConnectionStateChange = () => {
815
802
  const state = this.pc.iceConnectionState;
816
- logger('debug', `ICE Connection state changed to`, state);
803
+ this.logger('debug', `ICE Connection state changed to`, state);
817
804
 
818
- const hasNetworkConnection =
819
- this.state.callingState !== CallingState.OFFLINE;
805
+ if (this.state.callingState === CallingState.RECONNECTING) return;
820
806
 
821
- if (state === 'failed') {
822
- logger('debug', `Attempting to restart ICE`);
807
+ if (state === 'failed' || state === 'disconnected') {
808
+ this.logger('debug', `Attempting to restart ICE`);
823
809
  this.restartIce().catch((e) => {
824
- logger('error', `ICE restart error`, e);
810
+ this.logger('error', `ICE restart error`, e);
825
811
  this.onUnrecoverableError?.();
826
812
  });
827
- } else if (state === 'disconnected' && hasNetworkConnection) {
828
- // when in `disconnected` state, the browser may recover automatically,
829
- // hence, we delay the ICE restart
830
- logger('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
831
- this.iceRestartTimeout = setTimeout(() => {
832
- // check if the state is still `disconnected` or `failed`
833
- // as the connection may have recovered (or failed) in the meantime
834
- if (
835
- this.pc.iceConnectionState === 'disconnected' ||
836
- this.pc.iceConnectionState === 'failed'
837
- ) {
838
- this.restartIce().catch((e) => {
839
- logger('error', `ICE restart error`, e);
840
- this.onUnrecoverableError?.();
841
- });
842
- } else {
843
- logger(
844
- 'debug',
845
- `Scheduled ICE restart: connection recovered, canceled.`,
846
- );
847
- }
848
- }, this.iceRestartDelay);
849
813
  }
850
814
  };
851
815
 
852
816
  private onIceGatheringStateChange = () => {
853
- logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
817
+ this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
854
818
  };
855
819
 
856
820
  private onSignalingStateChange = () => {
857
- logger('debug', `Signaling state changed`, this.pc.signalingState);
821
+ this.logger('debug', `Signaling state changed`, this.pc.signalingState);
858
822
  };
859
823
 
860
824
  private ridToVideoQuality = (rid: string): VideoQuality => {