@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/Publisher.ts
CHANGED
|
@@ -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
|
-
* @
|
|
70
|
+
* @private
|
|
71
71
|
*/
|
|
72
|
-
private
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
'info',
|
|
522
|
-
`Update publish quality, enabled rids: `,
|
|
523
|
-
activeLayers,
|
|
524
|
-
);
|
|
519
|
+
logger('info', `Update publish quality, enabled rids: `, activeLayers);
|
|
525
520
|
} else {
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
+
logger('debug', 'Restarting ICE connection');
|
|
587
605
|
const signalingState = this.pc.signalingState;
|
|
588
606
|
if (this.isIceRestarting || signalingState === 'have-local-offer') {
|
|
589
|
-
|
|
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
|
-
|
|
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.
|
|
645
|
+
const trackInfos = this.getCurrentTrackInfos(offer.sdp);
|
|
620
646
|
if (trackInfos.length === 0) {
|
|
621
|
-
throw new Error(
|
|
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({
|
|
660
|
+
await this.pc.setRemoteDescription({
|
|
661
|
+
type: 'answer',
|
|
662
|
+
sdp: response.sdp,
|
|
663
|
+
});
|
|
634
664
|
} catch (e) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
+
logger('warn', 'No SDP found. Returning empty mid');
|
|
677
699
|
return '';
|
|
678
700
|
}
|
|
679
701
|
|
|
680
|
-
|
|
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
|
-
|
|
716
|
+
logger(
|
|
696
717
|
'debug',
|
|
697
|
-
`No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
811
|
+
logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
799
812
|
};
|
|
800
813
|
|
|
801
814
|
private onIceConnectionStateChange = () => {
|
|
802
815
|
const state = this.pc.iceConnectionState;
|
|
803
|
-
|
|
816
|
+
logger('debug', `ICE Connection state changed to`, state);
|
|
804
817
|
|
|
805
|
-
|
|
818
|
+
const hasNetworkConnection =
|
|
819
|
+
this.state.callingState !== CallingState.OFFLINE;
|
|
806
820
|
|
|
807
|
-
if (state === 'failed'
|
|
808
|
-
|
|
821
|
+
if (state === 'failed') {
|
|
822
|
+
logger('debug', `Attempting to restart ICE`);
|
|
809
823
|
this.restartIce().catch((e) => {
|
|
810
|
-
|
|
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
|
-
|
|
853
|
+
logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
818
854
|
};
|
|
819
855
|
|
|
820
856
|
private onSignalingStateChange = () => {
|
|
821
|
-
|
|
857
|
+
logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
822
858
|
};
|
|
823
859
|
|
|
824
860
|
private ridToVideoQuality = (rid: string): VideoQuality => {
|