@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +42 -12
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +42 -12
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +42 -12
- package/dist/index.es.js.map +1 -1
- package/dist/src/gen/coordinator/index.d.ts +5 -0
- package/dist/src/rtc/Publisher.d.ts +4 -1
- package/dist/src/rtc/Subscriber.d.ts +4 -1
- package/package.json +1 -1
- package/src/Call.ts +10 -0
- package/src/gen/coordinator/index.ts +5 -0
- package/src/rtc/Publisher.ts +10 -2
- package/src/rtc/Subscriber.ts +23 -16
- package/src/rtc/__tests__/Subscriber.test.ts +9 -0
|
@@ -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
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];
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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(
|
package/src/rtc/Subscriber.ts
CHANGED
|
@@ -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
|
-
'
|
|
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('
|
|
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('
|
|
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
|
-
},
|
|
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
|