@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.
- package/CHANGELOG.md +175 -0
- package/dist/index.browser.es.js +1986 -1482
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1983 -1478
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1986 -1482
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +93 -9
- package/dist/src/StreamSfuClient.d.ts +73 -56
- package/dist/src/StreamVideoClient.d.ts +2 -2
- package/dist/src/coordinator/connection/client.d.ts +3 -4
- package/dist/src/coordinator/connection/types.d.ts +5 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +1 -3
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
- package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
- package/dist/src/helpers/ensureExhausted.d.ts +1 -0
- package/dist/src/helpers/withResolvers.d.ts +14 -0
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/rpc/createClient.d.ts +2 -0
- package/dist/src/rpc/index.d.ts +1 -0
- package/dist/src/rpc/retryable.d.ts +23 -0
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
- package/dist/src/rtc/Publisher.d.ts +24 -25
- package/dist/src/rtc/Subscriber.d.ts +12 -11
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -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 +46 -2
- package/package.json +3 -3
- package/src/Call.ts +628 -566
- package/src/StreamSfuClient.ts +276 -246
- package/src/StreamVideoClient.ts +15 -16
- package/src/coordinator/connection/client.ts +25 -8
- package/src/coordinator/connection/connection.ts +1 -0
- package/src/coordinator/connection/types.ts +6 -0
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +12 -3
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +10 -13
- package/src/events/__tests__/participant.test.ts +75 -0
- package/src/events/callEventHandlers.ts +4 -7
- package/src/events/internal.ts +20 -3
- package/src/events/mutes.ts +5 -3
- package/src/events/participant.ts +48 -15
- package/src/gen/video/sfu/event/events.ts +451 -8
- package/src/gen/video/sfu/models/models.ts +211 -204
- package/src/helpers/ensureExhausted.ts +5 -0
- package/src/helpers/withResolvers.ts +43 -0
- package/src/logger.ts +3 -1
- package/src/rpc/__tests__/retryable.test.ts +72 -0
- package/src/rpc/createClient.ts +21 -0
- package/src/rpc/index.ts +1 -0
- package/src/rpc/retryable.ts +57 -0
- package/src/rtc/Dispatcher.ts +6 -2
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +127 -163
- package/src/rtc/Subscriber.ts +92 -155
- package/src/rtc/__tests__/Publisher.test.ts +18 -95
- package/src/rtc/__tests__/Subscriber.test.ts +63 -99
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/helpers/rtcConfiguration.ts +11 -0
- package/src/rtc/helpers/tracks.ts +27 -7
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +1 -10
- package/src/stats/SfuStatsReporter.ts +1 -0
- package/src/store/CallState.ts +109 -2
- package/src/store/__tests__/CallState.test.ts +48 -37
- package/dist/src/rtc/flows/join.d.ts +0 -20
- package/src/rtc/flows/join.ts +0 -65
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
667
|
-
|
|
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`,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 => {
|