@stream-io/video-client 1.4.0 → 1.4.2

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.
@@ -4999,6 +4999,11 @@ export declare const RecordSettingsRequestQualityEnum: {
4999
4999
  readonly _720P: "720p";
5000
5000
  readonly _1080P: "1080p";
5001
5001
  readonly _1440P: "1440p";
5002
+ readonly PORTRAIT_360X640: "portrait-360x640";
5003
+ readonly PORTRAIT_480X854: "portrait-480x854";
5004
+ readonly PORTRAIT_720X1280: "portrait-720x1280";
5005
+ readonly PORTRAIT_1080X1920: "portrait-1080x1920";
5006
+ readonly PORTRAIT_1440X2560: "portrait-1440x2560";
5002
5007
  };
5003
5008
  export type RecordSettingsRequestQualityEnum = (typeof RecordSettingsRequestQualityEnum)[keyof typeof RecordSettingsRequestQualityEnum];
5004
5009
  /**
@@ -12,6 +12,7 @@ export type PublisherConstructorOpts = {
12
12
  isDtxEnabled: boolean;
13
13
  isRedEnabled: boolean;
14
14
  iceRestartDelay?: number;
15
+ onUnrecoverableError?: () => void;
15
16
  };
16
17
  /**
17
18
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -35,6 +36,7 @@ export declare class Publisher {
35
36
  private readonly isDtxEnabled;
36
37
  private readonly isRedEnabled;
37
38
  private readonly unsubscribeOnIceRestart;
39
+ private readonly onUnrecoverableError?;
38
40
  private readonly iceRestartDelay;
39
41
  private isIceRestarting;
40
42
  private iceRestartTimeout?;
@@ -59,8 +61,9 @@ export declare class Publisher {
59
61
  * @param isDtxEnabled whether DTX is enabled.
60
62
  * @param isRedEnabled whether RED is enabled.
61
63
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
64
+ * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
62
65
  */
63
- constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay, }: PublisherConstructorOpts);
66
+ constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay, onUnrecoverableError, }: PublisherConstructorOpts);
64
67
  private createPeerConnection;
65
68
  /**
66
69
  * Closes the publisher PeerConnection and cleans up the resources.
@@ -7,6 +7,7 @@ export type SubscriberOpts = {
7
7
  state: CallState;
8
8
  connectionConfig?: RTCConfiguration;
9
9
  iceRestartDelay?: number;
10
+ onUnrecoverableError?: () => void;
10
11
  };
11
12
  /**
12
13
  * A wrapper around the `RTCPeerConnection` that handles the incoming
@@ -18,6 +19,7 @@ export declare class Subscriber {
18
19
  private state;
19
20
  private readonly unregisterOnSubscriberOffer;
20
21
  private readonly unregisterOnIceRestart;
22
+ private readonly onUnrecoverableError?;
21
23
  private readonly iceRestartDelay;
22
24
  private isIceRestarting;
23
25
  private iceRestartTimeout?;
@@ -36,8 +38,9 @@ export declare class Subscriber {
36
38
  * @param state the state of the call.
37
39
  * @param connectionConfig the connection configuration to use.
38
40
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
41
+ * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
39
42
  */
40
- constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay, }: SubscriberOpts);
43
+ constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay, onUnrecoverableError, }: SubscriberOpts);
41
44
  /**
42
45
  * Creates a new `RTCPeerConnection` instance with the given configuration.
43
46
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
package/src/Call.ts CHANGED
@@ -1014,6 +1014,11 @@ export class Call {
1014
1014
  dispatcher: this.dispatcher,
1015
1015
  state: this.state,
1016
1016
  connectionConfig,
1017
+ onUnrecoverableError: () => {
1018
+ reconnect('full', 'unrecoverable subscriber error').catch((err) => {
1019
+ this.logger('debug', '[Rejoin]: Rejoin failed', err);
1020
+ });
1021
+ },
1017
1022
  });
1018
1023
  }
1019
1024
 
@@ -1031,6 +1036,11 @@ export class Call {
1031
1036
  connectionConfig,
1032
1037
  isDtxEnabled,
1033
1038
  isRedEnabled,
1039
+ onUnrecoverableError: () => {
1040
+ reconnect('full', 'unrecoverable publisher error').catch((err) => {
1041
+ this.logger('debug', '[Rejoin]: Rejoin failed', err);
1042
+ });
1043
+ },
1034
1044
  });
1035
1045
  }
1036
1046
 
@@ -4985,6 +4985,11 @@ export const RecordSettingsRequestQualityEnum = {
4985
4985
  _720P: '720p',
4986
4986
  _1080P: '1080p',
4987
4987
  _1440P: '1440p',
4988
+ PORTRAIT_360X640: 'portrait-360x640',
4989
+ PORTRAIT_480X854: 'portrait-480x854',
4990
+ PORTRAIT_720X1280: 'portrait-720x1280',
4991
+ PORTRAIT_1080X1920: 'portrait-1080x1920',
4992
+ PORTRAIT_1440X2560: 'portrait-1440x2560',
4988
4993
  } as const;
4989
4994
  export type RecordSettingsRequestQualityEnum =
4990
4995
  (typeof RecordSettingsRequestQualityEnum)[keyof typeof RecordSettingsRequestQualityEnum];
@@ -36,6 +36,7 @@ export type PublisherConstructorOpts = {
36
36
  isDtxEnabled: boolean;
37
37
  isRedEnabled: boolean;
38
38
  iceRestartDelay?: number;
39
+ onUnrecoverableError?: () => void;
39
40
  };
40
41
 
41
42
  /**
@@ -94,6 +95,7 @@ export class Publisher {
94
95
  private readonly isRedEnabled: boolean;
95
96
 
96
97
  private readonly unsubscribeOnIceRestart: () => void;
98
+ private readonly onUnrecoverableError?: () => void;
97
99
 
98
100
  private readonly iceRestartDelay: number;
99
101
  private isIceRestarting = false;
@@ -127,6 +129,7 @@ export class Publisher {
127
129
  * @param isDtxEnabled whether DTX is enabled.
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.
132
+ * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
130
133
  */
