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