@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.
- package/CHANGELOG.md +6 -230
- package/dist/index.browser.es.js +1498 -1963
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1495 -1961
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1498 -1963
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -93
- package/dist/src/StreamSfuClient.d.ts +56 -72
- package/dist/src/StreamVideoClient.d.ts +10 -2
- package/dist/src/coordinator/connection/client.d.ts +4 -3
- package/dist/src/coordinator/connection/types.d.ts +1 -5
- package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +3 -1
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
- package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/rpc/createClient.d.ts +0 -2
- package/dist/src/rpc/index.d.ts +0 -1
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +25 -24
- package/dist/src/rtc/Subscriber.d.ts +11 -12
- package/dist/src/rtc/flows/join.d.ts +20 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -46
- package/package.json +3 -3
- package/src/Call.ts +562 -615
- package/src/StreamSfuClient.ts +246 -277
- package/src/StreamVideoClient.ts +65 -15
- package/src/coordinator/connection/client.ts +8 -25
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/coordinator/connection/token_manager.ts +1 -1
- package/src/coordinator/connection/types.ts +0 -6
- package/src/devices/BrowserPermission.ts +1 -5
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +3 -12
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +13 -10
- package/src/events/__tests__/participant.test.ts +0 -75
- package/src/events/callEventHandlers.ts +7 -4
- package/src/events/internal.ts +3 -20
- package/src/events/mutes.ts +3 -5
- package/src/events/participant.ts +15 -48
- package/src/gen/video/sfu/event/events.ts +8 -451
- package/src/gen/video/sfu/models/models.ts +204 -211
- package/src/logger.ts +1 -3
- package/src/rpc/createClient.ts +0 -21
- package/src/rpc/index.ts +0 -1
- package/src/rtc/Dispatcher.ts +2 -6
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +163 -127
- package/src/rtc/Subscriber.ts +155 -94
- package/src/rtc/__tests__/Publisher.test.ts +95 -18
- package/src/rtc/__tests__/Subscriber.test.ts +99 -63
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/flows/join.ts +65 -0
- package/src/rtc/helpers/tracks.ts +7 -27
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +10 -1
- package/src/stats/SfuStatsReporter.ts +0 -1
- package/src/store/CallState.ts +2 -109
- package/src/store/__tests__/CallState.test.ts +37 -48
- package/dist/src/helpers/ensureExhausted.d.ts +0 -1
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/dist/src/rpc/retryable.d.ts +0 -23
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
- package/src/helpers/ensureExhausted.ts +0 -5
- package/src/helpers/withResolvers.ts +0 -43
- package/src/rpc/__tests__/retryable.test.ts +0 -72
- package/src/rpc/retryable.ts +0 -57
- package/src/rtc/helpers/rtcConfiguration.ts +0 -11
package/src/rtc/Subscriber.ts
CHANGED
|
@@ -5,27 +5,23 @@ import { SubscriberOffer } from '../gen/video/sfu/event/events';
|
|
|
5
5
|
import { Dispatcher } from './Dispatcher';
|
|
6
6
|
import { getLogger } from '../logger';
|
|
7
7
|
import { CallingState, CallState } from '../store';
|
|
8
|
-
import { withoutConcurrency } from '../helpers/concurrency';
|
|
9
|
-
import { toTrackType, trackTypeToParticipantStreamKey } from './helpers/tracks';
|
|
10
|
-
import { Logger } from '../coordinator/connection/types';
|
|
11
8
|
|
|
12
9
|
export type SubscriberOpts = {
|
|
13
10
|
sfuClient: StreamSfuClient;
|
|
14
11
|
dispatcher: Dispatcher;
|
|
15
12
|
state: CallState;
|
|
16
13
|
connectionConfig?: RTCConfiguration;
|
|
14
|
+
iceRestartDelay?: number;
|
|
17
15
|
onUnrecoverableError?: () => void;
|
|
18
|
-
logTag: string;
|
|
19
16
|
};
|
|
20
17
|
|
|
18
|
+
const logger = getLogger(['Subscriber']);
|
|
19
|
+
|
|
21
20
|
/**
|
|
22
21
|
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
23
22
|
* media streams from the SFU.
|
|
24
|
-
*
|
|
25
|
-
* @internal
|
|
26
23
|
*/
|
|
27
24
|
export class Subscriber {
|
|
28
|
-
private readonly logger: Logger;
|
|
29
25
|
private pc: RTCPeerConnection;
|
|
30
26
|
private sfuClient: StreamSfuClient;
|
|
31
27
|
private state: CallState;
|
|
@@ -34,7 +30,9 @@ export class Subscriber {
|
|
|
34
30
|
private readonly unregisterOnIceRestart: () => void;
|
|
35
31
|
private readonly onUnrecoverableError?: () => void;
|
|
36
32
|
|
|
33
|
+
private readonly iceRestartDelay: number;
|
|
37
34
|
private isIceRestarting = false;
|
|
35
|
+
private iceRestartTimeout?: NodeJS.Timeout;
|
|
38
36
|
|
|
39
37
|
// workaround for the lack of RTCPeerConnection.getConfiguration() method in react-native-webrtc
|
|
40
38
|
private _connectionConfiguration: RTCConfiguration | undefined;
|
|
@@ -58,44 +56,35 @@ export class Subscriber {
|
|
|
58
56
|
* @param connectionConfig the connection configuration to use.
|
|
59
57
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
60
58
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
61
|
-
* @param logTag a tag to use for logging.
|
|
62
59
|
*/
|
|
63
60
|
constructor({
|
|
64
61
|
sfuClient,
|
|
65
62
|
dispatcher,
|
|
66
63
|
state,
|
|
67
64
|
connectionConfig,
|
|
65
|
+
iceRestartDelay = 2500,
|
|
68
66
|
onUnrecoverableError,
|
|
69
|
-
logTag,
|
|
70
67
|
}: SubscriberOpts) {
|
|
71
|
-
this.logger = getLogger(['Subscriber', logTag]);
|
|
72
68
|
this.sfuClient = sfuClient;
|
|
73
69
|
this.state = state;
|
|
70
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
74
71
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
75
72
|
|
|
76
73
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
77
74
|
|
|
78
|
-
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
79
75
|
this.unregisterOnSubscriberOffer = dispatcher.on(
|
|
80
76
|
'subscriberOffer',
|
|
81
77
|
(subscriberOffer) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
85
|
-
return this.negotiate(subscriberOffer);
|
|
86
|
-
}).catch((err) => {
|
|
87
|
-
this.logger('warn', `Negotiation failed.`, err);
|
|
78
|
+
this.negotiate(subscriberOffer).catch((err) => {
|
|
79
|
+
logger('warn', `Negotiation failed.`, err);
|
|
88
80
|
});
|
|
89
81
|
},
|
|
90
82
|
);
|
|
91
83
|
|
|
92
|
-
const iceRestartConcurrencyTag = Symbol('iceRestart');
|
|
93
84
|
this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}).catch((err) => {
|
|
98
|
-
this.logger('warn', `ICERestart failed`, err);
|
|
85
|
+
if (iceRestart.peerType !== PeerType.SUBSCRIBER) return;
|
|
86
|
+
this.restartIce().catch((err) => {
|
|
87
|
+
logger('warn', `ICERestart failed`, err);
|
|
99
88
|
this.onUnrecoverableError?.();
|
|
100
89
|
});
|
|
101
90
|
});
|
|
@@ -129,30 +118,10 @@ export class Subscriber {
|
|
|
129
118
|
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
130
119
|
*/
|
|
131
120
|
close = () => {
|
|
132
|
-
this.
|
|
133
|
-
this.pc.close();
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
138
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
139
|
-
* instance with a new one (in case of migration).
|
|
140
|
-
*/
|
|
141
|
-
detachEventHandlers = () => {
|
|
121
|
+
clearTimeout(this.iceRestartTimeout);
|
|
142
122
|
this.unregisterOnSubscriberOffer();
|
|
143
123
|
this.unregisterOnIceRestart();
|
|
144
|
-
|
|
145
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
146
|
-
this.pc.removeEventListener('track', this.handleOnTrack);
|
|
147
|
-
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
148
|
-
this.pc.removeEventListener(
|
|
149
|
-
'iceconnectionstatechange',
|
|
150
|
-
this.onIceConnectionStateChange,
|
|
151
|
-
);
|
|
152
|
-
this.pc.removeEventListener(
|
|
153
|
-
'icegatheringstatechange',
|
|
154
|
-
this.onIceGatheringStateChange,
|
|
155
|
-
);
|
|
124
|
+
this.pc.close();
|
|
156
125
|
};
|
|
157
126
|
|
|
158
127
|
/**
|
|
@@ -173,17 +142,95 @@ export class Subscriber {
|
|
|
173
142
|
this.sfuClient = sfuClient;
|
|
174
143
|
};
|
|
175
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Migrates the subscriber to a new SFU client.
|
|
147
|
+
*
|
|
148
|
+
* @param sfuClient the new SFU client to migrate to.
|
|
149
|
+
* @param connectionConfig the new connection configuration to use.
|
|
150
|
+
*/
|
|
151
|
+
migrateTo = (
|
|
152
|
+
sfuClient: StreamSfuClient,
|
|
153
|
+
connectionConfig?: RTCConfiguration,
|
|
154
|
+
) => {
|
|
155
|
+
this.setSfuClient(sfuClient);
|
|
156
|
+
|
|
157
|
+
// when migrating, we want to keep the previous subscriber open
|
|
158
|
+
// until the new one is connected
|
|
159
|
+
const previousPC = this.pc;
|
|
160
|
+
|
|
161
|
+
// we keep a record of previously available video tracks
|
|
162
|
+
// so that we can monitor when they become available on the new
|
|
163
|
+
// subscriber and close the previous one.
|
|
164
|
+
const trackIdsToMigrate = new Set<string>();
|
|
165
|
+
previousPC.getReceivers().forEach((r) => {
|
|
166
|
+
if (r.track.kind === 'video') {
|
|
167
|
+
trackIdsToMigrate.add(r.track.id);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// set up a new subscriber peer connection, configured to connect
|
|
172
|
+
// to the new SFU node
|
|
173
|
+
const pc = this.createPeerConnection(connectionConfig);
|
|
174
|
+
|
|
175
|
+
let migrationTimeoutId: NodeJS.Timeout;
|
|
176
|
+
const cleanupMigration = () => {
|
|
177
|
+
previousPC.close();
|
|
178
|
+
clearTimeout(migrationTimeoutId);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// When migrating, we want to keep track of the video tracks
|
|
182
|
+
// that are migrating to the new subscriber.
|
|
183
|
+
// Once all of them are available, we can close the previous subscriber.
|
|
184
|
+
const handleTrackMigration = (e: RTCTrackEvent) => {
|
|
185
|
+
logger(
|
|
186
|
+
'debug',
|
|
187
|
+
`[Migration]: Migrated track: ${e.track.id}, ${e.track.kind}`,
|
|
188
|
+
);
|
|
189
|
+
trackIdsToMigrate.delete(e.track.id);
|
|
190
|
+
if (trackIdsToMigrate.size === 0) {
|
|
191
|
+
logger('debug', `[Migration]: Migration complete`);
|
|
192
|
+
pc.removeEventListener('track', handleTrackMigration);
|
|
193
|
+
cleanupMigration();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// When migrating, we want to keep track of the connection state
|
|
198
|
+
// of the new subscriber.
|
|
199
|
+
// Once it is connected, we give it a 2-second grace period to receive
|
|
200
|
+
// all the video tracks that are migrating from the previous subscriber.
|
|
201
|
+
// After this threshold, we abruptly close the previous subscriber.
|
|
202
|
+
const handleConnectionStateChange = () => {
|
|
203
|
+
if (pc.connectionState === 'connected') {
|
|
204
|
+
migrationTimeoutId = setTimeout(() => {
|
|
205
|
+
pc.removeEventListener('track', handleTrackMigration);
|
|
206
|
+
cleanupMigration();
|
|
207
|
+
}, 2000);
|
|
208
|
+
|
|
209
|
+
pc.removeEventListener(
|
|
210
|
+
'connectionstatechange',
|
|
211
|
+
handleConnectionStateChange,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
pc.addEventListener('track', handleTrackMigration);
|
|
217
|
+
pc.addEventListener('connectionstatechange', handleConnectionStateChange);
|
|
218
|
+
|
|
219
|
+
// replace the PeerConnection instance
|
|
220
|
+
this.pc = pc;
|
|
221
|
+
};
|
|
222
|
+
|
|
176
223
|
/**
|
|
177
224
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
178
225
|
*/
|
|
179
226
|
restartIce = async () => {
|
|
180
|
-
|
|
227
|
+
logger('debug', 'Restarting ICE connection');
|
|
181
228
|
if (this.pc.signalingState === 'have-remote-offer') {
|
|
182
|
-
|
|
229
|
+
logger('debug', 'ICE restart is already in progress');
|
|
183
230
|
return;
|
|
184
231
|
}
|
|
185
232
|
if (this.pc.connectionState === 'new') {
|
|
186
|
-
|
|
233
|
+
logger(
|
|
187
234
|
'debug',
|
|
188
235
|
`ICE connection is not yet established, skipping restart.`,
|
|
189
236
|
);
|
|
@@ -205,59 +252,54 @@ export class Subscriber {
|
|
|
205
252
|
private handleOnTrack = (e: RTCTrackEvent) => {
|
|
206
253
|
const [primaryStream] = e.streams;
|
|
207
254
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
208
|
-
const [trackId,
|
|
255
|
+
const [trackId, trackType] = primaryStream.id.split(':');
|
|
209
256
|
const participantToUpdate = this.state.participants.find(
|
|
210
257
|
(p) => p.trackLookupPrefix === trackId,
|
|
211
258
|
);
|
|
212
|
-
|
|
259
|
+
logger(
|
|
213
260
|
'debug',
|
|
214
|
-
`[onTrack]: Got remote ${
|
|
261
|
+
`[onTrack]: Got remote ${trackType} track for userId: ${participantToUpdate?.userId}`,
|
|
215
262
|
e.track.id,
|
|
216
263
|
e.track,
|
|
217
264
|
);
|
|
265
|
+
if (!participantToUpdate) {
|
|
266
|
+
logger(
|
|
267
|
+
'warn',
|
|
268
|
+
`[onTrack]: Received track for unknown participant: ${trackId}`,
|
|
269
|
+
e,
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
218
273
|
|
|
219
|
-
const trackDebugInfo = `${participantToUpdate
|
|
274
|
+
const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
|
|
220
275
|
e.track.addEventListener('mute', () => {
|
|
221
|
-
|
|
276
|
+
logger('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
|
|
222
277
|
});
|
|
223
278
|
|
|
224
279
|
e.track.addEventListener('unmute', () => {
|
|
225
|
-
|
|
280
|
+
logger('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
226
281
|
});
|
|
227
282
|
|
|
228
283
|
e.track.addEventListener('ended', () => {
|
|
229
|
-
|
|
230
|
-
this.state.removeOrphanedTrack(primaryStream.id);
|
|
284
|
+
logger('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
231
285
|
});
|
|
232
286
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`[onTrack]: Received track for unknown participant: ${trackId}`,
|
|
242
|
-
e,
|
|
243
|
-
);
|
|
244
|
-
this.state.registerOrphanedTrack({
|
|
245
|
-
id: primaryStream.id,
|
|
246
|
-
trackLookupPrefix: trackId,
|
|
247
|
-
track: primaryStream,
|
|
248
|
-
trackType,
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
287
|
+
const streamKindProp = (
|
|
288
|
+
{
|
|
289
|
+
TRACK_TYPE_AUDIO: 'audioStream',
|
|
290
|
+
TRACK_TYPE_VIDEO: 'videoStream',
|
|
291
|
+
TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
|
|
292
|
+
TRACK_TYPE_SCREEN_SHARE_AUDIO: 'screenShareAudioStream',
|
|
293
|
+
} as const
|
|
294
|
+
)[trackType];
|
|
252
295
|
|
|
253
|
-
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
254
296
|
if (!streamKindProp) {
|
|
255
|
-
|
|
297
|
+
logger('error', `Unknown track type: ${trackType}`);
|
|
256
298
|
return;
|
|
257
299
|
}
|
|
258
300
|
const previousStream = participantToUpdate[streamKindProp];
|
|
259
301
|
if (previousStream) {
|
|
260
|
-
|
|
302
|
+
logger(
|
|
261
303
|
'info',
|
|
262
304
|
`[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`,
|
|
263
305
|
);
|
|
@@ -274,7 +316,7 @@ export class Subscriber {
|
|
|
274
316
|
private onIceCandidate = (e: RTCPeerConnectionIceEvent) => {
|
|
275
317
|
const { candidate } = e;
|
|
276
318
|
if (!candidate) {
|
|
277
|
-
|
|
319
|
+
logger('debug', 'null ice candidate');
|
|
278
320
|
return;
|
|
279
321
|
}
|
|
280
322
|
|
|
@@ -284,12 +326,12 @@ export class Subscriber {
|
|
|
284
326
|
peerType: PeerType.SUBSCRIBER,
|
|
285
327
|
})
|
|
286
328
|
.catch((err) => {
|
|
287
|
-
|
|
329
|
+
logger('warn', `ICETrickle failed`, err);
|
|
288
330
|
});
|
|
289
331
|
};
|
|
290
332
|
|
|
291
333
|
private negotiate = async (subscriberOffer: SubscriberOffer) => {
|
|
292
|
-
|
|
334
|
+
logger('info', `Received subscriberOffer`, subscriberOffer);
|
|
293
335
|
|
|
294
336
|
await this.pc.setRemoteDescription({
|
|
295
337
|
type: 'offer',
|
|
@@ -302,7 +344,7 @@ export class Subscriber {
|
|
|
302
344
|
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
303
345
|
await this.pc.addIceCandidate(iceCandidate);
|
|
304
346
|
} catch (e) {
|
|
305
|
-
|
|
347
|
+
logger('warn', `ICE candidate error`, [e, candidate]);
|
|
306
348
|
}
|
|
307
349
|
},
|
|
308
350
|
);
|
|
@@ -320,28 +362,47 @@ export class Subscriber {
|
|
|
320
362
|
|
|
321
363
|
private onIceConnectionStateChange = () => {
|
|
322
364
|
const state = this.pc.iceConnectionState;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (this.state.callingState === CallingState.RECONNECTING) return;
|
|
365
|
+
logger('debug', `ICE connection state changed`, state);
|
|
326
366
|
|
|
327
367
|
// do nothing when ICE is restarting
|
|
328
368
|
if (this.isIceRestarting) return;
|
|
329
369
|
|
|
330
|
-
|
|
331
|
-
this.
|
|
370
|
+
const hasNetworkConnection =
|
|
371
|
+
this.state.callingState !== CallingState.OFFLINE;
|
|
372
|
+
|
|
373
|
+
if (state === 'failed') {
|
|
374
|
+
logger('debug', `Attempting to restart ICE`);
|
|
332
375
|
this.restartIce().catch((e) => {
|
|
333
|
-
|
|
376
|
+
logger('error', `ICE restart failed`, e);
|
|
334
377
|
this.onUnrecoverableError?.();
|
|
335
378
|
});
|
|
379
|
+
} else if (state === 'disconnected' && hasNetworkConnection) {
|
|
380
|
+
// when in `disconnected` state, the browser may recover automatically,
|
|
381
|
+
// hence, we delay the ICE restart
|
|
382
|
+
logger('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
|
|
383
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
384
|
+
// check if the state is still `disconnected` or `failed`
|
|
385
|
+
// as the connection may have recovered (or failed) in the meantime
|
|
386
|
+
if (
|
|
387
|
+
this.pc.iceConnectionState === 'disconnected' ||
|
|
388
|
+
this.pc.iceConnectionState === 'failed'
|
|
389
|
+
) {
|
|
390
|
+
this.restartIce().catch((e) => {
|
|
391
|
+
logger('error', `ICE restart failed`, e);
|
|
392
|
+
this.onUnrecoverableError?.();
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
logger(
|
|
396
|
+
'debug',
|
|
397
|
+
`Scheduled ICE restart: connection recovered, canceled.`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}, this.iceRestartDelay);
|
|
336
401
|
}
|
|
337
402
|
};
|
|
338
403
|
|
|
339
404
|
private onIceGatheringStateChange = () => {
|
|
340
|
-
this.
|
|
341
|
-
'debug',
|
|
342
|
-
`ICE gathering state changed`,
|
|
343
|
-
this.pc.iceGatheringState,
|
|
344
|
-
);
|
|
405
|
+
logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
345
406
|
};
|
|
346
407
|
|
|
347
408
|
private onIceCandidateError = (e: Event) => {
|
|
@@ -351,6 +412,6 @@ export class Subscriber {
|
|
|
351
412
|
const iceState = this.pc.iceConnectionState;
|
|
352
413
|
const logLevel =
|
|
353
414
|
iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
354
|
-
|
|
415
|
+
logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
355
416
|
};
|
|
356
417
|
}
|
|
@@ -4,10 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { Publisher } from '../Publisher';
|
|
5
5
|
import { CallState } from '../../store';
|
|
6
6
|
import { StreamSfuClient } from '../../StreamSfuClient';
|
|
7
|
-
import {
|
|
7
|
+
import { Dispatcher } from '../Dispatcher';
|
|
8
8
|
import { PeerType, TrackType } from '../../gen/video/sfu/models/models';
|
|
9
|
-
import { SfuEvent } from '../../gen/video/sfu/event/events';
|
|
10
9
|
import { IceTrickleBuffer } from '../IceTrickleBuffer';
|
|
10
|
+
import { SfuEvent } from '../../gen/video/sfu/event/events';
|
|
11
11
|
|
|
12
12
|
vi.mock('../../StreamSfuClient', () => {
|
|
13
13
|
console.log('MOCKING StreamSfuClient');
|
|
@@ -40,22 +40,14 @@ describe('Publisher', () => {
|
|
|
40
40
|
dispatcher = new Dispatcher();
|
|
41
41
|
sfuClient = new StreamSfuClient({
|
|
42
42
|
dispatcher,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ws_endpoint: 'https://getstream.io/ws',
|
|
48
|
-
edge_name: 'sfu-1',
|
|
49
|
-
},
|
|
50
|
-
token: 'token',
|
|
51
|
-
ice_servers: [],
|
|
43
|
+
sfuServer: {
|
|
44
|
+
url: 'https://getstream.io/',
|
|
45
|
+
ws_endpoint: 'https://getstream.io/ws',
|
|
46
|
+
edge_name: 'sfu-1',
|
|
52
47
|
},
|
|
53
|
-
|
|
48
|
+
token: 'token',
|
|
54
49
|
});
|
|
55
50
|
|
|
56
|
-
// @ts-expect-error readonly field
|
|
57
|
-
sfuClient.iceTrickleBuffer = new IceTrickleBuffer();
|
|
58
|
-
|
|
59
51
|
// @ts-ignore
|
|
60
52
|
sfuClient['sessionId'] = sessionId;
|
|
61
53
|
|
|
@@ -66,7 +58,7 @@ describe('Publisher', () => {
|
|
|
66
58
|
state,
|
|
67
59
|
isDtxEnabled: true,
|
|
68
60
|
isRedEnabled: true,
|
|
69
|
-
|
|
61
|
+
iceRestartDelay: 100,
|
|
70
62
|
});
|
|
71
63
|
});
|
|
72
64
|
|
|
@@ -218,6 +210,74 @@ describe('Publisher', () => {
|
|
|
218
210
|
expect(addEventListenerSpy).not.toHaveBeenCalled();
|
|
219
211
|
});
|
|
220
212
|
|
|
213
|
+
describe('Publisher migration', () => {
|
|
214
|
+
it('should update the sfuClient and peer connection configuration', async () => {
|
|
215
|
+
const newSfuClient = new StreamSfuClient({
|
|
216
|
+
dispatcher: new Dispatcher(),
|
|
217
|
+
sfuServer: {
|
|
218
|
+
url: 'https://getstream.io/',
|
|
219
|
+
ws_endpoint: 'https://getstream.io/ws',
|
|
220
|
+
edge_name: 'sfu-1',
|
|
221
|
+
},
|
|
222
|
+
token: 'token',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const newPeerConnectionConfig = {
|
|
226
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
vi.spyOn(publisher['pc'], 'setConfiguration');
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
vi.spyOn(publisher, 'negotiate').mockReturnValue(Promise.resolve());
|
|
234
|
+
vi.spyOn(publisher, 'isPublishing').mockReturnValue(true);
|
|
235
|
+
|
|
236
|
+
await publisher.migrateTo(newSfuClient, newPeerConnectionConfig);
|
|
237
|
+
|
|
238
|
+
expect(publisher['sfuClient']).toEqual(newSfuClient);
|
|
239
|
+
expect(publisher['pc'].setConfiguration).toHaveBeenCalledWith(
|
|
240
|
+
newPeerConnectionConfig,
|
|
241
|
+
);
|
|
242
|
+
expect(publisher['negotiate']).toHaveBeenCalledWith({ iceRestart: true });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should initiate ICE Restart when there are published tracks', async () => {
|
|
246
|
+
vi.spyOn(publisher['pc'], 'getTransceivers').mockReturnValue([]);
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
sfuClient['iceTrickleBuffer'] = new IceTrickleBuffer();
|
|
249
|
+
sfuClient.setPublisher = vi.fn().mockResolvedValue({
|
|
250
|
+
response: {
|
|
251
|
+
sessionId: 'new-session-id',
|
|
252
|
+
sdp: 'new-sdp',
|
|
253
|
+
iceRestart: false,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// @ts-ignore
|
|
258
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
259
|
+
vi.spyOn(publisher, 'isPublishing').mockReturnValue(true);
|
|
260
|
+
vi.spyOn(publisher, 'getCurrentTrackInfos').mockReturnValue([
|
|
261
|
+
// @ts-expect-error
|
|
262
|
+
{ layers: [], trackType: TrackType.AUDIO, mid: '0' },
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
await publisher.migrateTo(sfuClient, {
|
|
266
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(publisher['pc'].createOffer).toHaveBeenCalledWith({
|
|
270
|
+
iceRestart: true,
|
|
271
|
+
});
|
|
272
|
+
expect(publisher['pc'].setLocalDescription).toHaveBeenCalled();
|
|
273
|
+
expect(publisher['pc'].setRemoteDescription).toHaveBeenCalledWith({
|
|
274
|
+
type: 'answer',
|
|
275
|
+
sdp: 'new-sdp',
|
|
276
|
+
});
|
|
277
|
+
expect(sfuClient.setPublisher).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
221
281
|
describe('Publisher ICE Restart', () => {
|
|
222
282
|
it('should perform ICE restart when iceRestart event is received', () => {
|
|
223
283
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
@@ -229,7 +289,7 @@ describe('Publisher', () => {
|
|
|
229
289
|
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
230
290
|
},
|
|
231
291
|
},
|
|
232
|
-
})
|
|
292
|
+
}),
|
|
233
293
|
);
|
|
234
294
|
expect(publisher.restartIce).toHaveBeenCalled();
|
|
235
295
|
});
|
|
@@ -244,7 +304,7 @@ describe('Publisher', () => {
|
|
|
244
304
|
peerType: PeerType.SUBSCRIBER,
|
|
245
305
|
},
|
|
246
306
|
},
|
|
247
|
-
})
|
|
307
|
+
}),
|
|
248
308
|
);
|
|
249
309
|
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
250
310
|
});
|
|
@@ -269,10 +329,27 @@ describe('Publisher', () => {
|
|
|
269
329
|
|
|
270
330
|
it(`should perform ICE restart when connection state changes to 'disconnected'`, () => {
|
|
271
331
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
332
|
+
vi.useFakeTimers();
|
|
333
|
+
|
|
272
334
|
// @ts-ignore
|
|
273
335
|
publisher['pc'].iceConnectionState = 'disconnected';
|
|
274
336
|
publisher['onIceConnectionStateChange']();
|
|
337
|
+
vi.runAllTimers();
|
|
275
338
|
expect(publisher.restartIce).toHaveBeenCalled();
|
|
276
339
|
});
|
|
340
|
+
|
|
341
|
+
it(`should bail-out from ICE restart once connection recovers before timeout`, () => {
|
|
342
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
343
|
+
vi.useFakeTimers();
|
|
344
|
+
|
|
345
|
+
// @ts-ignore
|
|
346
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
347
|
+
publisher['onIceConnectionStateChange']();
|
|
348
|
+
// @ts-ignore
|
|
349
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
350
|
+
|
|
351
|
+
vi.runAllTimers();
|
|
352
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
277
354
|
});
|
|
278
355
|
});
|