131
134
  constructor({
132
135
  connectionConfig,
@@ -136,6 +139,7 @@ export class Publisher {
136
139
  isDtxEnabled,
137
140
  isRedEnabled,
138
141
  iceRestartDelay = 2500,
142
+ onUnrecoverableError,
139
143
  }: PublisherConstructorOpts) {
140
144
  this.pc = this.createPeerConnection(connectionConfig);
141
145
  this.sfuClient = sfuClient;
@@ -143,11 +147,13 @@ export class Publisher {
143
147
  this.isDtxEnabled = isDtxEnabled;
144
148
  this.isRedEnabled = isRedEnabled;
145
149
  this.iceRestartDelay = iceRestartDelay;
150
+ this.onUnrecoverableError = onUnrecoverableError;
146
151
 
147
152
  this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
148
153
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
149
154
  this.restartIce().catch((err) => {
150
155
  logger('warn', `ICERestart failed`, err);
156
+ this.onUnrecoverableError?.();
151
157
  });
152
158
  });
153
159
  }
@@ -813,14 +819,15 @@ export class Publisher {
813
819
  this.state.callingState !== CallingState.OFFLINE;
814
820
 
815
821
  if (state === 'failed') {
816
- logger('warn', `Attempting to restart ICE`);
822
+ logger('debug', `Attempting to restart ICE`);
817
823
  this.restartIce().catch((e) => {
818
824
  logger('error', `ICE restart error`, e);
825
+ this.onUnrecoverableError?.();
819
826
  });
820
827
  } else if (state === 'disconnected' && hasNetworkConnection) {
821
828
  // when in `disconnected` state, the browser may recover automatically,
822
829
  // hence, we delay the ICE restart
823
- logger('warn', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
830
+ logger('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
824
831
  this.iceRestartTimeout = setTimeout(() => {
825
832
  // check if the state is still `disconnected` or `failed`
826
833
  // as the connection may have recovered (or failed) in the meantime
@@ -830,6 +837,7 @@ export class Publisher {
830
837
  ) {
831
838
  this.restartIce().catch((e) => {
832
839
  logger('error', `ICE restart error`, e);
840
+ this.onUnrecoverableError?.();
833
841
  });
834
842
  } else {
835
843
  logger(
@@ -12,6 +12,7 @@ export type SubscriberOpts = {
12
12
  state: CallState;
13
13
  connectionConfig?: RTCConfiguration;
14
14
  iceRestartDelay?: number;
15
+ onUnrecoverableError?: () => void;
15
16
  };
16
17
 
17
18
  const logger = getLogger(['Subscriber']);
@@ -27,6 +28,7 @@ export class Subscriber {
27
28
 
28
29
  private readonly unregisterOnSubscriberOffer: () => void;
29
30
  private readonly unregisterOnIceRestart: () => void;
31
+ private readonly onUnrecoverableError?: () => void;
30
32
 
31
33
  private readonly iceRestartDelay: number;
32
34
  private isIceRestarting = false;
@@ -53,6 +55,7 @@ export class Subscriber {
53
55
  * @param state the state of the call.
54
56
  * @param connectionConfig the connection configuration to use.
55
57
  * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
58
+ * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
56
59
  */
57
60
  constructor({
58
61
  sfuClient,
@@ -60,10 +63,12 @@ export class Subscriber {
60
63
  state,
61
64
  connectionConfig,
62
65
  iceRestartDelay = 2500,
66
+ onUnrecoverableError,
63
67
  }: SubscriberOpts) {
64
68
  this.sfuClient = sfuClient;
65
69
  this.state = state;
66
70
  this.iceRestartDelay = iceRestartDelay;
71
+ this.onUnrecoverableError = onUnrecoverableError;
67
72
 
68
73
  this.pc = this.createPeerConnection(connectionConfig);
69
74
 
@@ -80,6 +85,7 @@ export class Subscriber {
80
85
  if (iceRestart.peerType !== PeerType.SUBSCRIBER) return;
81
86
  this.restartIce().catch((err) => {
82
87
  logger('warn', `ICERestart failed`, err);
88
+ this.onUnrecoverableError?.();
83
89
  });
84
90
  });
85
91
  }
@@ -223,6 +229,13 @@ export class Subscriber {
223
229
  logger('debug', 'ICE restart is already in progress');
224
230
  return;
225
231
  }
232
+ if (this.pc.connectionState === 'new') {
233
+ logger(
234
+ 'debug',
235
+ `ICE connection is not yet established, skipping restart.`,
236
+ );
237
+ return;
238
+ }
226
239
  const previousIsIceRestarting = this.isIceRestarting;
227
240
  try {
228
241
  this.isIceRestarting = true;
@@ -251,32 +264,24 @@ export class Subscriber {
251
264
  );
252
265
  if (!participantToUpdate) {
253
266
  logger(
254
- 'error',
267
+ 'warn',
255
268
  `[onTrack]: Received track for unknown participant: ${trackId}`,
256
269
  e,
257
270
  );
258
271
  return;
259
272
  }
260
273
 
274
+ const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
261
275
  e.track.addEventListener('mute', () => {
262
- logger(
263
- 'info',
264
- `[onTrack]: Track muted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
265
- );
276
+ logger('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
266
277
  });
267
278
 
268
279
  e.track.addEventListener('unmute', () => {
269
- logger(
270
- 'info',
271
- `[onTrack]: Track unmuted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
272
- );
280
+ logger('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
273
281
  });
274
282
 
275
283
  e.track.addEventListener('ended', () => {
276
- logger(
277
- 'info',
278
- `[onTrack]: Track ended: ${participantToUpdate.userId} ${trackType}:${trackId}`,
279
- );
284
+ logger('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
280
285
  });
281
286
 
282
287
  const streamKindProp = (
@@ -366,14 +371,15 @@ export class Subscriber {
366
371
  this.state.callingState !== CallingState.OFFLINE;
367
372
 
368
373
  if (state === 'failed') {
369
- logger('warn', `Attempting to restart ICE`);
374
+ logger('debug', `Attempting to restart ICE`);
370
375
  this.restartIce().catch((e) => {
371
376
  logger('error', `ICE restart failed`, e);
377
+ this.onUnrecoverableError?.();
372
378
  });
373
379
  } else if (state === 'disconnected' && hasNetworkConnection) {
374
380
  // when in `disconnected` state, the browser may recover automatically,
375
381
  // hence, we delay the ICE restart
376
- logger('warn', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
382
+ logger('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
377
383
  this.iceRestartTimeout = setTimeout(() => {
378
384
  // check if the state is still `disconnected` or `failed`
379
385
  // as the connection may have recovered (or failed) in the meantime
@@ -383,6 +389,7 @@ export class Subscriber {
383
389
  ) {
384
390
  this.restartIce().catch((e) => {
385
391
  logger('error', `ICE restart failed`, e);
392
+ this.onUnrecoverableError?.();
386
393
  });
387
394
  } else {
388
395
  logger(
@@ -390,7 +397,7 @@ export class Subscriber {
390
397
  `Scheduled ICE restart: connection recovered, canceled.`,
391
398
  );
392
399
  }
393
- }, 5000);
400
+ }, this.iceRestartDelay);
394
401
  }
395
402
  };
396
403
 
@@ -168,6 +168,15 @@ describe('Subscriber', () => {
168
168
  expect(sfuClient.iceRestart).not.toHaveBeenCalled();
169
169
  });
170
170
 
171
+ it('should skip ICE restart when connection is still new', async () => {
172
+ sfuClient.iceRestart = vi.fn();
173
+ // @ts-ignore
174
+ subscriber['pc'].connectionState = 'new';
175
+
176
+ await subscriber.restartIce();
177
+ expect(sfuClient.iceRestart).not.toHaveBeenCalled();
178
+ });
179
+
171
180
  it(`should perform ICE restart when connection state changes to 'failed'`, () => {
172
181
  vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
173
182
  // @ts-ignore