@stream-io/video-client 1.14.0 → 1.15.1
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 +1533 -1783
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1514 -1783
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1533 -1783
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +43 -28
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/types.d.ts +12 -22
- package/package.json +1 -1
- package/src/Call.ts +254 -268
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +38 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/types.ts +12 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
package/dist/index.cjs.js
CHANGED
|
@@ -5,30 +5,11 @@ var runtime = require('@protobuf-ts/runtime');
|
|
|
5
5
|
var runtimeRpc = require('@protobuf-ts/runtime-rpc');
|
|
6
6
|
var axios = require('axios');
|
|
7
7
|
var twirpTransport = require('@protobuf-ts/twirp-transport');
|
|
8
|
-
var uaParserJs = require('ua-parser-js');
|
|
9
8
|
var rxjs = require('rxjs');
|
|
10
|
-
var
|
|
9
|
+
var sdpTransform = require('sdp-transform');
|
|
10
|
+
var uaParserJs = require('ua-parser-js');
|
|
11
11
|
var https = require('https');
|
|
12
12
|
|
|
13
|
-
function _interopNamespaceDefault(e) {
|
|
14
|
-
var n = Object.create(null);
|
|
15
|
-
if (e) {
|
|
16
|
-
Object.keys(e).forEach(function (k) {
|
|
17
|
-
if (k !== 'default') {
|
|
18
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
get: function () { return e[k]; }
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
n.default = e;
|
|
27
|
-
return Object.freeze(n);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
var SDP__namespace = /*#__PURE__*/_interopNamespaceDefault(SDP);
|
|
31
|
-
|
|
32
13
|
/* tslint:disable */
|
|
33
14
|
/* eslint-disable */
|
|
34
15
|
/**
|
|
@@ -1245,23 +1226,33 @@ class VideoLayer$Type extends runtime.MessageType {
|
|
|
1245
1226
|
*/
|
|
1246
1227
|
const VideoLayer = new VideoLayer$Type();
|
|
1247
1228
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1248
|
-
class
|
|
1229
|
+
class SubscribeOption$Type extends runtime.MessageType {
|
|
1249
1230
|
constructor() {
|
|
1250
|
-
super('stream.video.sfu.models.
|
|
1231
|
+
super('stream.video.sfu.models.SubscribeOption', [
|
|
1251
1232
|
{
|
|
1252
1233
|
no: 1,
|
|
1234
|
+
name: 'track_type',
|
|
1235
|
+
kind: 'enum',
|
|
1236
|
+
T: () => [
|
|
1237
|
+
'stream.video.sfu.models.TrackType',
|
|
1238
|
+
TrackType,
|
|
1239
|
+
'TRACK_TYPE_',
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
no: 2,
|
|
1253
1244
|
name: 'codecs',
|
|
1254
1245
|
kind: 'message',
|
|
1255
1246
|
repeat: 1 /*RepeatType.PACKED*/,
|
|
1256
|
-
T: () =>
|
|
1247
|
+
T: () => Codec,
|
|
1257
1248
|
},
|
|
1258
1249
|
]);
|
|
1259
1250
|
}
|
|
1260
1251
|
}
|
|
1261
1252
|
/**
|
|
1262
|
-
* @generated MessageType for protobuf message stream.video.sfu.models.
|
|
1253
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.SubscribeOption
|
|
1263
1254
|
*/
|
|
1264
|
-
const
|
|
1255
|
+
const SubscribeOption = new SubscribeOption$Type();
|
|
1265
1256
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1266
1257
|
class PublishOption$Type extends runtime.MessageType {
|
|
1267
1258
|
constructor() {
|
|
@@ -1291,6 +1282,13 @@ class PublishOption$Type extends runtime.MessageType {
|
|
|
1291
1282
|
kind: 'scalar',
|
|
1292
1283
|
T: 5 /*ScalarType.INT32*/,
|
|
1293
1284
|
},
|
|
1285
|
+
{
|
|
1286
|
+
no: 7,
|
|
1287
|
+
name: 'video_dimension',
|
|
1288
|
+
kind: 'message',
|
|
1289
|
+
T: () => VideoDimension,
|
|
1290
|
+
},
|
|
1291
|
+
{ no: 8, name: 'id', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
|
|
1294
1292
|
]);
|
|
1295
1293
|
}
|
|
1296
1294
|
}
|
|
@@ -1303,7 +1301,7 @@ class Codec$Type extends runtime.MessageType {
|
|
|
1303
1301
|
constructor() {
|
|
1304
1302
|
super('stream.video.sfu.models.Codec', [
|
|
1305
1303
|
{
|
|
1306
|
-
no:
|
|
1304
|
+
no: 16,
|
|
1307
1305
|
name: 'payload_type',
|
|
1308
1306
|
kind: 'scalar',
|
|
1309
1307
|
T: 13 /*ScalarType.UINT32*/,
|
|
@@ -1316,7 +1314,7 @@ class Codec$Type extends runtime.MessageType {
|
|
|
1316
1314
|
T: 13 /*ScalarType.UINT32*/,
|
|
1317
1315
|
},
|
|
1318
1316
|
{
|
|
1319
|
-
no:
|
|
1317
|
+
no: 15,
|
|
1320
1318
|
name: 'encoding_parameters',
|
|
1321
1319
|
kind: 'scalar',
|
|
1322
1320
|
T: 9 /*ScalarType.STRING*/,
|
|
@@ -1380,6 +1378,13 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1380
1378
|
{ no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1381
1379
|
{ no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1382
1380
|
{ no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1381
|
+
{ no: 11, name: 'codec', kind: 'message', T: () => Codec },
|
|
1382
|
+
{
|
|
1383
|
+
no: 12,
|
|
1384
|
+
name: 'publish_option_id',
|
|
1385
|
+
kind: 'scalar',
|
|
1386
|
+
T: 5 /*ScalarType.INT32*/,
|
|
1387
|
+
},
|
|
1383
1388
|
]);
|
|
1384
1389
|
}
|
|
1385
1390
|
}
|
|
@@ -1653,10 +1658,10 @@ var models = /*#__PURE__*/Object.freeze({
|
|
|
1653
1658
|
get PeerType () { return PeerType; },
|
|
1654
1659
|
Pin: Pin,
|
|
1655
1660
|
PublishOption: PublishOption,
|
|
1656
|
-
PublishOptions: PublishOptions,
|
|
1657
1661
|
Sdk: Sdk,
|
|
1658
1662
|
get SdkType () { return SdkType; },
|
|
1659
1663
|
StreamQuality: StreamQuality,
|
|
1664
|
+
SubscribeOption: SubscribeOption,
|
|
1660
1665
|
TrackInfo: TrackInfo,
|
|
1661
1666
|
get TrackType () { return TrackType; },
|
|
1662
1667
|
get TrackUnpublishReason () { return TrackUnpublishReason; },
|
|
@@ -2286,13 +2291,6 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
2286
2291
|
oneof: 'eventPayload',
|
|
2287
2292
|
T: () => ParticipantMigrationComplete,
|
|
2288
2293
|
},
|
|
2289
|
-
{
|
|
2290
|
-
no: 26,
|
|
2291
|
-
name: 'codec_negotiation_complete',
|
|
2292
|
-
kind: 'message',
|
|
2293
|
-
oneof: 'eventPayload',
|
|
2294
|
-
T: () => CodecNegotiationComplete,
|
|
2295
|
-
},
|
|
2296
2294
|
{
|
|
2297
2295
|
no: 27,
|
|
2298
2296
|
name: 'change_publish_options',
|
|
@@ -2313,10 +2311,12 @@ class ChangePublishOptions$Type extends runtime.MessageType {
|
|
|
2313
2311
|
super('stream.video.sfu.event.ChangePublishOptions', [
|
|
2314
2312
|
{
|
|
2315
2313
|
no: 1,
|
|
2316
|
-
name: '
|
|
2314
|
+
name: 'publish_options',
|
|
2317
2315
|
kind: 'message',
|
|
2316
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2318
2317
|
T: () => PublishOption,
|
|
2319
2318
|
},
|
|
2319
|
+
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2320
2320
|
]);
|
|
2321
2321
|
}
|
|
2322
2322
|
}
|
|
@@ -2325,15 +2325,15 @@ class ChangePublishOptions$Type extends runtime.MessageType {
|
|
|
2325
2325
|
*/
|
|
2326
2326
|
const ChangePublishOptions = new ChangePublishOptions$Type();
|
|
2327
2327
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2328
|
-
class
|
|
2328
|
+
class ChangePublishOptionsComplete$Type extends runtime.MessageType {
|
|
2329
2329
|
constructor() {
|
|
2330
|
-
super('stream.video.sfu.event.
|
|
2330
|
+
super('stream.video.sfu.event.ChangePublishOptionsComplete', []);
|
|
2331
2331
|
}
|
|
2332
2332
|
}
|
|
2333
2333
|
/**
|
|
2334
|
-
* @generated MessageType for protobuf message stream.video.sfu.event.
|
|
2334
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.ChangePublishOptionsComplete
|
|
2335
2335
|
*/
|
|
2336
|
-
const
|
|
2336
|
+
const ChangePublishOptionsComplete = new ChangePublishOptionsComplete$Type();
|
|
2337
2337
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2338
2338
|
class ParticipantMigrationComplete$Type extends runtime.MessageType {
|
|
2339
2339
|
constructor() {
|
|
@@ -2591,6 +2591,20 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
2591
2591
|
kind: 'message',
|
|
2592
2592
|
T: () => ReconnectDetails,
|
|
2593
2593
|
},
|
|
2594
|
+
{
|
|
2595
|
+
no: 9,
|
|
2596
|
+
name: 'preferred_publish_options',
|
|
2597
|
+
kind: 'message',
|
|
2598
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2599
|
+
T: () => PublishOption,
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
no: 10,
|
|
2603
|
+
name: 'preferred_subscribe_options',
|
|
2604
|
+
kind: 'message',
|
|
2605
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2606
|
+
T: () => SubscribeOption,
|
|
2607
|
+
},
|
|
2594
2608
|
]);
|
|
2595
2609
|
}
|
|
2596
2610
|
}
|
|
@@ -2698,7 +2712,8 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
2698
2712
|
no: 4,
|
|
2699
2713
|
name: 'publish_options',
|
|
2700
2714
|
kind: 'message',
|
|
2701
|
-
|
|
2715
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
2716
|
+
T: () => PublishOption,
|
|
2702
2717
|
},
|
|
2703
2718
|
]);
|
|
2704
2719
|
}
|
|
@@ -2863,6 +2878,22 @@ class AudioSender$Type extends runtime.MessageType {
|
|
|
2863
2878
|
constructor() {
|
|
2864
2879
|
super('stream.video.sfu.event.AudioSender', [
|
|
2865
2880
|
{ no: 2, name: 'codec', kind: 'message', T: () => Codec },
|
|
2881
|
+
{
|
|
2882
|
+
no: 3,
|
|
2883
|
+
name: 'track_type',
|
|
2884
|
+
kind: 'enum',
|
|
2885
|
+
T: () => [
|
|
2886
|
+
'stream.video.sfu.models.TrackType',
|
|
2887
|
+
TrackType,
|
|
2888
|
+
'TRACK_TYPE_',
|
|
2889
|
+
],
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
no: 4,
|
|
2893
|
+
name: 'publish_option_id',
|
|
2894
|
+
kind: 'scalar',
|
|
2895
|
+
T: 5 /*ScalarType.INT32*/,
|
|
2896
|
+
},
|
|
2866
2897
|
]);
|
|
2867
2898
|
}
|
|
2868
2899
|
}
|
|
@@ -2915,6 +2946,22 @@ class VideoSender$Type extends runtime.MessageType {
|
|
|
2915
2946
|
repeat: 1 /*RepeatType.PACKED*/,
|
|
2916
2947
|
T: () => VideoLayerSetting,
|
|
2917
2948
|
},
|
|
2949
|
+
{
|
|
2950
|
+
no: 4,
|
|
2951
|
+
name: 'track_type',
|
|
2952
|
+
kind: 'enum',
|
|
2953
|
+
T: () => [
|
|
2954
|
+
'stream.video.sfu.models.TrackType',
|
|
2955
|
+
TrackType,
|
|
2956
|
+
'TRACK_TYPE_',
|
|
2957
|
+
],
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
no: 5,
|
|
2961
|
+
name: 'publish_option_id',
|
|
2962
|
+
kind: 'scalar',
|
|
2963
|
+
T: 5 /*ScalarType.INT32*/,
|
|
2964
|
+
},
|
|
2918
2965
|
]);
|
|
2919
2966
|
}
|
|
2920
2967
|
}
|
|
@@ -3011,8 +3058,8 @@ var events = /*#__PURE__*/Object.freeze({
|
|
|
3011
3058
|
CallEnded: CallEnded,
|
|
3012
3059
|
CallGrantsUpdated: CallGrantsUpdated,
|
|
3013
3060
|
ChangePublishOptions: ChangePublishOptions,
|
|
3061
|
+
ChangePublishOptionsComplete: ChangePublishOptionsComplete,
|
|
3014
3062
|
ChangePublishQuality: ChangePublishQuality,
|
|
3015
|
-
CodecNegotiationComplete: CodecNegotiationComplete,
|
|
3016
3063
|
ConnectionQualityChanged: ConnectionQualityChanged,
|
|
3017
3064
|
ConnectionQualityInfo: ConnectionQualityInfo,
|
|
3018
3065
|
DominantSpeakerChanged: DominantSpeakerChanged,
|
|
@@ -3159,11 +3206,18 @@ const withHeaders = (headers) => {
|
|
|
3159
3206
|
const withRequestLogger = (logger, level) => {
|
|
3160
3207
|
return {
|
|
3161
3208
|
interceptUnary: (next, method, input, options) => {
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
options
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3209
|
+
let invocation;
|
|
3210
|
+
try {
|
|
3211
|
+
invocation = next(method, input, options);
|
|
3212
|
+
}
|
|
3213
|
+
finally {
|
|
3214
|
+
logger(level, `Invoked SFU RPC method ${method.name}`, {
|
|
3215
|
+
request: invocation?.request,
|
|
3216
|
+
headers: invocation?.requestHeaders,
|
|
3217
|
+
response: invocation?.response,
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
return invocation;
|
|
3167
3221
|
},
|
|
3168
3222
|
};
|
|
3169
3223
|
};
|
|
@@ -3380,374 +3434,98 @@ const retryable = async (rpc, signal) => {
|
|
|
3380
3434
|
return result;
|
|
3381
3435
|
};
|
|
3382
3436
|
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
const setOSInfo = (info) => {
|
|
3402
|
-
osInfo = info;
|
|
3403
|
-
};
|
|
3404
|
-
const getOSInfo = () => {
|
|
3405
|
-
return osInfo;
|
|
3406
|
-
};
|
|
3407
|
-
const setDeviceInfo = (info) => {
|
|
3408
|
-
deviceInfo = info;
|
|
3437
|
+
/**
|
|
3438
|
+
* Returns a generic SDP for the given direction.
|
|
3439
|
+
* We use this SDP to send it as part of our JoinRequest so that the SFU
|
|
3440
|
+
* can use it to determine the client's codec capabilities.
|
|
3441
|
+
*
|
|
3442
|
+
* @param direction the direction of the transceiver.
|
|
3443
|
+
*/
|
|
3444
|
+
const getGenericSdp = async (direction) => {
|
|
3445
|
+
const tempPc = new RTCPeerConnection();
|
|
3446
|
+
tempPc.addTransceiver('video', { direction });
|
|
3447
|
+
tempPc.addTransceiver('audio', { direction });
|
|
3448
|
+
const offer = await tempPc.createOffer();
|
|
3449
|
+
const sdp = offer.sdp ?? '';
|
|
3450
|
+
tempPc.getTransceivers().forEach((t) => {
|
|
3451
|
+
t.stop?.();
|
|
3452
|
+
});
|
|
3453
|
+
tempPc.close();
|
|
3454
|
+
return sdp;
|
|
3409
3455
|
};
|
|
3410
|
-
|
|
3411
|
-
|
|
3456
|
+
/**
|
|
3457
|
+
* Returns whether the codec is an SVC codec.
|
|
3458
|
+
*
|
|
3459
|
+
* @param codecOrMimeType the codec to check.
|
|
3460
|
+
*/
|
|
3461
|
+
const isSvcCodec = (codecOrMimeType) => {
|
|
3462
|
+
if (!codecOrMimeType)
|
|
3463
|
+
return false;
|
|
3464
|
+
codecOrMimeType = codecOrMimeType.toLowerCase();
|
|
3465
|
+
return (codecOrMimeType === 'vp9' ||
|
|
3466
|
+
codecOrMimeType === 'av1' ||
|
|
3467
|
+
codecOrMimeType === 'video/vp9' ||
|
|
3468
|
+
codecOrMimeType === 'video/av1');
|
|
3412
3469
|
};
|
|
3413
|
-
|
|
3414
|
-
|
|
3470
|
+
|
|
3471
|
+
const sfuEventKinds = {
|
|
3472
|
+
subscriberOffer: undefined,
|
|
3473
|
+
publisherAnswer: undefined,
|
|
3474
|
+
connectionQualityChanged: undefined,
|
|
3475
|
+
audioLevelChanged: undefined,
|
|
3476
|
+
iceTrickle: undefined,
|
|
3477
|
+
changePublishQuality: undefined,
|
|
3478
|
+
participantJoined: undefined,
|
|
3479
|
+
participantLeft: undefined,
|
|
3480
|
+
dominantSpeakerChanged: undefined,
|
|
3481
|
+
joinResponse: undefined,
|
|
3482
|
+
healthCheckResponse: undefined,
|
|
3483
|
+
trackPublished: undefined,
|
|
3484
|
+
trackUnpublished: undefined,
|
|
3485
|
+
error: undefined,
|
|
3486
|
+
callGrantsUpdated: undefined,
|
|
3487
|
+
goAway: undefined,
|
|
3488
|
+
iceRestart: undefined,
|
|
3489
|
+
pinsUpdated: undefined,
|
|
3490
|
+
callEnded: undefined,
|
|
3491
|
+
participantUpdated: undefined,
|
|
3492
|
+
participantMigrationComplete: undefined,
|
|
3493
|
+
changePublishOptions: undefined,
|
|
3415
3494
|
};
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3495
|
+
const isSfuEvent = (eventName) => {
|
|
3496
|
+
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
3418
3497
|
};
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3498
|
+
class Dispatcher {
|
|
3499
|
+
constructor() {
|
|
3500
|
+
this.logger = getLogger(['Dispatcher']);
|
|
3501
|
+
this.subscribers = {};
|
|
3502
|
+
this.dispatch = (message, logTag = '0') => {
|
|
3503
|
+
const eventKind = message.eventPayload.oneofKind;
|
|
3504
|
+
if (!eventKind)
|
|
3505
|
+
return;
|
|
3506
|
+
const payload = message.eventPayload[eventKind];
|
|
3507
|
+
this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
|
|
3508
|
+
const listeners = this.subscribers[eventKind];
|
|
3509
|
+
if (!listeners)
|
|
3510
|
+
return;
|
|
3511
|
+
for (const fn of listeners) {
|
|
3512
|
+
try {
|
|
3513
|
+
fn(payload);
|
|
3514
|
+
}
|
|
3515
|
+
catch (e) {
|
|
3516
|
+
this.logger('warn', 'Listener failed with error', e);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3434
3519
|
};
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
apple: {
|
|
3442
|
-
thermalState,
|
|
3443
|
-
isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
|
|
3444
|
-
deviceState.apple.isLowPowerModeEnabled,
|
|
3445
|
-
},
|
|
3520
|
+
this.on = (eventName, fn) => {
|
|
3521
|
+
var _a;
|
|
3522
|
+
((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
|
|
3523
|
+
return () => {
|
|
3524
|
+
this.off(eventName, fn);
|
|
3525
|
+
};
|
|
3446
3526
|
};
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
const setPowerState = (powerMode) => {
|
|
3450
|
-
if (!osInfo) {
|
|
3451
|
-
deviceState = { oneofKind: undefined };
|
|
3452
|
-
return;
|
|
3453
|
-
}
|
|
3454
|
-
if (osInfo.name === 'android') {
|
|
3455
|
-
deviceState = {
|
|
3456
|
-
oneofKind: 'android',
|
|
3457
|
-
android: {
|
|
3458
|
-
thermalState: deviceState?.oneofKind === 'android'
|
|
3459
|
-
? deviceState.android.thermalState
|
|
3460
|
-
: AndroidThermalState.UNSPECIFIED,
|
|
3461
|
-
isPowerSaverMode: powerMode,
|
|
3462
|
-
},
|
|
3463
|
-
};
|
|
3464
|
-
}
|
|
3465
|
-
if (osInfo.name.toLowerCase() === 'ios') {
|
|
3466
|
-
deviceState = {
|
|
3467
|
-
oneofKind: 'apple',
|
|
3468
|
-
apple: {
|
|
3469
|
-
thermalState: deviceState?.oneofKind === 'apple'
|
|
3470
|
-
? deviceState.apple.thermalState
|
|
3471
|
-
: AppleThermalState.UNSPECIFIED,
|
|
3472
|
-
isLowPowerModeEnabled: powerMode,
|
|
3473
|
-
},
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3476
|
-
};
|
|
3477
|
-
const getDeviceState = () => {
|
|
3478
|
-
return deviceState;
|
|
3479
|
-
};
|
|
3480
|
-
const getClientDetails = () => {
|
|
3481
|
-
if (isReactNative()) {
|
|
3482
|
-
// Since RN doesn't support web, sharing browser info is not required
|
|
3483
|
-
return {
|
|
3484
|
-
sdk: getSdkInfo(),
|
|
3485
|
-
os: getOSInfo(),
|
|
3486
|
-
device: getDeviceInfo(),
|
|
3487
|
-
};
|
|
3488
|
-
}
|
|
3489
|
-
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
3490
|
-
const { browser, os, device, cpu } = userAgent.getResult();
|
|
3491
|
-
return {
|
|
3492
|
-
sdk: getSdkInfo(),
|
|
3493
|
-
browser: {
|
|
3494
|
-
name: browser.name || navigator.userAgent,
|
|
3495
|
-
version: browser.version || '',
|
|
3496
|
-
},
|
|
3497
|
-
os: {
|
|
3498
|
-
name: os.name || '',
|
|
3499
|
-
version: os.version || '',
|
|
3500
|
-
architecture: cpu.architecture || '',
|
|
3501
|
-
},
|
|
3502
|
-
device: {
|
|
3503
|
-
name: [device.vendor, device.model, device.type]
|
|
3504
|
-
.filter(Boolean)
|
|
3505
|
-
.join(' '),
|
|
3506
|
-
version: '',
|
|
3507
|
-
},
|
|
3508
|
-
};
|
|
3509
|
-
};
|
|
3510
|
-
|
|
3511
|
-
/**
|
|
3512
|
-
* Checks whether the current browser is Safari.
|
|
3513
|
-
*/
|
|
3514
|
-
const isSafari = () => {
|
|
3515
|
-
if (typeof navigator === 'undefined')
|
|
3516
|
-
return false;
|
|
3517
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
|
|
3518
|
-
};
|
|
3519
|
-
/**
|
|
3520
|
-
* Checks whether the current browser is Firefox.
|
|
3521
|
-
*/
|
|
3522
|
-
const isFirefox = () => {
|
|
3523
|
-
if (typeof navigator === 'undefined')
|
|
3524
|
-
return false;
|
|
3525
|
-
return navigator.userAgent?.includes('Firefox');
|
|
3526
|
-
};
|
|
3527
|
-
/**
|
|
3528
|
-
* Checks whether the current browser is Google Chrome.
|
|
3529
|
-
*/
|
|
3530
|
-
const isChrome = () => {
|
|
3531
|
-
if (typeof navigator === 'undefined')
|
|
3532
|
-
return false;
|
|
3533
|
-
return navigator.userAgent?.includes('Chrome');
|
|
3534
|
-
};
|
|
3535
|
-
|
|
3536
|
-
var browsers = /*#__PURE__*/Object.freeze({
|
|
3537
|
-
__proto__: null,
|
|
3538
|
-
isChrome: isChrome,
|
|
3539
|
-
isFirefox: isFirefox,
|
|
3540
|
-
isSafari: isSafari
|
|
3541
|
-
});
|
|
3542
|
-
|
|
3543
|
-
/**
|
|
3544
|
-
* Returns back a list of sorted codecs, with the preferred codec first.
|
|
3545
|
-
*
|
|
3546
|
-
* @param kind the kind of codec to get.
|
|
3547
|
-
* @param preferredCodec the codec to prioritize (vp8, h264, vp9, av1...).
|
|
3548
|
-
* @param codecToRemove the codec to exclude from the list.
|
|
3549
|
-
* @param codecPreferencesSource the source of the codec preferences.
|
|
3550
|
-
*/
|
|
3551
|
-
const getPreferredCodecs = (kind, preferredCodec, codecToRemove, codecPreferencesSource) => {
|
|
3552
|
-
const source = codecPreferencesSource === 'receiver' ? RTCRtpReceiver : RTCRtpSender;
|
|
3553
|
-
if (!('getCapabilities' in source))
|
|
3554
|
-
return;
|
|
3555
|
-
const capabilities = source.getCapabilities(kind);
|
|
3556
|
-
if (!capabilities)
|
|
3557
|
-
return;
|
|
3558
|
-
const preferred = [];
|
|
3559
|
-
const partiallyPreferred = [];
|
|
3560
|
-
const unpreferred = [];
|
|
3561
|
-
const preferredCodecMimeType = `${kind}/${preferredCodec.toLowerCase()}`;
|
|
3562
|
-
const codecToRemoveMimeType = codecToRemove && `${kind}/${codecToRemove.toLowerCase()}`;
|
|
3563
|
-
for (const codec of capabilities.codecs) {
|
|
3564
|
-
const codecMimeType = codec.mimeType.toLowerCase();
|
|
3565
|
-
const shouldRemoveCodec = codecMimeType === codecToRemoveMimeType;
|
|
3566
|
-
if (shouldRemoveCodec)
|
|
3567
|
-
continue; // skip this codec
|
|
3568
|
-
const isPreferredCodec = codecMimeType === preferredCodecMimeType;
|
|
3569
|
-
if (!isPreferredCodec) {
|
|
3570
|
-
unpreferred.push(codec);
|
|
3571
|
-
continue;
|
|
3572
|
-
}
|
|
3573
|
-
// h264 is a special case, we want to prioritize the baseline codec with
|
|
3574
|
-
// profile-level-id is 42e01f and packetization-mode=0 for maximum
|
|
3575
|
-
// cross-browser compatibility.
|
|
3576
|
-
// this branch covers the other cases, such as vp8.
|
|
3577
|
-
if (codecMimeType !== 'video/h264') {
|
|
3578
|
-
preferred.push(codec);
|
|
3579
|
-
continue;
|
|
3580
|
-
}
|
|
3581
|
-
const sdpFmtpLine = codec.sdpFmtpLine;
|
|
3582
|
-
if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
|
|
3583
|
-
// this is not the baseline h264 codec, prioritize it lower
|
|
3584
|
-
partiallyPreferred.push(codec);
|
|
3585
|
-
continue;
|
|
3586
|
-
}
|
|
3587
|
-
if (sdpFmtpLine.includes('packetization-mode=1')) {
|
|
3588
|
-
preferred.unshift(codec);
|
|
3589
|
-
}
|
|
3590
|
-
else {
|
|
3591
|
-
preferred.push(codec);
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
// return a sorted list of codecs, with the preferred codecs first
|
|
3595
|
-
return [...preferred, ...partiallyPreferred, ...unpreferred];
|
|
3596
|
-
};
|
|
3597
|
-
/**
|
|
3598
|
-
* Returns a generic SDP for the given direction.
|
|
3599
|
-
* We use this SDP to send it as part of our JoinRequest so that the SFU
|
|
3600
|
-
* can use it to determine client's codec capabilities.
|
|
3601
|
-
*
|
|
3602
|
-
* @param direction the direction of the transceiver.
|
|
3603
|
-
*/
|
|
3604
|
-
const getGenericSdp = async (direction) => {
|
|
3605
|
-
const tempPc = new RTCPeerConnection();
|
|
3606
|
-
tempPc.addTransceiver('video', { direction });
|
|
3607
|
-
tempPc.addTransceiver('audio', { direction });
|
|
3608
|
-
const offer = await tempPc.createOffer();
|
|
3609
|
-
const sdp = offer.sdp ?? '';
|
|
3610
|
-
tempPc.getTransceivers().forEach((t) => {
|
|
3611
|
-
t.stop?.();
|
|
3612
|
-
});
|
|
3613
|
-
tempPc.close();
|
|
3614
|
-
return sdp;
|
|
3615
|
-
};
|
|
3616
|
-
/**
|
|
3617
|
-
* Returns the optimal video codec for the device.
|
|
3618
|
-
*/
|
|
3619
|
-
const getOptimalVideoCodec = (preferredCodec) => {
|
|
3620
|
-
if (isReactNative()) {
|
|
3621
|
-
const os = getOSInfo()?.name.toLowerCase();
|
|
3622
|
-
if (os === 'android')
|
|
3623
|
-
return preferredOr(preferredCodec, 'vp8');
|
|
3624
|
-
if (os === 'ios' || os === 'ipados') {
|
|
3625
|
-
return supportsH264Baseline() ? 'h264' : 'vp8';
|
|
3626
|
-
}
|
|
3627
|
-
return preferredOr(preferredCodec, 'h264');
|
|
3628
|
-
}
|
|
3629
|
-
if (isSafari())
|
|
3630
|
-
return 'h264';
|
|
3631
|
-
if (isFirefox())
|
|
3632
|
-
return 'vp8';
|
|
3633
|
-
return preferredOr(preferredCodec, 'vp8');
|
|
3634
|
-
};
|
|
3635
|
-
/**
|
|
3636
|
-
* Determines if the platform supports the preferred codec.
|
|
3637
|
-
* If not, it returns the fallback codec.
|
|
3638
|
-
*/
|
|
3639
|
-
const preferredOr = (codec, fallback) => {
|
|
3640
|
-
if (!codec)
|
|
3641
|
-
return fallback;
|
|
3642
|
-
if (!('getCapabilities' in RTCRtpSender))
|
|
3643
|
-
return fallback;
|
|
3644
|
-
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
3645
|
-
if (!capabilities)
|
|
3646
|
-
return fallback;
|
|
3647
|
-
// Safari and Firefox do not have a good support encoding to SVC codecs,
|
|
3648
|
-
// so we disable it for them.
|
|
3649
|
-
if (isSvcCodec(codec) && (isSafari() || isFirefox()))
|
|
3650
|
-
return fallback;
|
|
3651
|
-
const { codecs } = capabilities;
|
|
3652
|
-
const codecMimeType = `video/${codec}`.toLowerCase();
|
|
3653
|
-
return codecs.some((c) => c.mimeType.toLowerCase() === codecMimeType)
|
|
3654
|
-
? codec
|
|
3655
|
-
: fallback;
|
|
3656
|
-
};
|
|
3657
|
-
/**
|
|
3658
|
-
* Returns whether the platform supports the H264 baseline codec.
|
|
3659
|
-
*/
|
|
3660
|
-
const supportsH264Baseline = () => {
|
|
3661
|
-
if (!('getCapabilities' in RTCRtpSender))
|
|
3662
|
-
return false;
|
|
3663
|
-
const capabilities = RTCRtpSender.getCapabilities('video');
|
|
3664
|
-
if (!capabilities)
|
|
3665
|
-
return false;
|
|
3666
|
-
return capabilities.codecs.some((c) => c.mimeType.toLowerCase() === 'video/h264' &&
|
|
3667
|
-
c.sdpFmtpLine?.includes('profile-level-id=42e01f'));
|
|
3668
|
-
};
|
|
3669
|
-
/**
|
|
3670
|
-
* Returns whether the codec is an SVC codec.
|
|
3671
|
-
*
|
|
3672
|
-
* @param codecOrMimeType the codec to check.
|
|
3673
|
-
*/
|
|
3674
|
-
const isSvcCodec = (codecOrMimeType) => {
|
|
3675
|
-
if (!codecOrMimeType)
|
|
3676
|
-
return false;
|
|
3677
|
-
codecOrMimeType = codecOrMimeType.toLowerCase();
|
|
3678
|
-
return (codecOrMimeType === 'vp9' ||
|
|
3679
|
-
codecOrMimeType === 'av1' ||
|
|
3680
|
-
codecOrMimeType === 'video/vp9' ||
|
|
3681
|
-
codecOrMimeType === 'video/av1');
|
|
3682
|
-
};
|
|
3683
|
-
|
|
3684
|
-
const sfuEventKinds = {
|
|
3685
|
-
subscriberOffer: undefined,
|
|
3686
|
-
publisherAnswer: undefined,
|
|
3687
|
-
connectionQualityChanged: undefined,
|
|
3688
|
-
audioLevelChanged: undefined,
|
|
3689
|
-
iceTrickle: undefined,
|
|
3690
|
-
changePublishQuality: undefined,
|
|
3691
|
-
participantJoined: undefined,
|
|
3692
|
-
participantLeft: undefined,
|
|
3693
|
-
dominantSpeakerChanged: undefined,
|
|
3694
|
-
joinResponse: undefined,
|
|
3695
|
-
healthCheckResponse: undefined,
|
|
3696
|
-
trackPublished: undefined,
|
|
3697
|
-
trackUnpublished: undefined,
|
|
3698
|
-
error: undefined,
|
|
3699
|
-
callGrantsUpdated: undefined,
|
|
3700
|
-
goAway: undefined,
|
|
3701
|
-
iceRestart: undefined,
|
|
3702
|
-
pinsUpdated: undefined,
|
|
3703
|
-
callEnded: undefined,
|
|
3704
|
-
participantUpdated: undefined,
|
|
3705
|
-
participantMigrationComplete: undefined,
|
|
3706
|
-
codecNegotiationComplete: undefined,
|
|
3707
|
-
changePublishOptions: undefined,
|
|
3708
|
-
};
|
|
3709
|
-
const isSfuEvent = (eventName) => {
|
|
3710
|
-
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
3711
|
-
};
|
|
3712
|
-
class Dispatcher {
|
|
3713
|
-
constructor() {
|
|
3714
|
-
this.logger = getLogger(['Dispatcher']);
|
|
3715
|
-
this.subscribers = {};
|
|
3716
|
-
this.dispatch = (message, logTag = '0') => {
|
|
3717
|
-
const eventKind = message.eventPayload.oneofKind;
|
|
3718
|
-
if (!eventKind)
|
|
3719
|
-
return;
|
|
3720
|
-
const payload = message.eventPayload[eventKind];
|
|
3721
|
-
this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
|
|
3722
|
-
const listeners = this.subscribers[eventKind];
|
|
3723
|
-
if (!listeners)
|
|
3724
|
-
return;
|
|
3725
|
-
for (const fn of listeners) {
|
|
3726
|
-
try {
|
|
3727
|
-
fn(payload);
|
|
3728
|
-
}
|
|
3729
|
-
catch (e) {
|
|
3730
|
-
this.logger('warn', 'Listener failed with error', e);
|
|
3731
|
-
}
|
|
3732
|
-
}
|
|
3733
|
-
};
|
|
3734
|
-
this.on = (eventName, fn) => {
|
|
3735
|
-
var _a;
|
|
3736
|
-
((_a = this.subscribers)[eventName] ?? (_a[eventName] = [])).push(fn);
|
|
3737
|
-
return () => {
|
|
3738
|
-
this.off(eventName, fn);
|
|
3739
|
-
};
|
|
3740
|
-
};
|
|
3741
|
-
this.off = (eventName, fn) => {
|
|
3742
|
-
this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
|
|
3743
|
-
};
|
|
3744
|
-
this.offAll = (eventName) => {
|
|
3745
|
-
if (eventName) {
|
|
3746
|
-
this.subscribers[eventName] = [];
|
|
3747
|
-
}
|
|
3748
|
-
else {
|
|
3749
|
-
this.subscribers = {};
|
|
3750
|
-
}
|
|
3527
|
+
this.off = (eventName, fn) => {
|
|
3528
|
+
this.subscribers[eventName] = (this.subscribers[eventName] || []).filter((f) => f !== fn);
|
|
3751
3529
|
};
|
|
3752
3530
|
}
|
|
3753
3531
|
}
|
|
@@ -3761,285 +3539,35 @@ class IceTrickleBuffer {
|
|
|
3761
3539
|
this.subscriberCandidates = new rxjs.ReplaySubject();
|
|
3762
3540
|
this.publisherCandidates = new rxjs.ReplaySubject();
|
|
3763
3541
|
this.push = (iceTrickle) => {
|
|
3542
|
+
const iceCandidate = toIceCandidate(iceTrickle);
|
|
3543
|
+
if (!iceCandidate)
|
|
3544
|
+
return;
|
|
3764
3545
|
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
3765
|
-
this.subscriberCandidates.next(
|
|
3546
|
+
this.subscriberCandidates.next(iceCandidate);
|
|
3766
3547
|
}
|
|
3767
3548
|
else if (iceTrickle.peerType === PeerType.PUBLISHER_UNSPECIFIED) {
|
|
3768
|
-
this.publisherCandidates.next(
|
|
3549
|
+
this.publisherCandidates.next(iceCandidate);
|
|
3769
3550
|
}
|
|
3770
3551
|
else {
|
|
3771
3552
|
const logger = getLogger(['sfu-client']);
|
|
3772
3553
|
logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
3773
3554
|
}
|
|
3774
3555
|
};
|
|
3556
|
+
this.dispose = () => {
|
|
3557
|
+
this.subscriberCandidates.complete();
|
|
3558
|
+
this.publisherCandidates.complete();
|
|
3559
|
+
};
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
const toIceCandidate = (iceTrickle) => {
|
|
3563
|
+
try {
|
|
3564
|
+
return JSON.parse(iceTrickle.iceCandidate);
|
|
3565
|
+
}
|
|
3566
|
+
catch (e) {
|
|
3567
|
+
const logger = getLogger(['sfu-client']);
|
|
3568
|
+
logger('error', `Failed to parse ICE Trickle`, e, iceTrickle);
|
|
3569
|
+
return undefined;
|
|
3775
3570
|
}
|
|
3776
|
-
}
|
|
3777
|
-
|
|
3778
|
-
function getIceCandidate(candidate) {
|
|
3779
|
-
if (!candidate.usernameFragment) {
|
|
3780
|
-
// react-native-webrtc doesn't include usernameFragment in the candidate
|
|
3781
|
-
const splittedCandidate = candidate.candidate.split(' ');
|
|
3782
|
-
const ufragIndex = splittedCandidate.findIndex((s) => s === 'ufrag') + 1;
|
|
3783
|
-
const usernameFragment = splittedCandidate[ufragIndex];
|
|
3784
|
-
return JSON.stringify({ ...candidate, usernameFragment });
|
|
3785
|
-
}
|
|
3786
|
-
else {
|
|
3787
|
-
return JSON.stringify(candidate.toJSON());
|
|
3788
|
-
}
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
const bitrateLookupTable = {
|
|
3792
|
-
h264: {
|
|
3793
|
-
2160: 5000000,
|
|
3794
|
-
1440: 3000000,
|
|
3795
|
-
1080: 2000000,
|
|
3796
|
-
720: 1250000,
|
|
3797
|
-
540: 750000,
|
|
3798
|
-
360: 400000,
|
|
3799
|
-
default: 1250000,
|
|
3800
|
-
},
|
|
3801
|
-
vp8: {
|
|
3802
|
-
2160: 5000000,
|
|
3803
|
-
1440: 2750000,
|
|
3804
|
-
1080: 2000000,
|
|
3805
|
-
720: 1250000,
|
|
3806
|
-
540: 600000,
|
|
3807
|
-
360: 350000,
|
|
3808
|
-
default: 1250000,
|
|
3809
|
-
},
|
|
3810
|
-
vp9: {
|
|
3811
|
-
2160: 3000000,
|
|
3812
|
-
1440: 2000000,
|
|
3813
|
-
1080: 1500000,
|
|
3814
|
-
720: 1250000,
|
|
3815
|
-
540: 500000,
|
|
3816
|
-
360: 275000,
|
|
3817
|
-
default: 1250000,
|
|
3818
|
-
},
|
|
3819
|
-
av1: {
|
|
3820
|
-
2160: 2000000,
|
|
3821
|
-
1440: 1550000,
|
|
3822
|
-
1080: 1000000,
|
|
3823
|
-
720: 600000,
|
|
3824
|
-
540: 350000,
|
|
3825
|
-
360: 200000,
|
|
3826
|
-
default: 600000,
|
|
3827
|
-
},
|
|
3828
|
-
};
|
|
3829
|
-
const getOptimalBitrate = (codec, frameHeight) => {
|
|
3830
|
-
const codecLookup = bitrateLookupTable[codec];
|
|
3831
|
-
if (!codecLookup)
|
|
3832
|
-
throw new Error(`Unknown codec: ${codec}`);
|
|
3833
|
-
let bitrate = codecLookup[frameHeight];
|
|
3834
|
-
if (!bitrate) {
|
|
3835
|
-
const keys = Object.keys(codecLookup).map(Number);
|
|
3836
|
-
const nearest = keys.reduce((a, b) => Math.abs(b - frameHeight) < Math.abs(a - frameHeight) ? b : a);
|
|
3837
|
-
bitrate = codecLookup[nearest];
|
|
3838
|
-
}
|
|
3839
|
-
return bitrate ?? codecLookup.default;
|
|
3840
|
-
};
|
|
3841
|
-
|
|
3842
|
-
const DEFAULT_BITRATE = 1250000;
|
|
3843
|
-
const defaultTargetResolution = {
|
|
3844
|
-
bitrate: DEFAULT_BITRATE,
|
|
3845
|
-
width: 1280,
|
|
3846
|
-
height: 720,
|
|
3847
|
-
};
|
|
3848
|
-
const defaultBitratePerRid = {
|
|
3849
|
-
q: 300000,
|
|
3850
|
-
h: 750000,
|
|
3851
|
-
f: DEFAULT_BITRATE,
|
|
3852
|
-
};
|
|
3853
|
-
/**
|
|
3854
|
-
* In SVC, we need to send only one video encoding (layer).
|
|
3855
|
-
* this layer will have the additional spatial and temporal layers
|
|
3856
|
-
* defined via the scalabilityMode property.
|
|
3857
|
-
*
|
|
3858
|
-
* @param layers the layers to process.
|
|
3859
|
-
*/
|
|
3860
|
-
const toSvcEncodings = (layers) => {
|
|
3861
|
-
// we take the `f` layer, and we rename it to `q`.
|
|
3862
|
-
return layers?.filter((l) => l.rid === 'f').map((l) => ({ ...l, rid: 'q' }));
|
|
3863
|
-
};
|
|
3864
|
-
/**
|
|
3865
|
-
* Converts the rid to a video quality.
|
|
3866
|
-
*/
|
|
3867
|
-
const ridToVideoQuality = (rid) => {
|
|
3868
|
-
return rid === 'q'
|
|
3869
|
-
? VideoQuality.LOW_UNSPECIFIED
|
|
3870
|
-
: rid === 'h'
|
|
3871
|
-
? VideoQuality.MID
|
|
3872
|
-
: VideoQuality.HIGH; // default to HIGH
|
|
3873
|
-
};
|
|
3874
|
-
/**
|
|
3875
|
-
* Determines the most optimal video layers for simulcasting
|
|
3876
|
-
* for the given track.
|
|
3877
|
-
*
|
|
3878
|
-
* @param videoTrack the video track to find optimal layers for.
|
|
3879
|
-
* @param targetResolution the expected target resolution.
|
|
3880
|
-
* @param codecInUse the codec in use.
|
|
3881
|
-
* @param publishOptions the publish options for the track.
|
|
3882
|
-
*/
|
|
3883
|
-
const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetResolution, codecInUse, publishOptions) => {
|
|
3884
|
-
const optimalVideoLayers = [];
|
|
3885
|
-
const settings = videoTrack.getSettings();
|
|
3886
|
-
const { width = 0, height = 0 } = settings;
|
|
3887
|
-
const { scalabilityMode, bitrateDownscaleFactor = 2, maxSimulcastLayers = 3, } = publishOptions || {};
|
|
3888
|
-
const maxBitrate = getComputedMaxBitrate(targetResolution, width, height, codecInUse, publishOptions);
|
|
3889
|
-
let downscaleFactor = 1;
|
|
3890
|
-
let bitrateFactor = 1;
|
|
3891
|
-
const svcCodec = isSvcCodec(codecInUse);
|
|
3892
|
-
const totalLayers = svcCodec ? 3 : Math.min(3, maxSimulcastLayers);
|
|
3893
|
-
for (const rid of ['f', 'h', 'q'].slice(0, totalLayers)) {
|
|
3894
|
-
const layer = {
|
|
3895
|
-
active: true,
|
|
3896
|
-
rid,
|
|
3897
|
-
width: Math.round(width / downscaleFactor),
|
|
3898
|
-
height: Math.round(height / downscaleFactor),
|
|
3899
|
-
maxBitrate: Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
|
|
3900
|
-
maxFramerate: 30,
|
|
3901
|
-
};
|
|
3902
|
-
if (svcCodec) {
|
|
3903
|
-
// for SVC codecs, we need to set the scalability mode, and the
|
|
3904
|
-
// codec will handle the rest (layers, temporal layers, etc.)
|
|
3905
|
-
layer.scalabilityMode = scalabilityMode || 'L3T2_KEY';
|
|
3906
|
-
}
|
|
3907
|
-
else {
|
|
3908
|
-
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
3909
|
-
layer.scaleResolutionDownBy = downscaleFactor;
|
|
3910
|
-
}
|
|
3911
|
-
downscaleFactor *= 2;
|
|
3912
|
-
bitrateFactor *= bitrateDownscaleFactor;
|
|
3913
|
-
// Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
|
|
3914
|
-
// when deciding which layer to disable when CPU or bandwidth is constrained.
|
|
3915
|
-
// Encodings should be ordered in increasing spatial resolution order.
|
|
3916
|
-
optimalVideoLayers.unshift(layer);
|
|
3917
|
-
}
|
|
3918
|
-
// for simplicity, we start with all layers enabled, then this function
|
|
3919
|
-
// will clear/reassign the layers that are not needed
|
|
3920
|
-
return withSimulcastConstraints(settings, optimalVideoLayers);
|
|
3921
|
-
};
|
|
3922
|
-
/**
|
|
3923
|
-
* Computes the maximum bitrate for a given resolution.
|
|
3924
|
-
* If the current resolution is lower than the target resolution,
|
|
3925
|
-
* we want to proportionally reduce the target bitrate.
|
|
3926
|
-
* If the current resolution is higher than the target resolution,
|
|
3927
|
-
* we want to use the target bitrate.
|
|
3928
|
-
*
|
|
3929
|
-
* @param targetResolution the target resolution.
|
|
3930
|
-
* @param currentWidth the current width of the track.
|
|
3931
|
-
* @param currentHeight the current height of the track.
|
|
3932
|
-
* @param codecInUse the codec in use.
|
|
3933
|
-
* @param publishOptions the publish options.
|
|
3934
|
-
*/
|
|
3935
|
-
const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, codecInUse, publishOptions) => {
|
|
3936
|
-
// if the current resolution is lower than the target resolution,
|
|
3937
|
-
// we want to proportionally reduce the target bitrate
|
|
3938
|
-
const { width: targetWidth, height: targetHeight, bitrate: targetBitrate, } = targetResolution;
|
|
3939
|
-
const { preferredBitrate } = publishOptions || {};
|
|
3940
|
-
const frameHeight = currentWidth > currentHeight ? currentHeight : currentWidth;
|
|
3941
|
-
const bitrate = preferredBitrate ||
|
|
3942
|
-
(codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
|
|
3943
|
-
if (currentWidth < targetWidth || currentHeight < targetHeight) {
|
|
3944
|
-
const currentPixels = currentWidth * currentHeight;
|
|
3945
|
-
const targetPixels = targetWidth * targetHeight;
|
|
3946
|
-
const reductionFactor = currentPixels / targetPixels;
|
|
3947
|
-
return Math.round(bitrate * reductionFactor);
|
|
3948
|
-
}
|
|
3949
|
-
return bitrate;
|
|
3950
|
-
};
|
|
3951
|
-
/**
|
|
3952
|
-
* Browsers have different simulcast constraints for different video resolutions.
|
|
3953
|
-
*
|
|
3954
|
-
* This function modifies the provided list of video layers according to the
|
|
3955
|
-
* current implementation of simulcast constraints in the Chromium based browsers.
|
|
3956
|
-
*
|
|
3957
|
-
* https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
|
|
3958
|
-
*/
|
|
3959
|
-
const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
3960
|
-
let layers;
|
|
3961
|
-
const size = Math.max(settings.width || 0, settings.height || 0);
|
|
3962
|
-
if (size <= 320) {
|
|
3963
|
-
// provide only one layer 320x240 (q), the one with the highest quality
|
|
3964
|
-
layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
|
|
3965
|
-
}
|
|
3966
|
-
else if (size <= 640) {
|
|
3967
|
-
// provide two layers, 160x120 (q) and 640x480 (h)
|
|
3968
|
-
layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
|
|
3969
|
-
}
|
|
3970
|
-
else {
|
|
3971
|
-
// provide three layers for sizes > 640x480
|
|
3972
|
-
layers = optimalVideoLayers;
|
|
3973
|
-
}
|
|
3974
|
-
const ridMapping = ['q', 'h', 'f'];
|
|
3975
|
-
return layers.map((layer, index) => ({
|
|
3976
|
-
...layer,
|
|
3977
|
-
rid: ridMapping[index], // reassign rid
|
|
3978
|
-
}));
|
|
3979
|
-
};
|
|
3980
|
-
const findOptimalScreenSharingLayers = (videoTrack, publishOptions, defaultMaxBitrate = 3000000) => {
|
|
3981
|
-
const { screenShareSettings: preferences } = publishOptions || {};
|
|
3982
|
-
const settings = videoTrack.getSettings();
|
|
3983
|
-
return [
|
|
3984
|
-
{
|
|
3985
|
-
active: true,
|
|
3986
|
-
rid: 'q', // single track, start from 'q'
|
|
3987
|
-
width: settings.width || 0,
|
|
3988
|
-
height: settings.height || 0,
|
|
3989
|
-
scaleResolutionDownBy: 1,
|
|
3990
|
-
maxBitrate: preferences?.maxBitrate ?? defaultMaxBitrate,
|
|
3991
|
-
maxFramerate: preferences?.maxFramerate ?? 30,
|
|
3992
|
-
},
|
|
3993
|
-
];
|
|
3994
|
-
};
|
|
3995
|
-
|
|
3996
|
-
const ensureExhausted = (x, message) => {
|
|
3997
|
-
getLogger(['helpers'])('warn', message, x);
|
|
3998
|
-
};
|
|
3999
|
-
|
|
4000
|
-
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
4001
|
-
switch (trackType) {
|
|
4002
|
-
case TrackType.SCREEN_SHARE:
|
|
4003
|
-
return 'screenShareStream';
|
|
4004
|
-
case TrackType.SCREEN_SHARE_AUDIO:
|
|
4005
|
-
return 'screenShareAudioStream';
|
|
4006
|
-
case TrackType.VIDEO:
|
|
4007
|
-
return 'videoStream';
|
|
4008
|
-
case TrackType.AUDIO:
|
|
4009
|
-
return 'audioStream';
|
|
4010
|
-
case TrackType.UNSPECIFIED:
|
|
4011
|
-
throw new Error('Track type is unspecified');
|
|
4012
|
-
default:
|
|
4013
|
-
ensureExhausted(trackType, 'Unknown track type');
|
|
4014
|
-
}
|
|
4015
|
-
};
|
|
4016
|
-
const muteTypeToTrackType = (muteType) => {
|
|
4017
|
-
switch (muteType) {
|
|
4018
|
-
case 'audio':
|
|
4019
|
-
return TrackType.AUDIO;
|
|
4020
|
-
case 'video':
|
|
4021
|
-
return TrackType.VIDEO;
|
|
4022
|
-
case 'screenshare':
|
|
4023
|
-
return TrackType.SCREEN_SHARE;
|
|
4024
|
-
case 'screenshare_audio':
|
|
4025
|
-
return TrackType.SCREEN_SHARE_AUDIO;
|
|
4026
|
-
default:
|
|
4027
|
-
ensureExhausted(muteType, 'Unknown mute type');
|
|
4028
|
-
}
|
|
4029
|
-
};
|
|
4030
|
-
const toTrackType = (trackType) => {
|
|
4031
|
-
switch (trackType) {
|
|
4032
|
-
case 'TRACK_TYPE_AUDIO':
|
|
4033
|
-
return TrackType.AUDIO;
|
|
4034
|
-
case 'TRACK_TYPE_VIDEO':
|
|
4035
|
-
return TrackType.VIDEO;
|
|
4036
|
-
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
4037
|
-
return TrackType.SCREEN_SHARE;
|
|
4038
|
-
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
4039
|
-
return TrackType.SCREEN_SHARE_AUDIO;
|
|
4040
|
-
default:
|
|
4041
|
-
return undefined;
|
|
4042
|
-
}
|
|
4043
3571
|
};
|
|
4044
3572
|
|
|
4045
3573
|
/**
|
|
@@ -5606,198 +5134,446 @@ class CallState {
|
|
|
5606
5134
|
}
|
|
5607
5135
|
}
|
|
5608
5136
|
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5137
|
+
/**
|
|
5138
|
+
* A base class for the `Publisher` and `Subscriber` classes.
|
|
5139
|
+
* @internal
|
|
5140
|
+
*/
|
|
5141
|
+
class BasePeerConnection {
|
|
5142
|
+
/**
|
|
5143
|
+
* Constructs a new `BasePeerConnection` instance.
|
|
5144
|
+
*/
|
|
5145
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, }) {
|
|
5146
|
+
this.isIceRestarting = false;
|
|
5147
|
+
this.subscriptions = [];
|
|
5148
|
+
/**
|
|
5149
|
+
* Disposes the `RTCPeerConnection` instance.
|
|
5150
|
+
*/
|
|
5151
|
+
this.dispose = () => {
|
|
5152
|
+
this.detachEventHandlers();
|
|
5153
|
+
this.pc.close();
|
|
5154
|
+
};
|
|
5155
|
+
/**
|
|
5156
|
+
* Handles events synchronously.
|
|
5157
|
+
* Consecutive events are queued and executed one after the other.
|
|
5158
|
+
*/
|
|
5159
|
+
this.on = (event, fn) => {
|
|
5160
|
+
this.subscriptions.push(this.dispatcher.on(event, (e) => {
|
|
5161
|
+
withoutConcurrency(`pc.${event}`, async () => fn(e)).catch((err) => {
|
|
5162
|
+
this.logger('warn', `Error handling ${event}`, err);
|
|
5163
|
+
});
|
|
5164
|
+
}));
|
|
5165
|
+
};
|
|
5166
|
+
/**
|
|
5167
|
+
* Appends the trickled ICE candidates to the `RTCPeerConnection`.
|
|
5168
|
+
*/
|
|
5169
|
+
this.addTrickledIceCandidates = () => {
|
|
5170
|
+
const { iceTrickleBuffer } = this.sfuClient;
|
|
5171
|
+
const observable = this.peerType === PeerType.SUBSCRIBER
|
|
5172
|
+
? iceTrickleBuffer.subscriberCandidates
|
|
5173
|
+
: iceTrickleBuffer.publisherCandidates;
|
|
5174
|
+
this.unsubscribeIceTrickle?.();
|
|
5175
|
+
this.unsubscribeIceTrickle = createSafeAsyncSubscription(observable, async (candidate) => {
|
|
5176
|
+
return this.pc.addIceCandidate(candidate).catch((e) => {
|
|
5177
|
+
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
5178
|
+
});
|
|
5179
|
+
});
|
|
5180
|
+
};
|
|
5181
|
+
/**
|
|
5182
|
+
* Sets the SFU client to use.
|
|
5183
|
+
*
|
|
5184
|
+
* @param sfuClient the SFU client to use.
|
|
5185
|
+
*/
|
|
5186
|
+
this.setSfuClient = (sfuClient) => {
|
|
5187
|
+
this.sfuClient = sfuClient;
|
|
5188
|
+
};
|
|
5189
|
+
/**
|
|
5190
|
+
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
5191
|
+
* @param selector an optional `MediaStreamTrack` to get the stats for.
|
|
5192
|
+
*/
|
|
5193
|
+
this.getStats = (selector) => {
|
|
5194
|
+
return this.pc.getStats(selector);
|
|
5195
|
+
};
|
|
5196
|
+
/**
|
|
5197
|
+
* Handles the ICECandidate event and
|
|
5198
|
+
* Initiates an ICE Trickle process with the SFU.
|
|
5199
|
+
*/
|
|
5200
|
+
this.onIceCandidate = (e) => {
|
|
5201
|
+
const { candidate } = e;
|
|
5202
|
+
if (!candidate) {
|
|
5203
|
+
this.logger('debug', 'null ice candidate');
|
|
5204
|
+
return;
|
|
5205
|
+
}
|
|
5206
|
+
const iceCandidate = this.toJSON(candidate);
|
|
5207
|
+
this.sfuClient
|
|
5208
|
+
.iceTrickle({ peerType: this.peerType, iceCandidate })
|
|
5209
|
+
.catch((err) => this.logger('warn', `ICETrickle failed`, err));
|
|
5210
|
+
};
|
|
5211
|
+
/**
|
|
5212
|
+
* Converts the ICE candidate to a JSON string.
|
|
5213
|
+
*/
|
|
5214
|
+
this.toJSON = (candidate) => {
|
|
5215
|
+
if (!candidate.usernameFragment) {
|
|
5216
|
+
// react-native-webrtc doesn't include usernameFragment in the candidate
|
|
5217
|
+
const segments = candidate.candidate.split(' ');
|
|
5218
|
+
const ufragIndex = segments.findIndex((s) => s === 'ufrag') + 1;
|
|
5219
|
+
const usernameFragment = segments[ufragIndex];
|
|
5220
|
+
return JSON.stringify({ ...candidate, usernameFragment });
|
|
5221
|
+
}
|
|
5222
|
+
return JSON.stringify(candidate.toJSON());
|
|
5223
|
+
};
|
|
5224
|
+
/**
|
|
5225
|
+
* Handles the ICE connection state change event.
|
|
5226
|
+
*/
|
|
5227
|
+
this.onIceConnectionStateChange = () => {
|
|
5228
|
+
const state = this.pc.iceConnectionState;
|
|
5229
|
+
this.logger('debug', `ICE connection state changed`, state);
|
|
5230
|
+
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
5231
|
+
return;
|
|
5232
|
+
// do nothing when ICE is restarting
|
|
5233
|
+
if (this.isIceRestarting)
|
|
5234
|
+
return;
|
|
5235
|
+
if (state === 'failed' || state === 'disconnected') {
|
|
5236
|
+
this.logger('debug', `Attempting to restart ICE`);
|
|
5237
|
+
this.restartIce().catch((e) => {
|
|
5238
|
+
this.logger('error', `ICE restart failed`, e);
|
|
5239
|
+
this.onUnrecoverableError?.();
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
/**
|
|
5244
|
+
* Handles the ICE candidate error event.
|
|
5245
|
+
*/
|
|
5246
|
+
this.onIceCandidateError = (e) => {
|
|
5247
|
+
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
5248
|
+
`${e.errorCode}: ${e.errorText}`;
|
|
5249
|
+
const iceState = this.pc.iceConnectionState;
|
|
5250
|
+
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
5251
|
+
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
5252
|
+
};
|
|
5253
|
+
/**
|
|
5254
|
+
* Handles the ICE gathering state change event.
|
|
5255
|
+
*/
|
|
5256
|
+
this.onIceGatherChange = () => {
|
|
5257
|
+
this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
5258
|
+
};
|
|
5259
|
+
/**
|
|
5260
|
+
* Handles the signaling state change event.
|
|
5261
|
+
*/
|
|
5262
|
+
this.onSignalingChange = () => {
|
|
5263
|
+
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
5619
5264
|
};
|
|
5265
|
+
this.peerType = peerType;
|
|
5266
|
+
this.sfuClient = sfuClient;
|
|
5267
|
+
this.state = state;
|
|
5268
|
+
this.dispatcher = dispatcher;
|
|
5269
|
+
this.onUnrecoverableError = onUnrecoverableError;
|
|
5270
|
+
this.logger = getLogger([
|
|
5271
|
+
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
5272
|
+
logTag,
|
|
5273
|
+
]);
|
|
5274
|
+
this.pc = new RTCPeerConnection(connectionConfig);
|
|
5275
|
+
this.pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
5276
|
+
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5277
|
+
this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5278
|
+
this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
5279
|
+
this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
|
|
5620
5280
|
}
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5281
|
+
/**
|
|
5282
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5283
|
+
*/
|
|
5284
|
+
detachEventHandlers() {
|
|
5285
|
+
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
5286
|
+
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5287
|
+
this.pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
5288
|
+
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5289
|
+
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
5290
|
+
this.unsubscribeIceTrickle?.();
|
|
5291
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
class TransceiverCache {
|
|
5296
|
+
constructor() {
|
|
5297
|
+
this.cache = [];
|
|
5298
|
+
this.layers = [];
|
|
5299
|
+
/**
|
|
5300
|
+
* An array maintaining the order how transceivers were added to the peer connection.
|
|
5301
|
+
* This is needed because some browsers (Firefox) don't reliably report
|
|
5302
|
+
* trackId and `mid` parameters.
|
|
5303
|
+
*/
|
|
5304
|
+
this.transceiverOrder = [];
|
|
5305
|
+
/**
|
|
5306
|
+
* Adds a transceiver to the cache.
|
|
5307
|
+
*/
|
|
5308
|
+
this.add = (publishOption, transceiver) => {
|
|
5309
|
+
this.cache.push({ publishOption, transceiver });
|
|
5310
|
+
this.transceiverOrder.push(transceiver);
|
|
5311
|
+
};
|
|
5312
|
+
/**
|
|
5313
|
+
* Gets the transceiver for the given publish option.
|
|
5314
|
+
*/
|
|
5315
|
+
this.get = (publishOption) => {
|
|
5316
|
+
return this.findTransceiver(publishOption)?.transceiver;
|
|
5317
|
+
};
|
|
5318
|
+
/**
|
|
5319
|
+
* Gets the last transceiver for the given track type and publish option id.
|
|
5320
|
+
*/
|
|
5321
|
+
this.getWith = (trackType, id) => {
|
|
5322
|
+
return this.findTransceiver({ trackType, id })?.transceiver;
|
|
5323
|
+
};
|
|
5324
|
+
/**
|
|
5325
|
+
* Checks if the cache has the given publish option.
|
|
5326
|
+
*/
|
|
5327
|
+
this.has = (publishOption) => {
|
|
5328
|
+
return !!this.get(publishOption);
|
|
5329
|
+
};
|
|
5330
|
+
/**
|
|
5331
|
+
* Finds the first transceiver that satisfies the given predicate.
|
|
5332
|
+
*/
|
|
5333
|
+
this.find = (predicate) => {
|
|
5334
|
+
return this.cache.find(predicate);
|
|
5335
|
+
};
|
|
5336
|
+
/**
|
|
5337
|
+
* Provides all the items in the cache.
|
|
5338
|
+
*/
|
|
5339
|
+
this.items = () => {
|
|
5340
|
+
return this.cache;
|
|
5632
5341
|
};
|
|
5342
|
+
/**
|
|
5343
|
+
* Init index of the transceiver in the cache.
|
|
5344
|
+
*/
|
|
5345
|
+
this.indexOf = (transceiver) => {
|
|
5346
|
+
return this.transceiverOrder.indexOf(transceiver);
|
|
5347
|
+
};
|
|
5348
|
+
/**
|
|
5349
|
+
* Gets cached video layers for the given track.
|
|
5350
|
+
*/
|
|
5351
|
+
this.getLayers = (publishOption) => {
|
|
5352
|
+
const entry = this.layers.find((item) => item.publishOption.id === publishOption.id &&
|
|
5353
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5354
|
+
return entry?.layers;
|
|
5355
|
+
};
|
|
5356
|
+
/**
|
|
5357
|
+
* Sets the video layers for the given track.
|
|
5358
|
+
*/
|
|
5359
|
+
this.setLayers = (publishOption, layers = []) => {
|
|
5360
|
+
const entry = this.findLayer(publishOption);
|
|
5361
|
+
if (entry) {
|
|
5362
|
+
entry.layers = layers;
|
|
5363
|
+
}
|
|
5364
|
+
else {
|
|
5365
|
+
this.layers.push({ publishOption, layers });
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
this.findTransceiver = (publishOption) => {
|
|
5369
|
+
return this.cache.find((item) => item.publishOption.id === publishOption.id &&
|
|
5370
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5371
|
+
};
|
|
5372
|
+
this.findLayer = (publishOption) => {
|
|
5373
|
+
return this.layers.find((item) => item.publishOption.id === publishOption.id &&
|
|
5374
|
+
item.publishOption.trackType === publishOption.trackType);
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
const ensureExhausted = (x, message) => {
|
|
5380
|
+
getLogger(['helpers'])('warn', message, x);
|
|
5381
|
+
};
|
|
5382
|
+
|
|
5383
|
+
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
5384
|
+
switch (trackType) {
|
|
5385
|
+
case TrackType.SCREEN_SHARE:
|
|
5386
|
+
return 'screenShareStream';
|
|
5387
|
+
case TrackType.SCREEN_SHARE_AUDIO:
|
|
5388
|
+
return 'screenShareAudioStream';
|
|
5389
|
+
case TrackType.VIDEO:
|
|
5390
|
+
return 'videoStream';
|
|
5391
|
+
case TrackType.AUDIO:
|
|
5392
|
+
return 'audioStream';
|
|
5393
|
+
case TrackType.UNSPECIFIED:
|
|
5394
|
+
throw new Error('Track type is unspecified');
|
|
5395
|
+
default:
|
|
5396
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
5397
|
+
}
|
|
5398
|
+
};
|
|
5399
|
+
const muteTypeToTrackType = (muteType) => {
|
|
5400
|
+
switch (muteType) {
|
|
5401
|
+
case 'audio':
|
|
5402
|
+
return TrackType.AUDIO;
|
|
5403
|
+
case 'video':
|
|
5404
|
+
return TrackType.VIDEO;
|
|
5405
|
+
case 'screenshare':
|
|
5406
|
+
return TrackType.SCREEN_SHARE;
|
|
5407
|
+
case 'screenshare_audio':
|
|
5408
|
+
return TrackType.SCREEN_SHARE_AUDIO;
|
|
5409
|
+
default:
|
|
5410
|
+
ensureExhausted(muteType, 'Unknown mute type');
|
|
5411
|
+
}
|
|
5412
|
+
};
|
|
5413
|
+
const toTrackType = (trackType) => {
|
|
5414
|
+
switch (trackType) {
|
|
5415
|
+
case 'TRACK_TYPE_AUDIO':
|
|
5416
|
+
return TrackType.AUDIO;
|
|
5417
|
+
case 'TRACK_TYPE_VIDEO':
|
|
5418
|
+
return TrackType.VIDEO;
|
|
5419
|
+
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
5420
|
+
return TrackType.SCREEN_SHARE;
|
|
5421
|
+
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
5422
|
+
return TrackType.SCREEN_SHARE_AUDIO;
|
|
5423
|
+
default:
|
|
5424
|
+
return undefined;
|
|
5633
5425
|
}
|
|
5634
5426
|
};
|
|
5427
|
+
const isAudioTrackType = (trackType) => trackType === TrackType.AUDIO || trackType === TrackType.SCREEN_SHARE_AUDIO;
|
|
5428
|
+
|
|
5429
|
+
const defaultBitratePerRid = {
|
|
5430
|
+
q: 300000,
|
|
5431
|
+
h: 750000,
|
|
5432
|
+
f: 1250000,
|
|
5433
|
+
};
|
|
5635
5434
|
/**
|
|
5636
|
-
*
|
|
5637
|
-
*
|
|
5638
|
-
*
|
|
5435
|
+
* In SVC, we need to send only one video encoding (layer).
|
|
5436
|
+
* this layer will have the additional spatial and temporal layers
|
|
5437
|
+
* defined via the scalabilityMode property.
|
|
5438
|
+
*
|
|
5439
|
+
* @param layers the layers to process.
|
|
5639
5440
|
*/
|
|
5640
|
-
const
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
}
|
|
5441
|
+
const toSvcEncodings = (layers) => {
|
|
5442
|
+
if (!layers)
|
|
5443
|
+
return;
|
|
5444
|
+
// we take the highest quality layer, and we assign it to `q` encoder.
|
|
5445
|
+
const withRid = (rid) => (l) => l.rid === rid;
|
|
5446
|
+
const highestLayer = layers.find(withRid('f')) ||
|
|
5447
|
+
layers.find(withRid('h')) ||
|
|
5448
|
+
layers.find(withRid('q'));
|
|
5449
|
+
return [{ ...highestLayer, rid: 'q' }];
|
|
5650
5450
|
};
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5451
|
+
/**
|
|
5452
|
+
* Converts the rid to a video quality.
|
|
5453
|
+
*/
|
|
5454
|
+
const ridToVideoQuality = (rid) => {
|
|
5455
|
+
return rid === 'q'
|
|
5456
|
+
? VideoQuality.LOW_UNSPECIFIED
|
|
5457
|
+
: rid === 'h'
|
|
5458
|
+
? VideoQuality.MID
|
|
5459
|
+
: VideoQuality.HIGH; // default to HIGH
|
|
5460
|
+
};
|
|
5461
|
+
/**
|
|
5462
|
+
* Converts the given video layers to SFU video layers.
|
|
5463
|
+
*/
|
|
5464
|
+
const toVideoLayers = (layers = []) => {
|
|
5465
|
+
return layers.map((layer) => ({
|
|
5466
|
+
rid: layer.rid || '',
|
|
5467
|
+
bitrate: layer.maxBitrate || 0,
|
|
5468
|
+
fps: layer.maxFramerate || 0,
|
|
5469
|
+
quality: ridToVideoQuality(layer.rid || ''),
|
|
5470
|
+
videoDimension: { width: layer.width, height: layer.height },
|
|
5471
|
+
}));
|
|
5472
|
+
};
|
|
5473
|
+
/**
|
|
5474
|
+
* Converts the spatial and temporal layers to a scalability mode.
|
|
5475
|
+
*/
|
|
5476
|
+
const toScalabilityMode = (spatialLayers, temporalLayers) => `L${spatialLayers}T${temporalLayers}${spatialLayers > 1 ? '_KEY' : ''}`;
|
|
5477
|
+
/**
|
|
5478
|
+
* Determines the most optimal video layers for the given track.
|
|
5479
|
+
*
|
|
5480
|
+
* @param videoTrack the video track to find optimal layers for.
|
|
5481
|
+
* @param publishOption the publish options for the track.
|
|
5482
|
+
*/
|
|
5483
|
+
const computeVideoLayers = (videoTrack, publishOption) => {
|
|
5484
|
+
if (isAudioTrackType(publishOption.trackType))
|
|
5485
|
+
return;
|
|
5486
|
+
const optimalVideoLayers = [];
|
|
5487
|
+
const settings = videoTrack.getSettings();
|
|
5488
|
+
const { width = 0, height = 0 } = settings;
|
|
5489
|
+
const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, } = publishOption;
|
|
5490
|
+
const maxBitrate = getComputedMaxBitrate(videoDimension, width, height, bitrate);
|
|
5491
|
+
let downscaleFactor = 1;
|
|
5492
|
+
let bitrateFactor = 1;
|
|
5493
|
+
const svcCodec = isSvcCodec(codec?.name);
|
|
5494
|
+
for (const rid of ['f', 'h', 'q'].slice(0, maxSpatialLayers)) {
|
|
5495
|
+
const layer = {
|
|
5496
|
+
active: true,
|
|
5497
|
+
rid,
|
|
5498
|
+
width: Math.round(width / downscaleFactor),
|
|
5499
|
+
height: Math.round(height / downscaleFactor),
|
|
5500
|
+
maxBitrate: maxBitrate / bitrateFactor || defaultBitratePerRid[rid],
|
|
5501
|
+
maxFramerate: fps,
|
|
5502
|
+
};
|
|
5503
|
+
if (svcCodec) {
|
|
5504
|
+
// for SVC codecs, we need to set the scalability mode, and the
|
|
5505
|
+
// codec will handle the rest (layers, temporal layers, etc.)
|
|
5506
|
+
layer.scalabilityMode = toScalabilityMode(maxSpatialLayers, maxTemporalLayers);
|
|
5671
5507
|
}
|
|
5672
|
-
else
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
if (rtpMapLine) {
|
|
5676
|
-
rtpMap.push(rtpMapLine);
|
|
5677
|
-
}
|
|
5678
|
-
else if (fmtpLine) {
|
|
5679
|
-
fmtp.push(fmtpLine);
|
|
5680
|
-
}
|
|
5508
|
+
else {
|
|
5509
|
+
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
5510
|
+
layer.scaleResolutionDownBy = downscaleFactor;
|
|
5681
5511
|
}
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
};
|
|
5512
|
+
downscaleFactor *= 2;
|
|
5513
|
+
bitrateFactor *= 2;
|
|
5514
|
+
// Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
|
|
5515
|
+
// when deciding which layer to disable when CPU or bandwidth is constrained.
|
|
5516
|
+
// Encodings should be ordered in increasing spatial resolution order.
|
|
5517
|
+
optimalVideoLayers.unshift(layer);
|
|
5689
5518
|
}
|
|
5519
|
+
// for simplicity, we start with all layers enabled, then this function
|
|
5520
|
+
// will clear/reassign the layers that are not needed
|
|
5521
|
+
return withSimulcastConstraints(settings, optimalVideoLayers);
|
|
5690
5522
|
};
|
|
5691
5523
|
/**
|
|
5692
|
-
*
|
|
5524
|
+
* Computes the maximum bitrate for a given resolution.
|
|
5525
|
+
* If the current resolution is lower than the target resolution,
|
|
5526
|
+
* we want to proportionally reduce the target bitrate.
|
|
5527
|
+
* If the current resolution is higher than the target resolution,
|
|
5528
|
+
* we want to use the target bitrate.
|
|
5529
|
+
*
|
|
5530
|
+
* @param targetResolution the target resolution.
|
|
5531
|
+
* @param currentWidth the current width of the track.
|
|
5532
|
+
* @param currentHeight the current height of the track.
|
|
5533
|
+
* @param bitrate the target bitrate.
|
|
5693
5534
|
*/
|
|
5694
|
-
const
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
const
|
|
5698
|
-
if (
|
|
5699
|
-
|
|
5535
|
+
const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, bitrate) => {
|
|
5536
|
+
// if the current resolution is lower than the target resolution,
|
|
5537
|
+
// we want to proportionally reduce the target bitrate
|
|
5538
|
+
const { width: targetWidth, height: targetHeight } = targetResolution;
|
|
5539
|
+
if (currentWidth < targetWidth || currentHeight < targetHeight) {
|
|
5540
|
+
const currentPixels = currentWidth * currentHeight;
|
|
5541
|
+
const targetPixels = targetWidth * targetHeight;
|
|
5542
|
+
const reductionFactor = currentPixels / targetPixels;
|
|
5543
|
+
return Math.round(bitrate * reductionFactor);
|
|
5700
5544
|
}
|
|
5545
|
+
return bitrate;
|
|
5701
5546
|
};
|
|
5702
5547
|
/**
|
|
5703
|
-
*
|
|
5704
|
-
*/
|
|
5705
|
-
const toggleDtx = (sdp, enable) => {
|
|
5706
|
-
const opusFmtp = getOpusFmtp(sdp);
|
|
5707
|
-
if (!opusFmtp)
|
|
5708
|
-
return sdp;
|
|
5709
|
-
const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
|
|
5710
|
-
const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
|
|
5711
|
-
const newFmtp = matchDtx
|
|
5712
|
-
? opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig)
|
|
5713
|
-
: `${opusFmtp.original};${requiredDtxConfig}`;
|
|
5714
|
-
return sdp.replace(opusFmtp.original, newFmtp);
|
|
5715
|
-
};
|
|
5716
|
-
/**
|
|
5717
|
-
* Returns and SDP with all the codecs except the given codec removed.
|
|
5718
|
-
*/
|
|
5719
|
-
const preserveCodec = (sdp, mid, codec) => {
|
|
5720
|
-
const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
|
|
5721
|
-
const toSet = (fmtpLine) => new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
|
|
5722
|
-
const equal = (a, b) => {
|
|
5723
|
-
if (a.size !== b.size)
|
|
5724
|
-
return false;
|
|
5725
|
-
for (const item of a)
|
|
5726
|
-
if (!b.has(item))
|
|
5727
|
-
return false;
|
|
5728
|
-
return true;
|
|
5729
|
-
};
|
|
5730
|
-
const codecFmtp = toSet(codec.sdpFmtpLine || '');
|
|
5731
|
-
const parsedSdp = SDP__namespace.parse(sdp);
|
|
5732
|
-
for (const media of parsedSdp.media) {
|
|
5733
|
-
if (media.type !== kind || String(media.mid) !== mid)
|
|
5734
|
-
continue;
|
|
5735
|
-
// find the payload id of the desired codec
|
|
5736
|
-
const payloads = new Set();
|
|
5737
|
-
for (const rtp of media.rtp) {
|
|
5738
|
-
if (rtp.codec.toLowerCase() !== codecName)
|
|
5739
|
-
continue;
|
|
5740
|
-
const match =
|
|
5741
|
-
// vp8 doesn't have any fmtp, we preserve it without any additional checks
|
|
5742
|
-
codecName === 'vp8'
|
|
5743
|
-
? true
|
|
5744
|
-
: media.fmtp.some((f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp));
|
|
5745
|
-
if (match) {
|
|
5746
|
-
payloads.add(rtp.payload);
|
|
5747
|
-
}
|
|
5748
|
-
}
|
|
5749
|
-
// find the corresponding rtx codec by matching apt=<preserved-codec-payload>
|
|
5750
|
-
for (const fmtp of media.fmtp) {
|
|
5751
|
-
const match = fmtp.config.match(/(apt)=(\d+)/);
|
|
5752
|
-
if (!match)
|
|
5753
|
-
continue;
|
|
5754
|
-
const [, , preservedCodecPayload] = match;
|
|
5755
|
-
if (payloads.has(Number(preservedCodecPayload))) {
|
|
5756
|
-
payloads.add(fmtp.payload);
|
|
5757
|
-
}
|
|
5758
|
-
}
|
|
5759
|
-
media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
|
|
5760
|
-
media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
|
|
5761
|
-
media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
|
|
5762
|
-
media.payloads = Array.from(payloads).join(' ');
|
|
5763
|
-
}
|
|
5764
|
-
return SDP__namespace.write(parsedSdp);
|
|
5765
|
-
};
|
|
5766
|
-
/**
|
|
5767
|
-
* Enables high-quality audio through SDP munging for the given trackMid.
|
|
5548
|
+
* Browsers have different simulcast constraints for different video resolutions.
|
|
5768
5549
|
*
|
|
5769
|
-
*
|
|
5770
|
-
*
|
|
5771
|
-
*
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
const
|
|
5777
|
-
if (
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
if (!opusRtp)
|
|
5781
|
-
return sdp;
|
|
5782
|
-
const opusFmtp = audioMedia.fmtp.find((f) => f.payload === opusRtp.payload);
|
|
5783
|
-
if (!opusFmtp)
|
|
5784
|
-
return sdp;
|
|
5785
|
-
// enable stereo, if not already enabled
|
|
5786
|
-
if (opusFmtp.config.match(/stereo=(\d)/)) {
|
|
5787
|
-
opusFmtp.config = opusFmtp.config.replace(/stereo=(\d)/, 'stereo=1');
|
|
5788
|
-
}
|
|
5789
|
-
else {
|
|
5790
|
-
opusFmtp.config = `${opusFmtp.config};stereo=1`;
|
|
5550
|
+
* This function modifies the provided list of video layers according to the
|
|
5551
|
+
* current implementation of simulcast constraints in the Chromium based browsers.
|
|
5552
|
+
*
|
|
5553
|
+
* https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
|
|
5554
|
+
*/
|
|
5555
|
+
const withSimulcastConstraints = (settings, optimalVideoLayers) => {
|
|
5556
|
+
let layers;
|
|
5557
|
+
const size = Math.max(settings.width || 0, settings.height || 0);
|
|
5558
|
+
if (size <= 320) {
|
|
5559
|
+
// provide only one layer 320x240 (q), the one with the highest quality
|
|
5560
|
+
layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
|
|
5791
5561
|
}
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5562
|
+
else if (size <= 640) {
|
|
5563
|
+
// provide two layers, 160x120 (q) and 640x480 (h)
|
|
5564
|
+
layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
|
|
5795
5565
|
}
|
|
5796
5566
|
else {
|
|
5797
|
-
|
|
5567
|
+
// provide three layers for sizes > 640x480
|
|
5568
|
+
layers = optimalVideoLayers;
|
|
5798
5569
|
}
|
|
5799
|
-
|
|
5570
|
+
const ridMapping = ['q', 'h', 'f'];
|
|
5571
|
+
return layers.map((layer, index) => ({
|
|
5572
|
+
...layer,
|
|
5573
|
+
rid: ridMapping[index], // reassign rid
|
|
5574
|
+
}));
|
|
5800
5575
|
};
|
|
5576
|
+
|
|
5801
5577
|
/**
|
|
5802
5578
|
* Extracts the mid from the transceiver or the SDP.
|
|
5803
5579
|
*
|
|
@@ -5809,9 +5585,9 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5809
5585
|
if (transceiver.mid)
|
|
5810
5586
|
return transceiver.mid;
|
|
5811
5587
|
if (!sdp)
|
|
5812
|
-
return
|
|
5588
|
+
return String(transceiverInitIndex);
|
|
5813
5589
|
const track = transceiver.sender.track;
|
|
5814
|
-
const parsedSdp =
|
|
5590
|
+
const parsedSdp = sdpTransform.parse(sdp);
|
|
5815
5591
|
const media = parsedSdp.media.find((m) => {
|
|
5816
5592
|
return (m.type === track.kind &&
|
|
5817
5593
|
// if `msid` is not present, we assume that the track is the first one
|
|
@@ -5819,7 +5595,7 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5819
5595
|
});
|
|
5820
5596
|
if (typeof media?.mid !== 'undefined')
|
|
5821
5597
|
return String(media.mid);
|
|
5822
|
-
if (transceiverInitIndex
|
|
5598
|
+
if (transceiverInitIndex < 0)
|
|
5823
5599
|
return '';
|
|
5824
5600
|
return String(transceiverInitIndex);
|
|
5825
5601
|
};
|
|
@@ -5829,164 +5605,87 @@ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
|
|
|
5829
5605
|
*
|
|
5830
5606
|
* @internal
|
|
5831
5607
|
*/
|
|
5832
|
-
class Publisher {
|
|
5608
|
+
class Publisher extends BasePeerConnection {
|
|
5833
5609
|
/**
|
|
5834
5610
|
* Constructs a new `Publisher` instance.
|
|
5835
5611
|
*/
|
|
5836
|
-
constructor({
|
|
5837
|
-
|
|
5838
|
-
this.
|
|
5839
|
-
this.publishOptsForTrack = new Map();
|
|
5840
|
-
/**
|
|
5841
|
-
* An array maintaining the order how transceivers were added to the peer connection.
|
|
5842
|
-
* This is needed because some browsers (Firefox) don't reliably report
|
|
5843
|
-
* trackId and `mid` parameters.
|
|
5844
|
-
*
|
|
5845
|
-
* @internal
|
|
5846
|
-
*/
|
|
5847
|
-
this.transceiverInitOrder = [];
|
|
5848
|
-
this.isIceRestarting = false;
|
|
5849
|
-
this.createPeerConnection = (connectionConfig) => {
|
|
5850
|
-
const pc = new RTCPeerConnection(connectionConfig);
|
|
5851
|
-
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
5852
|
-
pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5853
|
-
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5854
|
-
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5855
|
-
pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
5856
|
-
pc.addEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
5857
|
-
return pc;
|
|
5858
|
-
};
|
|
5859
|
-
/**
|
|
5860
|
-
* Closes the publisher PeerConnection and cleans up the resources.
|
|
5861
|
-
*/
|
|
5862
|
-
this.close = ({ stopTracks }) => {
|
|
5863
|
-
if (stopTracks) {
|
|
5864
|
-
this.stopPublishing();
|
|
5865
|
-
this.transceiverCache.clear();
|
|
5866
|
-
this.trackLayersCache.clear();
|
|
5867
|
-
}
|
|
5868
|
-
this.detachEventHandlers();
|
|
5869
|
-
this.pc.close();
|
|
5870
|
-
};
|
|
5871
|
-
/**
|
|
5872
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5873
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
5874
|
-
* instance with a new one (in case of migration).
|
|
5875
|
-
*/
|
|
5876
|
-
this.detachEventHandlers = () => {
|
|
5877
|
-
this.unsubscribeOnIceRestart();
|
|
5878
|
-
this.unsubscribeChangePublishQuality();
|
|
5879
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
5880
|
-
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5881
|
-
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
5882
|
-
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
5883
|
-
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
5884
|
-
this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
5885
|
-
};
|
|
5612
|
+
constructor({ publishOptions, ...baseOptions }) {
|
|
5613
|
+
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
5614
|
+
this.transceiverCache = new TransceiverCache();
|
|
5886
5615
|
/**
|
|
5887
5616
|
* Starts publishing the given track of the given media stream.
|
|
5888
5617
|
*
|
|
5889
5618
|
* Consecutive calls to this method will replace the stream.
|
|
5890
5619
|
* The previous stream will be stopped.
|
|
5891
5620
|
*
|
|
5892
|
-
* @param mediaStream the media stream to publish.
|
|
5893
5621
|
* @param track the track to publish.
|
|
5894
5622
|
* @param trackType the track type to publish.
|
|
5895
|
-
* @param opts the optional publish options to use.
|
|
5896
5623
|
*/
|
|
5897
|
-
this.
|
|
5898
|
-
if (
|
|
5899
|
-
throw new Error(`
|
|
5900
|
-
}
|
|
5901
|
-
// enable the track if it is disabled
|
|
5902
|
-
if (!track.enabled)
|
|
5903
|
-
track.enabled = true;
|
|
5904
|
-
const transceiver = this.transceiverCache.get(trackType);
|
|
5905
|
-
if (!transceiver || !transceiver.sender.track) {
|
|
5906
|
-
// listen for 'ended' event on the track as it might be ended abruptly
|
|
5907
|
-
// by an external factors such as permission revokes, a disconnected device, etc.
|
|
5908
|
-
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
5909
|
-
const handleTrackEnded = () => {
|
|
5910
|
-
this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
|
|
5911
|
-
track.removeEventListener('ended', handleTrackEnded);
|
|
5912
|
-
this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch((err) => this.logger('warn', `Couldn't notify track mute state`, err));
|
|
5913
|
-
};
|
|
5914
|
-
track.addEventListener('ended', handleTrackEnded);
|
|
5915
|
-
this.addTransceiver(trackType, track, opts, mediaStream);
|
|
5624
|
+
this.publish = async (track, trackType) => {
|
|
5625
|
+
if (!this.publishOptions.some((o) => o.trackType === trackType)) {
|
|
5626
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
5916
5627
|
}
|
|
5917
|
-
|
|
5918
|
-
|
|
5628
|
+
for (const publishOption of this.publishOptions) {
|
|
5629
|
+
if (publishOption.trackType !== trackType)
|
|
5630
|
+
continue;
|
|
5631
|
+
// create a clone of the track as otherwise the same trackId will
|
|
5632
|
+
// appear in the SDP in multiple transceivers
|
|
5633
|
+
const trackToPublish = track.clone();
|
|
5634
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
5635
|
+
if (!transceiver) {
|
|
5636
|
+
this.addTransceiver(trackToPublish, publishOption);
|
|
5637
|
+
}
|
|
5638
|
+
else {
|
|
5639
|
+
await transceiver.sender.replaceTrack(trackToPublish);
|
|
5640
|
+
}
|
|
5919
5641
|
}
|
|
5920
|
-
await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
|
|
5921
5642
|
};
|
|
5922
5643
|
/**
|
|
5923
|
-
* Adds a new transceiver to the peer connection.
|
|
5924
|
-
* This needs to be called when a new track kind is added to the peer connection.
|
|
5925
|
-
* In other cases, use `updateTransceiver` method.
|
|
5644
|
+
* Adds a new transceiver carrying the given track to the peer connection.
|
|
5926
5645
|
*/
|
|
5927
|
-
this.addTransceiver = (
|
|
5928
|
-
const
|
|
5929
|
-
const
|
|
5930
|
-
|
|
5646
|
+
this.addTransceiver = (track, publishOption) => {
|
|
5647
|
+
const videoEncodings = computeVideoLayers(track, publishOption);
|
|
5648
|
+
const sendEncodings = isSvcCodec(publishOption.codec?.name)
|
|
5649
|
+
? toSvcEncodings(videoEncodings)
|
|
5650
|
+
: videoEncodings;
|
|
5931
5651
|
const transceiver = this.pc.addTransceiver(track, {
|
|
5932
5652
|
direction: 'sendonly',
|
|
5933
|
-
|
|
5934
|
-
? [mediaStream]
|
|
5935
|
-
: undefined,
|
|
5936
|
-
sendEncodings: isSvcCodec(codecInUse)
|
|
5937
|
-
? toSvcEncodings(videoEncodings)
|
|
5938
|
-
: videoEncodings,
|
|
5653
|
+
sendEncodings,
|
|
5939
5654
|
});
|
|
5655
|
+
const trackType = publishOption.trackType;
|
|
5940
5656
|
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
5941
|
-
this.
|
|
5942
|
-
this.transceiverCache.set(trackType, transceiver);
|
|
5943
|
-
this.publishOptsForTrack.set(trackType, opts);
|
|
5944
|
-
// handle codec preferences
|
|
5945
|
-
if (!('setCodecPreferences' in transceiver))
|
|
5946
|
-
return;
|
|
5947
|
-
const codecPreferences = this.getCodecPreferences(trackType, trackType === TrackType.VIDEO ? codecInUse : undefined, 'receiver');
|
|
5948
|
-
if (!codecPreferences)
|
|
5949
|
-
return;
|
|
5950
|
-
try {
|
|
5951
|
-
this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
5952
|
-
transceiver.setCodecPreferences(codecPreferences);
|
|
5953
|
-
}
|
|
5954
|
-
catch (err) {
|
|
5955
|
-
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
5956
|
-
}
|
|
5957
|
-
};
|
|
5958
|
-
/**
|
|
5959
|
-
* Updates the given transceiver with the new track.
|
|
5960
|
-
* Stops the previous track and replaces it with the new one.
|
|
5961
|
-
*/
|
|
5962
|
-
this.updateTransceiver = async (transceiver, track) => {
|
|
5963
|
-
const previousTrack = transceiver.sender.track;
|
|
5964
|
-
// don't stop the track if we are re-publishing the same track
|
|
5965
|
-
if (previousTrack && previousTrack !== track) {
|
|
5966
|
-
previousTrack.stop();
|
|
5967
|
-
}
|
|
5968
|
-
await transceiver.sender.replaceTrack(track);
|
|
5657
|
+
this.transceiverCache.add(publishOption, transceiver);
|
|
5969
5658
|
};
|
|
5970
5659
|
/**
|
|
5971
|
-
*
|
|
5972
|
-
* Underlying track will be stopped and removed from the publisher.
|
|
5973
|
-
* @param trackType the track type to unpublish.
|
|
5974
|
-
* @param stopTrack specifies whether track should be stopped or just disabled
|
|
5660
|
+
* Synchronizes the current Publisher state with the provided publish options.
|
|
5975
5661
|
*/
|
|
5976
|
-
this.
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
(
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5662
|
+
this.syncPublishOptions = async () => {
|
|
5663
|
+
// enable publishing with new options -> [av1, vp9]
|
|
5664
|
+
for (const publishOption of this.publishOptions) {
|
|
5665
|
+
const { trackType } = publishOption;
|
|
5666
|
+
if (!this.isPublishing(trackType))
|
|
5667
|
+
continue;
|
|
5668
|
+
if (this.transceiverCache.has(publishOption))
|
|
5669
|
+
continue;
|
|
5670
|
+
const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
|
|
5671
|
+
i.publishOption.trackType === trackType);
|
|
5672
|
+
if (!item || !item.transceiver)
|
|
5673
|
+
continue;
|
|
5674
|
+
// take the track from the existing transceiver for the same track type,
|
|
5675
|
+
// clone it and publish it with the new publish options
|
|
5676
|
+
const track = item.transceiver.sender.track.clone();
|
|
5677
|
+
this.addTransceiver(track, publishOption);
|
|
5678
|
+
}
|
|
5679
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
5680
|
+
for (const item of this.transceiverCache.items()) {
|
|
5681
|
+
const { publishOption, transceiver } = item;
|
|
5682
|
+
const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
|
|
5683
|
+
option.trackType === publishOption.trackType);
|
|
5684
|
+
if (hasPublishOption)
|
|
5685
|
+
continue;
|
|
5686
|
+
// it is safe to stop the track here, it is a clone
|
|
5687
|
+
transceiver.sender.track?.stop();
|
|
5688
|
+
await transceiver.sender.replaceTrack(null);
|
|
5990
5689
|
}
|
|
5991
5690
|
};
|
|
5992
5691
|
/**
|
|
@@ -5995,57 +5694,52 @@ class Publisher {
|
|
|
5995
5694
|
* @param trackType the track type to check.
|
|
5996
5695
|
*/
|
|
5997
5696
|
this.isPublishing = (trackType) => {
|
|
5998
|
-
const
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
|
|
6007
|
-
if (!audioOrVideoOrScreenShareStream)
|
|
6008
|
-
return;
|
|
6009
|
-
if (isMuted) {
|
|
6010
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
6011
|
-
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
6012
|
-
[audioOrVideoOrScreenShareStream]: undefined,
|
|
6013
|
-
}));
|
|
6014
|
-
}
|
|
6015
|
-
else {
|
|
6016
|
-
this.state.updateParticipant(this.sfuClient.sessionId, (p) => {
|
|
6017
|
-
return {
|
|
6018
|
-
publishedTracks: p.publishedTracks.includes(trackType)
|
|
6019
|
-
? p.publishedTracks
|
|
6020
|
-
: [...p.publishedTracks, trackType],
|
|
6021
|
-
[audioOrVideoOrScreenShareStream]: mediaStream,
|
|
6022
|
-
};
|
|
6023
|
-
});
|
|
5697
|
+
for (const item of this.transceiverCache.items()) {
|
|
5698
|
+
if (item.publishOption.trackType !== trackType)
|
|
5699
|
+
continue;
|
|
5700
|
+
const track = item.transceiver.sender.track;
|
|
5701
|
+
if (!track)
|
|
5702
|
+
continue;
|
|
5703
|
+
if (track.readyState === 'live' && track.enabled)
|
|
5704
|
+
return true;
|
|
6024
5705
|
}
|
|
5706
|
+
return false;
|
|
6025
5707
|
};
|
|
6026
5708
|
/**
|
|
6027
|
-
*
|
|
5709
|
+
* Maps the given track ID to the corresponding track type.
|
|
6028
5710
|
*/
|
|
6029
|
-
this.
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
this.pc.removeTrack(s);
|
|
5711
|
+
this.getTrackType = (trackId) => {
|
|
5712
|
+
for (const transceiverId of this.transceiverCache.items()) {
|
|
5713
|
+
const { publishOption, transceiver } = transceiverId;
|
|
5714
|
+
if (transceiver.sender.track?.id === trackId) {
|
|
5715
|
+
return publishOption.trackType;
|
|
6035
5716
|
}
|
|
6036
|
-
}
|
|
5717
|
+
}
|
|
5718
|
+
return undefined;
|
|
6037
5719
|
};
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
5720
|
+
/**
|
|
5721
|
+
* Stops the cloned track that is being published to the SFU.
|
|
5722
|
+
*/
|
|
5723
|
+
this.stopTracks = (...trackTypes) => {
|
|
5724
|
+
for (const item of this.transceiverCache.items()) {
|
|
5725
|
+
const { publishOption, transceiver } = item;
|
|
5726
|
+
if (!trackTypes.includes(publishOption.trackType))
|
|
5727
|
+
continue;
|
|
5728
|
+
transceiver.sender.track?.stop();
|
|
5729
|
+
}
|
|
5730
|
+
};
|
|
5731
|
+
this.changePublishQuality = async (videoSender) => {
|
|
5732
|
+
const { trackType, layers, publishOptionId } = videoSender;
|
|
5733
|
+
const enabledLayers = layers.filter((l) => l.active);
|
|
5734
|
+
const tag = 'Update publish quality:';
|
|
5735
|
+
this.logger('info', `${tag} requested layers by SFU:`, enabledLayers);
|
|
5736
|
+
const sender = this.transceiverCache.getWith(trackType, publishOptionId)?.sender;
|
|
5737
|
+
if (!sender) {
|
|
5738
|
+
return this.logger('warn', `${tag} no video sender found.`);
|
|
6044
5739
|
}
|
|
6045
|
-
const params =
|
|
5740
|
+
const params = sender.getParameters();
|
|
6046
5741
|
if (params.encodings.length === 0) {
|
|
6047
|
-
this.logger('warn',
|
|
6048
|
-
return;
|
|
5742
|
+
return this.logger('warn', `${tag} there are no encodings set.`);
|
|
6049
5743
|
}
|
|
6050
5744
|
const [codecInUse] = params.codecs;
|
|
6051
5745
|
const usesSvcCodec = codecInUse && isSvcCodec(codecInUse.mimeType);
|
|
@@ -6087,54 +5781,12 @@ class Publisher {
|
|
|
6087
5781
|
changed = true;
|
|
6088
5782
|
}
|
|
6089
5783
|
}
|
|
6090
|
-
const
|
|
5784
|
+
const activeEncoders = params.encodings.filter((e) => e.active);
|
|
6091
5785
|
if (!changed) {
|
|
6092
|
-
this.logger('info',
|
|
6093
|
-
return;
|
|
6094
|
-
}
|
|
6095
|
-
await videoSender.setParameters(params);
|
|
6096
|
-
this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
|
|
6097
|
-
};
|
|
6098
|
-
/**
|
|
6099
|
-
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
6100
|
-
* @param selector
|
|
6101
|
-
* @returns
|
|
6102
|
-
*/
|
|
6103
|
-
this.getStats = (selector) => {
|
|
6104
|
-
return this.pc.getStats(selector);
|
|
6105
|
-
};
|
|
6106
|
-
this.getCodecPreferences = (trackType, preferredCodec, codecPreferencesSource) => {
|
|
6107
|
-
if (trackType === TrackType.VIDEO) {
|
|
6108
|
-
return getPreferredCodecs('video', preferredCodec || 'vp8', undefined, codecPreferencesSource);
|
|
6109
|
-
}
|
|
6110
|
-
if (trackType === TrackType.AUDIO) {
|
|
6111
|
-
const defaultAudioCodec = this.isRedEnabled ? 'red' : 'opus';
|
|
6112
|
-
const codecToRemove = !this.isRedEnabled ? 'red' : undefined;
|
|
6113
|
-
return getPreferredCodecs('audio', preferredCodec ?? defaultAudioCodec, codecToRemove, codecPreferencesSource);
|
|
6114
|
-
}
|
|
6115
|
-
};
|
|
6116
|
-
this.onIceCandidate = (e) => {
|
|
6117
|
-
const { candidate } = e;
|
|
6118
|
-
if (!candidate) {
|
|
6119
|
-
this.logger('debug', 'null ice candidate');
|
|
6120
|
-
return;
|
|
5786
|
+
return this.logger('info', `${tag} no change:`, activeEncoders);
|
|
6121
5787
|
}
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
iceCandidate: getIceCandidate(candidate),
|
|
6125
|
-
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
6126
|
-
})
|
|
6127
|
-
.catch((err) => {
|
|
6128
|
-
this.logger('warn', `ICETrickle failed`, err);
|
|
6129
|
-
});
|
|
6130
|
-
};
|
|
6131
|
-
/**
|
|
6132
|
-
* Sets the SFU client to use.
|
|
6133
|
-
*
|
|
6134
|
-
* @param sfuClient the SFU client to use.
|
|
6135
|
-
*/
|
|
6136
|
-
this.setSfuClient = (sfuClient) => {
|
|
6137
|
-
this.sfuClient = sfuClient;
|
|
5788
|
+
await sender.setParameters(params);
|
|
5789
|
+
this.logger('info', `${tag} enabled rids:`, activeEncoders);
|
|
6138
5790
|
};
|
|
6139
5791
|
/**
|
|
6140
5792
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
@@ -6149,7 +5801,7 @@ class Publisher {
|
|
|
6149
5801
|
await this.negotiate({ iceRestart: true });
|
|
6150
5802
|
};
|
|
6151
5803
|
this.onNegotiationNeeded = () => {
|
|
6152
|
-
this.negotiate().catch((err) => {
|
|
5804
|
+
withoutConcurrency('publisher.negotiate', () => this.negotiate()).catch((err) => {
|
|
6153
5805
|
this.logger('error', `Negotiation failed.`, err);
|
|
6154
5806
|
this.onUnrecoverableError?.();
|
|
6155
5807
|
});
|
|
@@ -6161,18 +5813,6 @@ class Publisher {
|
|
|
6161
5813
|
*/
|
|
6162
5814
|
this.negotiate = async (options) => {
|
|
6163
5815
|
const offer = await this.pc.createOffer(options);
|
|
6164
|
-
if (offer.sdp) {
|
|
6165
|
-
offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
|
|
6166
|
-
if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
6167
|
-
offer.sdp = this.enableHighQualityAudio(offer.sdp);
|
|
6168
|
-
}
|
|
6169
|
-
if (this.isPublishing(TrackType.VIDEO)) {
|
|
6170
|
-
// Hotfix for platforms that don't respect the ordered codec list
|
|
6171
|
-
// (Firefox, Android, Linux, etc...).
|
|
6172
|
-
// We remove all the codecs from the SDP except the one we want to use.
|
|
6173
|
-
offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
|
|
6174
|
-
}
|
|
6175
|
-
}
|
|
6176
5816
|
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
6177
5817
|
if (trackInfos.length === 0) {
|
|
6178
5818
|
throw new Error(`Can't negotiate without announcing any tracks`);
|
|
@@ -6191,238 +5831,121 @@ class Publisher {
|
|
|
6191
5831
|
finally {
|
|
6192
5832
|
this.isIceRestarting = false;
|
|
6193
5833
|
}
|
|
6194
|
-
this.
|
|
6195
|
-
try {
|
|
6196
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
6197
|
-
await this.pc.addIceCandidate(iceCandidate);
|
|
6198
|
-
}
|
|
6199
|
-
catch (e) {
|
|
6200
|
-
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
6201
|
-
}
|
|
6202
|
-
});
|
|
6203
|
-
};
|
|
6204
|
-
this.enableHighQualityAudio = (sdp) => {
|
|
6205
|
-
const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
|
|
6206
|
-
if (!transceiver)
|
|
6207
|
-
return sdp;
|
|
6208
|
-
const transceiverInitIndex = this.transceiverInitOrder.indexOf(TrackType.SCREEN_SHARE_AUDIO);
|
|
6209
|
-
const mid = extractMid(transceiver, transceiverInitIndex, sdp);
|
|
6210
|
-
return enableHighQualityAudio(sdp, mid);
|
|
5834
|
+
this.addTrickledIceCandidates();
|
|
6211
5835
|
};
|
|
6212
5836
|
/**
|
|
6213
5837
|
* Returns a list of tracks that are currently being published.
|
|
6214
|
-
*
|
|
6215
|
-
* @internal
|
|
6216
|
-
* @param sdp an optional SDP to extract the `mid` from.
|
|
6217
5838
|
*/
|
|
6218
|
-
this.
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
.getTransceivers()
|
|
6222
|
-
.filter((t) => t.direction === 'sendonly' && t.sender.track)
|
|
6223
|
-
.map((transceiver) => {
|
|
6224
|
-
let trackType;
|
|
6225
|
-
this.transceiverCache.forEach((value, key) => {
|
|
6226
|
-
if (value === transceiver)
|
|
6227
|
-
trackType = key;
|
|
6228
|
-
});
|
|
5839
|
+
this.getPublishedTracks = () => {
|
|
5840
|
+
const tracks = [];
|
|
5841
|
+
for (const { transceiver } of this.transceiverCache.items()) {
|
|
6229
5842
|
const track = transceiver.sender.track;
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
if (isTrackLive) {
|
|
6233
|
-
optimalLayers = this.computeLayers(trackType, track) || [];
|
|
6234
|
-
this.trackLayersCache.set(trackType, optimalLayers);
|
|
6235
|
-
}
|
|
6236
|
-
else {
|
|
6237
|
-
// we report the last known optimal layers for ended tracks
|
|
6238
|
-
optimalLayers = this.trackLayersCache.get(trackType) || [];
|
|
6239
|
-
this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
|
|
6240
|
-
}
|
|
6241
|
-
const layers = optimalLayers.map((optimalLayer) => ({
|
|
6242
|
-
rid: optimalLayer.rid || '',
|
|
6243
|
-
bitrate: optimalLayer.maxBitrate || 0,
|
|
6244
|
-
fps: optimalLayer.maxFramerate || 0,
|
|
6245
|
-
quality: ridToVideoQuality(optimalLayer.rid || ''),
|
|
6246
|
-
videoDimension: {
|
|
6247
|
-
width: optimalLayer.width,
|
|
6248
|
-
height: optimalLayer.height,
|
|
6249
|
-
},
|
|
6250
|
-
}));
|
|
6251
|
-
const isAudioTrack = [
|
|
6252
|
-
TrackType.AUDIO,
|
|
6253
|
-
TrackType.SCREEN_SHARE_AUDIO,
|
|
6254
|
-
].includes(trackType);
|
|
6255
|
-
const trackSettings = track.getSettings();
|
|
6256
|
-
const isStereo = isAudioTrack && trackSettings.channelCount === 2;
|
|
6257
|
-
const transceiverInitIndex = this.transceiverInitOrder.indexOf(trackType);
|
|
6258
|
-
return {
|
|
6259
|
-
trackId: track.id,
|
|
6260
|
-
layers: layers,
|
|
6261
|
-
trackType,
|
|
6262
|
-
mid: extractMid(transceiver, transceiverInitIndex, sdp),
|
|
6263
|
-
stereo: isStereo,
|
|
6264
|
-
dtx: isAudioTrack && this.isDtxEnabled,
|
|
6265
|
-
red: isAudioTrack && this.isRedEnabled,
|
|
6266
|
-
muted: !isTrackLive,
|
|
6267
|
-
};
|
|
6268
|
-
});
|
|
6269
|
-
};
|
|
6270
|
-
this.computeLayers = (trackType, track, opts) => {
|
|
6271
|
-
const { settings } = this.state;
|
|
6272
|
-
const targetResolution = settings?.video
|
|
6273
|
-
.target_resolution;
|
|
6274
|
-
const screenShareBitrate = settings?.screensharing.target_resolution?.bitrate;
|
|
6275
|
-
const publishOpts = opts || this.publishOptsForTrack.get(trackType);
|
|
6276
|
-
const codecInUse = opts?.forceCodec || getOptimalVideoCodec(opts?.preferredCodec);
|
|
6277
|
-
return trackType === TrackType.VIDEO
|
|
6278
|
-
? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
|
|
6279
|
-
: trackType === TrackType.SCREEN_SHARE
|
|
6280
|
-
? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
|
|
6281
|
-
: undefined;
|
|
6282
|
-
};
|
|
6283
|
-
this.onIceCandidateError = (e) => {
|
|
6284
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
6285
|
-
`${e.errorCode}: ${e.errorText}`;
|
|
6286
|
-
const iceState = this.pc.iceConnectionState;
|
|
6287
|
-
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
6288
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6289
|
-
};
|
|
6290
|
-
this.onIceConnectionStateChange = () => {
|
|
6291
|
-
const state = this.pc.iceConnectionState;
|
|
6292
|
-
this.logger('debug', `ICE Connection state changed to`, state);
|
|
6293
|
-
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
6294
|
-
return;
|
|
6295
|
-
if (state === 'failed' || state === 'disconnected') {
|
|
6296
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
6297
|
-
this.restartIce().catch((e) => {
|
|
6298
|
-
this.logger('error', `ICE restart error`, e);
|
|
6299
|
-
this.onUnrecoverableError?.();
|
|
6300
|
-
});
|
|
5843
|
+
if (track && track.readyState === 'live')
|
|
5844
|
+
tracks.push(track);
|
|
6301
5845
|
}
|
|
6302
|
-
|
|
6303
|
-
this.onIceGatheringStateChange = () => {
|
|
6304
|
-
this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
6305
|
-
};
|
|
6306
|
-
this.onSignalingStateChange = () => {
|
|
6307
|
-
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
6308
|
-
};
|
|
6309
|
-
this.logger = getLogger(['Publisher', logTag]);
|
|
6310
|
-
this.pc = this.createPeerConnection(connectionConfig);
|
|
6311
|
-
this.sfuClient = sfuClient;
|
|
6312
|
-
this.state = state;
|
|
6313
|
-
this.isDtxEnabled = isDtxEnabled;
|
|
6314
|
-
this.isRedEnabled = isRedEnabled;
|
|
6315
|
-
this.onUnrecoverableError = onUnrecoverableError;
|
|
6316
|
-
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
6317
|
-
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
6318
|
-
return;
|
|
6319
|
-
this.restartIce().catch((err) => {
|
|
6320
|
-
this.logger('warn', `ICERestart failed`, err);
|
|
6321
|
-
this.onUnrecoverableError?.();
|
|
6322
|
-
});
|
|
6323
|
-
});
|
|
6324
|
-
this.unsubscribeChangePublishQuality = dispatcher.on('changePublishQuality', ({ videoSenders }) => {
|
|
6325
|
-
withoutConcurrency('publisher.changePublishQuality', async () => {
|
|
6326
|
-
for (const videoSender of videoSenders) {
|
|
6327
|
-
const { layers } = videoSender;
|
|
6328
|
-
const enabledLayers = layers.filter((l) => l.active);
|
|
6329
|
-
await this.changePublishQuality(enabledLayers);
|
|
6330
|
-
}
|
|
6331
|
-
}).catch((err) => {
|
|
6332
|
-
this.logger('warn', 'Failed to change publish quality', err);
|
|
6333
|
-
});
|
|
6334
|
-
});
|
|
6335
|
-
}
|
|
6336
|
-
removeUnpreferredCodecs(sdp, trackType) {
|
|
6337
|
-
const opts = this.publishOptsForTrack.get(trackType);
|
|
6338
|
-
const forceSingleCodec = !!opts?.forceSingleCodec || isReactNative() || isFirefox();
|
|
6339
|
-
if (!opts || !forceSingleCodec)
|
|
6340
|
-
return sdp;
|
|
6341
|
-
const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec);
|
|
6342
|
-
const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender');
|
|
6343
|
-
if (!orderedCodecs || orderedCodecs.length === 0)
|
|
6344
|
-
return sdp;
|
|
6345
|
-
const transceiver = this.transceiverCache.get(trackType);
|
|
6346
|
-
if (!transceiver)
|
|
6347
|
-
return sdp;
|
|
6348
|
-
const index = this.transceiverInitOrder.indexOf(trackType);
|
|
6349
|
-
const mid = extractMid(transceiver, index, sdp);
|
|
6350
|
-
const [codecToPreserve] = orderedCodecs;
|
|
6351
|
-
return preserveCodec(sdp, mid, codecToPreserve);
|
|
6352
|
-
}
|
|
6353
|
-
}
|
|
6354
|
-
|
|
6355
|
-
/**
|
|
6356
|
-
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
6357
|
-
* media streams from the SFU.
|
|
6358
|
-
*
|
|
6359
|
-
* @internal
|
|
6360
|
-
*/
|
|
6361
|
-
class Subscriber {
|
|
6362
|
-
/**
|
|
6363
|
-
* Constructs a new `Subscriber` instance.
|
|
6364
|
-
*
|
|
6365
|
-
* @param sfuClient the SFU client to use.
|
|
6366
|
-
* @param dispatcher the dispatcher to use.
|
|
6367
|
-
* @param state the state of the call.
|
|
6368
|
-
* @param connectionConfig the connection configuration to use.
|
|
6369
|
-
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
6370
|
-
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
6371
|
-
* @param logTag a tag to use for logging.
|
|
6372
|
-
*/
|
|
6373
|
-
constructor({ sfuClient, dispatcher, state, connectionConfig, onUnrecoverableError, logTag, }) {
|
|
6374
|
-
this.isIceRestarting = false;
|
|
6375
|
-
/**
|
|
6376
|
-
* Creates a new `RTCPeerConnection` instance with the given configuration.
|
|
6377
|
-
*
|
|
6378
|
-
* @param connectionConfig the connection configuration to use.
|
|
6379
|
-
*/
|
|
6380
|
-
this.createPeerConnection = (connectionConfig) => {
|
|
6381
|
-
const pc = new RTCPeerConnection(connectionConfig);
|
|
6382
|
-
pc.addEventListener('icecandidate', this.onIceCandidate);
|
|
6383
|
-
pc.addEventListener('track', this.handleOnTrack);
|
|
6384
|
-
pc.addEventListener('icecandidateerror', this.onIceCandidateError);
|
|
6385
|
-
pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
6386
|
-
pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
6387
|
-
return pc;
|
|
5846
|
+
return tracks;
|
|
6388
5847
|
};
|
|
6389
5848
|
/**
|
|
6390
|
-
*
|
|
6391
|
-
|
|
6392
|
-
this.close = () => {
|
|
6393
|
-
this.detachEventHandlers();
|
|
6394
|
-
this.pc.close();
|
|
6395
|
-
};
|
|
6396
|
-
/**
|
|
6397
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
6398
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
6399
|
-
* instance with a new one (in case of migration).
|
|
5849
|
+
* Returns a list of tracks that are currently being published.
|
|
5850
|
+
* @param sdp an optional SDP to extract the `mid` from.
|
|
6400
5851
|
*/
|
|
6401
|
-
this.
|
|
6402
|
-
|
|
6403
|
-
this.
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
5852
|
+
this.getAnnouncedTracks = (sdp) => {
|
|
5853
|
+
const trackInfos = [];
|
|
5854
|
+
for (const bundle of this.transceiverCache.items()) {
|
|
5855
|
+
const { transceiver, publishOption } = bundle;
|
|
5856
|
+
const track = transceiver.sender.track;
|
|
5857
|
+
if (!track)
|
|
5858
|
+
continue;
|
|
5859
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
5860
|
+
}
|
|
5861
|
+
return trackInfos;
|
|
6409
5862
|
};
|
|
6410
5863
|
/**
|
|
6411
|
-
* Returns
|
|
6412
|
-
*
|
|
6413
|
-
*
|
|
6414
|
-
*/
|
|
6415
|
-
this.
|
|
6416
|
-
|
|
5864
|
+
* Returns a list of tracks that are currently being published.
|
|
5865
|
+
* This method shall be used for the reconnection flow.
|
|
5866
|
+
* There we shouldn't announce the tracks that have been stopped due to a codec switch.
|
|
5867
|
+
*/
|
|
5868
|
+
this.getAnnouncedTracksForReconnect = () => {
|
|
5869
|
+
const sdp = this.pc.localDescription?.sdp;
|
|
5870
|
+
const trackInfos = [];
|
|
5871
|
+
for (const publishOption of this.publishOptions) {
|
|
5872
|
+
const transceiver = this.transceiverCache.get(publishOption);
|
|
5873
|
+
if (!transceiver || !transceiver.sender.track)
|
|
5874
|
+
continue;
|
|
5875
|
+
trackInfos.push(this.toTrackInfo(transceiver, publishOption, sdp));
|
|
5876
|
+
}
|
|
5877
|
+
return trackInfos;
|
|
6417
5878
|
};
|
|
6418
5879
|
/**
|
|
6419
|
-
*
|
|
6420
|
-
*
|
|
6421
|
-
* @param sfuClient the SFU client to use.
|
|
5880
|
+
* Converts the given transceiver to a `TrackInfo` object.
|
|
6422
5881
|
*/
|
|
6423
|
-
this.
|
|
6424
|
-
|
|
5882
|
+
this.toTrackInfo = (transceiver, publishOption, sdp) => {
|
|
5883
|
+
const track = transceiver.sender.track;
|
|
5884
|
+
const isTrackLive = track.readyState === 'live';
|
|
5885
|
+
const layers = isTrackLive
|
|
5886
|
+
? computeVideoLayers(track, publishOption)
|
|
5887
|
+
: this.transceiverCache.getLayers(publishOption);
|
|
5888
|
+
this.transceiverCache.setLayers(publishOption, layers);
|
|
5889
|
+
const isAudioTrack = isAudioTrackType(publishOption.trackType);
|
|
5890
|
+
const isStereo = isAudioTrack && track.getSettings().channelCount === 2;
|
|
5891
|
+
const transceiverIndex = this.transceiverCache.indexOf(transceiver);
|
|
5892
|
+
const audioSettings = this.state.settings?.audio;
|
|
5893
|
+
return {
|
|
5894
|
+
trackId: track.id,
|
|
5895
|
+
layers: toVideoLayers(layers),
|
|
5896
|
+
trackType: publishOption.trackType,
|
|
5897
|
+
mid: extractMid(transceiver, transceiverIndex, sdp),
|
|
5898
|
+
stereo: isStereo,
|
|
5899
|
+
dtx: isAudioTrack && !!audioSettings?.opus_dtx_enabled,
|
|
5900
|
+
red: isAudioTrack && !!audioSettings?.redundant_coding_enabled,
|
|
5901
|
+
muted: !isTrackLive,
|
|
5902
|
+
codec: publishOption.codec,
|
|
5903
|
+
publishOptionId: publishOption.id,
|
|
5904
|
+
};
|
|
6425
5905
|
};
|
|
5906
|
+
this.publishOptions = publishOptions;
|
|
5907
|
+
this.pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5908
|
+
this.on('iceRestart', (iceRestart) => {
|
|
5909
|
+
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
5910
|
+
return;
|
|
5911
|
+
this.restartIce().catch((err) => {
|
|
5912
|
+
this.logger('warn', `ICERestart failed`, err);
|
|
5913
|
+
this.onUnrecoverableError?.();
|
|
5914
|
+
});
|
|
5915
|
+
});
|
|
5916
|
+
this.on('changePublishQuality', async (event) => {
|
|
5917
|
+
for (const videoSender of event.videoSenders) {
|
|
5918
|
+
await this.changePublishQuality(videoSender);
|
|
5919
|
+
}
|
|
5920
|
+
});
|
|
5921
|
+
this.on('changePublishOptions', (event) => {
|
|
5922
|
+
this.publishOptions = event.publishOptions;
|
|
5923
|
+
return this.syncPublishOptions();
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
/**
|
|
5927
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
5928
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
5929
|
+
* instance with a new one (in case of migration).
|
|
5930
|
+
*/
|
|
5931
|
+
detachEventHandlers() {
|
|
5932
|
+
super.detachEventHandlers();
|
|
5933
|
+
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
/**
|
|
5938
|
+
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
5939
|
+
* media streams from the SFU.
|
|
5940
|
+
*
|
|
5941
|
+
* @internal
|
|
5942
|
+
*/
|
|
5943
|
+
class Subscriber extends BasePeerConnection {
|
|
5944
|
+
/**
|
|
5945
|
+
* Constructs a new `Subscriber` instance.
|
|
5946
|
+
*/
|
|
5947
|
+
constructor(opts) {
|
|
5948
|
+
super(PeerType.SUBSCRIBER, opts);
|
|
6426
5949
|
/**
|
|
6427
5950
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
6428
5951
|
*/
|
|
@@ -6485,7 +6008,15 @@ class Subscriber {
|
|
|
6485
6008
|
this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
6486
6009
|
return;
|
|
6487
6010
|
}
|
|
6011
|
+
// get the previous stream to dispose it later
|
|
6012
|
+
// usually this happens during migration, when the stream is replaced
|
|
6013
|
+
// with a new one but the old one is still in the state
|
|
6488
6014
|
const previousStream = participantToUpdate[streamKindProp];
|
|
6015
|
+
// replace the previous stream with the new one, prevents flickering
|
|
6016
|
+
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
6017
|
+
[streamKindProp]: primaryStream,
|
|
6018
|
+
});
|
|
6019
|
+
// now, dispose the previous stream if it exists
|
|
6489
6020
|
if (previousStream) {
|
|
6490
6021
|
this.logger('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
6491
6022
|
previousStream.getTracks().forEach((t) => {
|
|
@@ -6493,24 +6024,6 @@ class Subscriber {
|
|
|
6493
6024
|
previousStream.removeTrack(t);
|
|
6494
6025
|
});
|
|
6495
6026
|
}
|
|
6496
|
-
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
6497
|
-
[streamKindProp]: primaryStream,
|
|
6498
|
-
});
|
|
6499
|
-
};
|
|
6500
|
-
this.onIceCandidate = (e) => {
|
|
6501
|
-
const { candidate } = e;
|
|
6502
|
-
if (!candidate) {
|
|
6503
|
-
this.logger('debug', 'null ice candidate');
|
|
6504
|
-
return;
|
|
6505
|
-
}
|
|
6506
|
-
this.sfuClient
|
|
6507
|
-
.iceTrickle({
|
|
6508
|
-
iceCandidate: getIceCandidate(candidate),
|
|
6509
|
-
peerType: PeerType.SUBSCRIBER,
|
|
6510
|
-
})
|
|
6511
|
-
.catch((err) => {
|
|
6512
|
-
this.logger('warn', `ICETrickle failed`, err);
|
|
6513
|
-
});
|
|
6514
6027
|
};
|
|
6515
6028
|
this.negotiate = async (subscriberOffer) => {
|
|
6516
6029
|
this.logger('info', `Received subscriberOffer`, subscriberOffer);
|
|
@@ -6518,15 +6031,7 @@ class Subscriber {
|
|
|
6518
6031
|
type: 'offer',
|
|
6519
6032
|
sdp: subscriberOffer.sdp,
|
|
6520
6033
|
});
|
|
6521
|
-
this.
|
|
6522
|
-
try {
|
|
6523
|
-
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
6524
|
-
await this.pc.addIceCandidate(iceCandidate);
|
|
6525
|
-
}
|
|
6526
|
-
catch (e) {
|
|
6527
|
-
this.logger('warn', `ICE candidate error`, [e, candidate]);
|
|
6528
|
-
}
|
|
6529
|
-
});
|
|
6034
|
+
this.addTrickledIceCandidates();
|
|
6530
6035
|
const answer = await this.pc.createAnswer();
|
|
6531
6036
|
await this.pc.setLocalDescription(answer);
|
|
6532
6037
|
await this.sfuClient.sendAnswer({
|
|
@@ -6535,56 +6040,21 @@ class Subscriber {
|
|
|
6535
6040
|
});
|
|
6536
6041
|
this.isIceRestarting = false;
|
|
6537
6042
|
};
|
|
6538
|
-
this.
|
|
6539
|
-
|
|
6540
|
-
this.
|
|
6541
|
-
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
6542
|
-
return;
|
|
6543
|
-
// do nothing when ICE is restarting
|
|
6544
|
-
if (this.isIceRestarting)
|
|
6545
|
-
return;
|
|
6546
|
-
if (state === 'failed' || state === 'disconnected') {
|
|
6547
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
6548
|
-
this.restartIce().catch((e) => {
|
|
6549
|
-
this.logger('error', `ICE restart failed`, e);
|
|
6550
|
-
this.onUnrecoverableError?.();
|
|
6551
|
-
});
|
|
6552
|
-
}
|
|
6553
|
-
};
|
|
6554
|
-
this.onIceGatheringStateChange = () => {
|
|
6555
|
-
this.logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
6556
|
-
};
|
|
6557
|
-
this.onIceCandidateError = (e) => {
|
|
6558
|
-
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
6559
|
-
`${e.errorCode}: ${e.errorText}`;
|
|
6560
|
-
const iceState = this.pc.iceConnectionState;
|
|
6561
|
-
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
6562
|
-
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
6563
|
-
};
|
|
6564
|
-
this.logger = getLogger(['Subscriber', logTag]);
|
|
6565
|
-
this.sfuClient = sfuClient;
|
|
6566
|
-
this.state = state;
|
|
6567
|
-
this.onUnrecoverableError = onUnrecoverableError;
|
|
6568
|
-
this.pc = this.createPeerConnection(connectionConfig);
|
|
6569
|
-
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
6570
|
-
this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
|
|
6571
|
-
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
6572
|
-
return this.negotiate(subscriberOffer);
|
|
6573
|
-
}).catch((err) => {
|
|
6043
|
+
this.pc.addEventListener('track', this.handleOnTrack);
|
|
6044
|
+
this.on('subscriberOffer', async (subscriberOffer) => {
|
|
6045
|
+
return this.negotiate(subscriberOffer).catch((err) => {
|
|
6574
6046
|
this.logger('error', `Negotiation failed.`, err);
|
|
6575
6047
|
});
|
|
6576
6048
|
});
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
});
|
|
6587
|
-
});
|
|
6049
|
+
}
|
|
6050
|
+
/**
|
|
6051
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
6052
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
6053
|
+
* instance with a new one (in case of migration).
|
|
6054
|
+
*/
|
|
6055
|
+
detachEventHandlers() {
|
|
6056
|
+
super.detachEventHandlers();
|
|
6057
|
+
this.pc.removeEventListener('track', this.handleOnTrack);
|
|
6588
6058
|
}
|
|
6589
6059
|
}
|
|
6590
6060
|
|
|
@@ -6616,6 +6086,16 @@ const createWebSocketSignalChannel = (opts) => {
|
|
|
6616
6086
|
return ws;
|
|
6617
6087
|
};
|
|
6618
6088
|
|
|
6089
|
+
const toRtcConfiguration = (config) => {
|
|
6090
|
+
return {
|
|
6091
|
+
iceServers: config.map((ice) => ({
|
|
6092
|
+
urls: ice.urls,
|
|
6093
|
+
username: ice.username,
|
|
6094
|
+
credential: ice.password,
|
|
6095
|
+
})),
|
|
6096
|
+
};
|
|
6097
|
+
};
|
|
6098
|
+
|
|
6619
6099
|
/**
|
|
6620
6100
|
* Saving a long-lived reference to a promise that can reject can be unsafe,
|
|
6621
6101
|
* since rejecting the promise causes an unhandled rejection error (even if the
|
|
@@ -6900,6 +6380,7 @@ class StreamSfuClient {
|
|
|
6900
6380
|
clearTimeout(this.migrateAwayTimeout);
|
|
6901
6381
|
this.abortController.abort();
|
|
6902
6382
|
this.migrationTask?.resolve();
|
|
6383
|
+
this.iceTrickleBuffer.dispose();
|
|
6903
6384
|
};
|
|
6904
6385
|
this.leaveAndClose = async (reason) => {
|
|
6905
6386
|
await this.joinTask;
|
|
@@ -6932,13 +6413,9 @@ class StreamSfuClient {
|
|
|
6932
6413
|
await this.joinTask;
|
|
6933
6414
|
return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
6934
6415
|
};
|
|
6935
|
-
this.
|
|
6936
|
-
await this.joinTask;
|
|
6937
|
-
return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
|
|
6938
|
-
};
|
|
6939
|
-
this.updateMuteStates = async (data) => {
|
|
6416
|
+
this.updateMuteStates = async (muteStates) => {
|
|
6940
6417
|
await this.joinTask;
|
|
6941
|
-
return retryable(() => this.rpc.updateMuteStates({
|
|
6418
|
+
return retryable(() => this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }), this.abortController.signal);
|
|
6942
6419
|
};
|
|
6943
6420
|
this.sendStats = async (stats) => {
|
|
6944
6421
|
await this.joinTask;
|
|
@@ -7118,16 +6595,6 @@ StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
|
7118
6595
|
*/
|
|
7119
6596
|
StreamSfuClient.DISPOSE_OLD_SOCKET = 4002;
|
|
7120
6597
|
|
|
7121
|
-
const toRtcConfiguration = (config) => {
|
|
7122
|
-
return {
|
|
7123
|
-
iceServers: config.map((ice) => ({
|
|
7124
|
-
urls: ice.urls,
|
|
7125
|
-
username: ice.username,
|
|
7126
|
-
credential: ice.password,
|
|
7127
|
-
})),
|
|
7128
|
-
};
|
|
7129
|
-
};
|
|
7130
|
-
|
|
7131
6598
|
/**
|
|
7132
6599
|
* Event handler that watched the delivery of `call.accepted`.
|
|
7133
6600
|
* Once the event is received, the call is joined.
|
|
@@ -7346,6 +6813,21 @@ const handleRemoteSoftMute = (call) => {
|
|
|
7346
6813
|
});
|
|
7347
6814
|
};
|
|
7348
6815
|
|
|
6816
|
+
/**
|
|
6817
|
+
* Adds unique values to an array.
|
|
6818
|
+
*
|
|
6819
|
+
* @param arr the array to add to.
|
|
6820
|
+
* @param values the values to add.
|
|
6821
|
+
*/
|
|
6822
|
+
const pushToIfMissing = (arr, ...values) => {
|
|
6823
|
+
for (const v of values) {
|
|
6824
|
+
if (!arr.includes(v)) {
|
|
6825
|
+
arr.push(v);
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
return arr;
|
|
6829
|
+
};
|
|
6830
|
+
|
|
7349
6831
|
/**
|
|
7350
6832
|
* An event responder which handles the `participantJoined` event.
|
|
7351
6833
|
*/
|
|
@@ -7411,7 +6893,7 @@ const watchTrackPublished = (state) => {
|
|
|
7411
6893
|
}
|
|
7412
6894
|
else {
|
|
7413
6895
|
state.updateParticipant(sessionId, (p) => ({
|
|
7414
|
-
publishedTracks: [...p.publishedTracks, type
|
|
6896
|
+
publishedTracks: pushToIfMissing([...p.publishedTracks], type),
|
|
7415
6897
|
}));
|
|
7416
6898
|
}
|
|
7417
6899
|
};
|
|
@@ -7436,7 +6918,6 @@ const watchTrackUnpublished = (state) => {
|
|
|
7436
6918
|
}
|
|
7437
6919
|
};
|
|
7438
6920
|
};
|
|
7439
|
-
const unique = (v, i, arr) => arr.indexOf(v) === i;
|
|
7440
6921
|
/**
|
|
7441
6922
|
* Reconciles orphaned tracks (if any) for the given participant.
|
|
7442
6923
|
*
|
|
@@ -7586,6 +7067,38 @@ const getSdkVersion = (sdk) => {
|
|
|
7586
7067
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
7587
7068
|
};
|
|
7588
7069
|
|
|
7070
|
+
/**
|
|
7071
|
+
* Checks whether the current browser is Safari.
|
|
7072
|
+
*/
|
|
7073
|
+
const isSafari = () => {
|
|
7074
|
+
if (typeof navigator === 'undefined')
|
|
7075
|
+
return false;
|
|
7076
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
|
|
7077
|
+
};
|
|
7078
|
+
/**
|
|
7079
|
+
* Checks whether the current browser is Firefox.
|
|
7080
|
+
*/
|
|
7081
|
+
const isFirefox = () => {
|
|
7082
|
+
if (typeof navigator === 'undefined')
|
|
7083
|
+
return false;
|
|
7084
|
+
return navigator.userAgent?.includes('Firefox');
|
|
7085
|
+
};
|
|
7086
|
+
/**
|
|
7087
|
+
* Checks whether the current browser is Google Chrome.
|
|
7088
|
+
*/
|
|
7089
|
+
const isChrome = () => {
|
|
7090
|
+
if (typeof navigator === 'undefined')
|
|
7091
|
+
return false;
|
|
7092
|
+
return navigator.userAgent?.includes('Chrome');
|
|
7093
|
+
};
|
|
7094
|
+
|
|
7095
|
+
var browsers = /*#__PURE__*/Object.freeze({
|
|
7096
|
+
__proto__: null,
|
|
7097
|
+
isChrome: isChrome,
|
|
7098
|
+
isFirefox: isFirefox,
|
|
7099
|
+
isSafari: isSafari
|
|
7100
|
+
});
|
|
7101
|
+
|
|
7589
7102
|
/**
|
|
7590
7103
|
* Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
|
|
7591
7104
|
*/
|
|
@@ -7602,12 +7115,12 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7602
7115
|
return undefined;
|
|
7603
7116
|
}
|
|
7604
7117
|
};
|
|
7605
|
-
const getStatsForStream = async (kind,
|
|
7118
|
+
const getStatsForStream = async (kind, tracks) => {
|
|
7606
7119
|
const pc = kind === 'subscriber' ? subscriber : publisher;
|
|
7607
7120
|
if (!pc)
|
|
7608
7121
|
return [];
|
|
7609
7122
|
const statsForStream = [];
|
|
7610
|
-
for (
|
|
7123
|
+
for (const track of tracks) {
|
|
7611
7124
|
const report = await pc.getStats(track);
|
|
7612
7125
|
const stats = transform(report, {
|
|
7613
7126
|
// @ts-ignore
|
|
@@ -7632,26 +7145,24 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7632
7145
|
*/
|
|
7633
7146
|
const run = async () => {
|
|
7634
7147
|
const participantStats = {};
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
for (
|
|
7148
|
+
if (sessionIdsToTrack.size > 0) {
|
|
7149
|
+
const sessionIds = new Set(sessionIdsToTrack);
|
|
7150
|
+
for (const participant of state.participants) {
|
|
7638
7151
|
if (!sessionIds.has(participant.sessionId))
|
|
7639
7152
|
continue;
|
|
7640
|
-
const
|
|
7641
|
-
|
|
7642
|
-
: 'subscriber';
|
|
7153
|
+
const { audioStream, isLocalParticipant, sessionId, userId, videoStream, } = participant;
|
|
7154
|
+
const kind = isLocalParticipant ? 'publisher' : 'subscriber';
|
|
7643
7155
|
try {
|
|
7644
|
-
const
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
});
|
|
7156
|
+
const tracks = isLocalParticipant
|
|
7157
|
+
? publisher?.getPublishedTracks() || []
|
|
7158
|
+
: [
|
|
7159
|
+
...(videoStream?.getVideoTracks() || []),
|
|
7160
|
+
...(audioStream?.getAudioTracks() || []),
|
|
7161
|
+
];
|
|
7162
|
+
participantStats[sessionId] = await getStatsForStream(kind, tracks);
|
|
7652
7163
|
}
|
|
7653
7164
|
catch (e) {
|
|
7654
|
-
logger('
|
|
7165
|
+
logger('warn', `Failed to collect ${kind} stats for ${userId}`, e);
|
|
7655
7166
|
}
|
|
7656
7167
|
}
|
|
7657
7168
|
}
|
|
@@ -7661,6 +7172,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7661
7172
|
.then((report) => transform(report, {
|
|
7662
7173
|
kind: 'subscriber',
|
|
7663
7174
|
trackKind: 'video',
|
|
7175
|
+
publisher,
|
|
7664
7176
|
}))
|
|
7665
7177
|
.then(aggregate),
|
|
7666
7178
|
publisher
|
|
@@ -7669,6 +7181,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7669
7181
|
.then((report) => transform(report, {
|
|
7670
7182
|
kind: 'publisher',
|
|
7671
7183
|
trackKind: 'video',
|
|
7184
|
+
publisher,
|
|
7672
7185
|
}))
|
|
7673
7186
|
.then(aggregate)
|
|
7674
7187
|
: getEmptyStats(),
|
|
@@ -7717,7 +7230,7 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
|
|
|
7717
7230
|
* @param opts the transform options.
|
|
7718
7231
|
*/
|
|
7719
7232
|
const transform = (report, opts) => {
|
|
7720
|
-
const { trackKind, kind } = opts;
|
|
7233
|
+
const { trackKind, kind, publisher } = opts;
|
|
7721
7234
|
const direction = kind === 'subscriber' ? 'inbound-rtp' : 'outbound-rtp';
|
|
7722
7235
|
const stats = flatten(report);
|
|
7723
7236
|
const streams = stats
|
|
@@ -7733,6 +7246,16 @@ const transform = (report, opts) => {
|
|
|
7733
7246
|
s.id === transport.selectedCandidatePairId);
|
|
7734
7247
|
roundTripTime = candidatePair?.currentRoundTripTime;
|
|
7735
7248
|
}
|
|
7249
|
+
let trackType;
|
|
7250
|
+
if (kind === 'publisher' && publisher) {
|
|
7251
|
+
const firefox = isFirefox();
|
|
7252
|
+
const mediaSource = stats.find((s) => s.type === 'media-source' &&
|
|
7253
|
+
// Firefox doesn't have mediaSourceId, so we need to guess the media source
|
|
7254
|
+
(firefox ? true : s.id === rtcStreamStats.mediaSourceId));
|
|
7255
|
+
if (mediaSource) {
|
|
7256
|
+
trackType = publisher.getTrackType(mediaSource.trackIdentifier);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7736
7259
|
return {
|
|
7737
7260
|
bytesSent: rtcStreamStats.bytesSent,
|
|
7738
7261
|
bytesReceived: rtcStreamStats.bytesReceived,
|
|
@@ -7743,10 +7266,12 @@ const transform = (report, opts) => {
|
|
|
7743
7266
|
framesPerSecond: rtcStreamStats.framesPerSecond,
|
|
7744
7267
|
jitter: rtcStreamStats.jitter,
|
|
7745
7268
|
kind: rtcStreamStats.kind,
|
|
7269
|
+
mediaSourceId: rtcStreamStats.mediaSourceId,
|
|
7746
7270
|
// @ts-ignore: available in Chrome only, TS doesn't recognize this
|
|
7747
7271
|
qualityLimitationReason: rtcStreamStats.qualityLimitationReason,
|
|
7748
7272
|
rid: rtcStreamStats.rid,
|
|
7749
7273
|
ssrc: rtcStreamStats.ssrc,
|
|
7274
|
+
trackType,
|
|
7750
7275
|
};
|
|
7751
7276
|
});
|
|
7752
7277
|
return {
|
|
@@ -7767,6 +7292,7 @@ const getEmptyStats = (stats) => {
|
|
|
7767
7292
|
highestFrameHeight: 0,
|
|
7768
7293
|
highestFramesPerSecond: 0,
|
|
7769
7294
|
codec: '',
|
|
7295
|
+
codecPerTrackType: {},
|
|
7770
7296
|
timestamp: Date.now(),
|
|
7771
7297
|
};
|
|
7772
7298
|
};
|
|
@@ -7802,6 +7328,12 @@ const aggregate = (stats) => {
|
|
|
7802
7328
|
report.averageRoundTripTimeInMs = Math.round((report.averageRoundTripTimeInMs / streams.length) * 1000);
|
|
7803
7329
|
// we take the first codec we find, as it should be the same for all streams
|
|
7804
7330
|
report.codec = streams[0].codec || '';
|
|
7331
|
+
report.codecPerTrackType = streams.reduce((acc, stream) => {
|
|
7332
|
+
if (stream.trackType) {
|
|
7333
|
+
acc[stream.trackType] = stream.codec || '';
|
|
7334
|
+
}
|
|
7335
|
+
return acc;
|
|
7336
|
+
}, {});
|
|
7805
7337
|
}
|
|
7806
7338
|
const qualityLimitationReason = [
|
|
7807
7339
|
qualityLimitationReasons.has('cpu') && 'cpu',
|
|
@@ -7813,7 +7345,135 @@ const aggregate = (stats) => {
|
|
|
7813
7345
|
if (qualityLimitationReason) {
|
|
7814
7346
|
report.qualityLimitationReasons = qualityLimitationReason;
|
|
7815
7347
|
}
|
|
7816
|
-
return report;
|
|
7348
|
+
return report;
|
|
7349
|
+
};
|
|
7350
|
+
|
|
7351
|
+
const version = "1.15.1";
|
|
7352
|
+
const [major, minor, patch] = version.split('.');
|
|
7353
|
+
let sdkInfo = {
|
|
7354
|
+
type: SdkType.PLAIN_JAVASCRIPT,
|
|
7355
|
+
major,
|
|
7356
|
+
minor,
|
|
7357
|
+
patch,
|
|
7358
|
+
};
|
|
7359
|
+
let osInfo;
|
|
7360
|
+
let deviceInfo;
|
|
7361
|
+
let webRtcInfo;
|
|
7362
|
+
let deviceState = { oneofKind: undefined };
|
|
7363
|
+
const setSdkInfo = (info) => {
|
|
7364
|
+
sdkInfo = info;
|
|
7365
|
+
};
|
|
7366
|
+
const getSdkInfo = () => {
|
|
7367
|
+
return sdkInfo;
|
|
7368
|
+
};
|
|
7369
|
+
const setOSInfo = (info) => {
|
|
7370
|
+
osInfo = info;
|
|
7371
|
+
};
|
|
7372
|
+
const getOSInfo = () => {
|
|
7373
|
+
return osInfo;
|
|
7374
|
+
};
|
|
7375
|
+
const setDeviceInfo = (info) => {
|
|
7376
|
+
deviceInfo = info;
|
|
7377
|
+
};
|
|
7378
|
+
const getDeviceInfo = () => {
|
|
7379
|
+
return deviceInfo;
|
|
7380
|
+
};
|
|
7381
|
+
const getWebRTCInfo = () => {
|
|
7382
|
+
return webRtcInfo;
|
|
7383
|
+
};
|
|
7384
|
+
const setWebRTCInfo = (info) => {
|
|
7385
|
+
webRtcInfo = info;
|
|
7386
|
+
};
|
|
7387
|
+
const setThermalState = (state) => {
|
|
7388
|
+
if (!osInfo) {
|
|
7389
|
+
deviceState = { oneofKind: undefined };
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7392
|
+
if (osInfo.name === 'android') {
|
|
7393
|
+
const thermalState = AndroidThermalState[state] ||
|
|
7394
|
+
AndroidThermalState.UNSPECIFIED;
|
|
7395
|
+
deviceState = {
|
|
7396
|
+
oneofKind: 'android',
|
|
7397
|
+
android: {
|
|
7398
|
+
thermalState,
|
|
7399
|
+
isPowerSaverMode: deviceState?.oneofKind === 'android' &&
|
|
7400
|
+
deviceState.android.isPowerSaverMode,
|
|
7401
|
+
},
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
if (osInfo.name.toLowerCase() === 'ios') {
|
|
7405
|
+
const thermalState = AppleThermalState[state] ||
|
|
7406
|
+
AppleThermalState.UNSPECIFIED;
|
|
7407
|
+
deviceState = {
|
|
7408
|
+
oneofKind: 'apple',
|
|
7409
|
+
apple: {
|
|
7410
|
+
thermalState,
|
|
7411
|
+
isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
|
|
7412
|
+
deviceState.apple.isLowPowerModeEnabled,
|
|
7413
|
+
},
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
};
|
|
7417
|
+
const setPowerState = (powerMode) => {
|
|
7418
|
+
if (!osInfo) {
|
|
7419
|
+
deviceState = { oneofKind: undefined };
|
|
7420
|
+
return;
|
|
7421
|
+
}
|
|
7422
|
+
if (osInfo.name === 'android') {
|
|
7423
|
+
deviceState = {
|
|
7424
|
+
oneofKind: 'android',
|
|
7425
|
+
android: {
|
|
7426
|
+
thermalState: deviceState?.oneofKind === 'android'
|
|
7427
|
+
? deviceState.android.thermalState
|
|
7428
|
+
: AndroidThermalState.UNSPECIFIED,
|
|
7429
|
+
isPowerSaverMode: powerMode,
|
|
7430
|
+
},
|
|
7431
|
+
};
|
|
7432
|
+
}
|
|
7433
|
+
if (osInfo.name.toLowerCase() === 'ios') {
|
|
7434
|
+
deviceState = {
|
|
7435
|
+
oneofKind: 'apple',
|
|
7436
|
+
apple: {
|
|
7437
|
+
thermalState: deviceState?.oneofKind === 'apple'
|
|
7438
|
+
? deviceState.apple.thermalState
|
|
7439
|
+
: AppleThermalState.UNSPECIFIED,
|
|
7440
|
+
isLowPowerModeEnabled: powerMode,
|
|
7441
|
+
},
|
|
7442
|
+
};
|
|
7443
|
+
}
|
|
7444
|
+
};
|
|
7445
|
+
const getDeviceState = () => {
|
|
7446
|
+
return deviceState;
|
|
7447
|
+
};
|
|
7448
|
+
const getClientDetails = () => {
|
|
7449
|
+
if (isReactNative()) {
|
|
7450
|
+
// Since RN doesn't support web, sharing browser info is not required
|
|
7451
|
+
return {
|
|
7452
|
+
sdk: getSdkInfo(),
|
|
7453
|
+
os: getOSInfo(),
|
|
7454
|
+
device: getDeviceInfo(),
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
7458
|
+
const { browser, os, device, cpu } = userAgent.getResult();
|
|
7459
|
+
return {
|
|
7460
|
+
sdk: getSdkInfo(),
|
|
7461
|
+
browser: {
|
|
7462
|
+
name: browser.name || navigator.userAgent,
|
|
7463
|
+
version: browser.version || '',
|
|
7464
|
+
},
|
|
7465
|
+
os: {
|
|
7466
|
+
name: os.name || '',
|
|
7467
|
+
version: os.version || '',
|
|
7468
|
+
architecture: cpu.architecture || '',
|
|
7469
|
+
},
|
|
7470
|
+
device: {
|
|
7471
|
+
name: [device.vendor, device.model, device.type]
|
|
7472
|
+
.filter(Boolean)
|
|
7473
|
+
.join(' '),
|
|
7474
|
+
version: '',
|
|
7475
|
+
},
|
|
7476
|
+
};
|
|
7817
7477
|
};
|
|
7818
7478
|
|
|
7819
7479
|
class SfuStatsReporter {
|
|
@@ -7849,8 +7509,28 @@ class SfuStatsReporter {
|
|
|
7849
7509
|
});
|
|
7850
7510
|
});
|
|
7851
7511
|
};
|
|
7852
|
-
this.
|
|
7853
|
-
|
|
7512
|
+
this.sendConnectionTime = (connectionTimeSeconds) => {
|
|
7513
|
+
this.sendTelemetryData({
|
|
7514
|
+
data: {
|
|
7515
|
+
oneofKind: 'connectionTimeSeconds',
|
|
7516
|
+
connectionTimeSeconds,
|
|
7517
|
+
},
|
|
7518
|
+
});
|
|
7519
|
+
};
|
|
7520
|
+
this.sendReconnectionTime = (strategy, timeSeconds) => {
|
|
7521
|
+
this.sendTelemetryData({
|
|
7522
|
+
data: {
|
|
7523
|
+
oneofKind: 'reconnection',
|
|
7524
|
+
reconnection: { strategy, timeSeconds },
|
|
7525
|
+
},
|
|
7526
|
+
});
|
|
7527
|
+
};
|
|
7528
|
+
this.sendTelemetryData = (telemetryData) => {
|
|
7529
|
+
// intentionally not awaiting the promise here
|
|
7530
|
+
// to avoid impeding with the ongoing actions.
|
|
7531
|
+
this.run(telemetryData).catch((err) => {
|
|
7532
|
+
this.logger('warn', 'Failed to send telemetry data', err);
|
|
7533
|
+
});
|
|
7854
7534
|
};
|
|
7855
7535
|
this.run = async (telemetryData) => {
|
|
7856
7536
|
const [subscriberStats, publisherStats] = await Promise.all([
|
|
@@ -8418,6 +8098,25 @@ class PermissionsContext {
|
|
|
8418
8098
|
this.hasPermission = (permission) => {
|
|
8419
8099
|
return this.permissions.includes(permission);
|
|
8420
8100
|
};
|
|
8101
|
+
/**
|
|
8102
|
+
* Helper method that checks whether the current user has the permission
|
|
8103
|
+
* to publish the given track type.
|
|
8104
|
+
*/
|
|
8105
|
+
this.canPublish = (trackType) => {
|
|
8106
|
+
switch (trackType) {
|
|
8107
|
+
case TrackType.AUDIO:
|
|
8108
|
+
return this.hasPermission(OwnCapability.SEND_AUDIO);
|
|
8109
|
+
case TrackType.VIDEO:
|
|
8110
|
+
return this.hasPermission(OwnCapability.SEND_VIDEO);
|
|
8111
|
+
case TrackType.SCREEN_SHARE:
|
|
8112
|
+
case TrackType.SCREEN_SHARE_AUDIO:
|
|
8113
|
+
return this.hasPermission(OwnCapability.SCREENSHARE);
|
|
8114
|
+
case TrackType.UNSPECIFIED:
|
|
8115
|
+
return false;
|
|
8116
|
+
default:
|
|
8117
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
8118
|
+
}
|
|
8119
|
+
};
|
|
8421
8120
|
/**
|
|
8422
8121
|
* Checks if the current user can request a specific permission
|
|
8423
8122
|
* within the call.
|
|
@@ -8874,6 +8573,14 @@ const disposeOfMediaStream = (stream) => {
|
|
|
8874
8573
|
}
|
|
8875
8574
|
};
|
|
8876
8575
|
|
|
8576
|
+
/**
|
|
8577
|
+
* Checks if the current platform is a mobile device.
|
|
8578
|
+
*
|
|
8579
|
+
* See:
|
|
8580
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
|
8581
|
+
*/
|
|
8582
|
+
const isMobile = () => /Mobi/i.test(navigator.userAgent);
|
|
8583
|
+
|
|
8877
8584
|
class InputMediaDeviceManager {
|
|
8878
8585
|
constructor(call, state, trackType) {
|
|
8879
8586
|
this.call = call;
|
|
@@ -9056,36 +8763,42 @@ class InputMediaDeviceManager {
|
|
|
9056
8763
|
}
|
|
9057
8764
|
});
|
|
9058
8765
|
}
|
|
8766
|
+
publishStream(stream) {
|
|
8767
|
+
return this.call.publish(stream, this.trackType);
|
|
8768
|
+
}
|
|
8769
|
+
stopPublishStream() {
|
|
8770
|
+
return this.call.stopPublish(this.trackType);
|
|
8771
|
+
}
|
|
9059
8772
|
getTracks() {
|
|
9060
8773
|
return this.state.mediaStream?.getTracks() ?? [];
|
|
9061
8774
|
}
|
|
9062
8775
|
async muteStream(stopTracks = true) {
|
|
9063
|
-
|
|
8776
|
+
const mediaStream = this.state.mediaStream;
|
|
8777
|
+
if (!mediaStream)
|
|
9064
8778
|
return;
|
|
9065
8779
|
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
9066
8780
|
if (this.call.state.callingState === exports.CallingState.JOINED) {
|
|
9067
|
-
await this.stopPublishStream(
|
|
8781
|
+
await this.stopPublishStream();
|
|
9068
8782
|
}
|
|
9069
8783
|
this.muteLocalStream(stopTracks);
|
|
9070
8784
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
9071
8785
|
if (allEnded) {
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
typeof this.state.mediaStream.release === 'function') {
|
|
8786
|
+
// @ts-expect-error release() is present in react-native-webrtc
|
|
8787
|
+
if (typeof mediaStream.release === 'function') {
|
|
9075
8788
|
// @ts-expect-error called to dispose the stream in RN
|
|
9076
|
-
|
|
8789
|
+
mediaStream.release();
|
|
9077
8790
|
}
|
|
9078
8791
|
this.state.setMediaStream(undefined, undefined);
|
|
9079
8792
|
this.filters.forEach((entry) => entry.stop?.());
|
|
9080
8793
|
}
|
|
9081
8794
|
}
|
|
9082
|
-
|
|
8795
|
+
disableTracks() {
|
|
9083
8796
|
this.getTracks().forEach((track) => {
|
|
9084
8797
|
if (track.enabled)
|
|
9085
8798
|
track.enabled = false;
|
|
9086
8799
|
});
|
|
9087
8800
|
}
|
|
9088
|
-
|
|
8801
|
+
enableTracks() {
|
|
9089
8802
|
this.getTracks().forEach((track) => {
|
|
9090
8803
|
if (!track.enabled)
|
|
9091
8804
|
track.enabled = true;
|
|
@@ -9105,7 +8818,7 @@ class InputMediaDeviceManager {
|
|
|
9105
8818
|
this.stopTracks();
|
|
9106
8819
|
}
|
|
9107
8820
|
else {
|
|
9108
|
-
this.
|
|
8821
|
+
this.disableTracks();
|
|
9109
8822
|
}
|
|
9110
8823
|
}
|
|
9111
8824
|
async unmuteStream() {
|
|
@@ -9115,7 +8828,7 @@ class InputMediaDeviceManager {
|
|
|
9115
8828
|
if (this.state.mediaStream &&
|
|
9116
8829
|
this.getTracks().every((t) => t.readyState === 'live')) {
|
|
9117
8830
|
stream = this.state.mediaStream;
|
|
9118
|
-
this.
|
|
8831
|
+
this.enableTracks();
|
|
9119
8832
|
}
|
|
9120
8833
|
else {
|
|
9121
8834
|
const defaultConstraints = this.state.defaultConstraints;
|
|
@@ -9209,9 +8922,24 @@ class InputMediaDeviceManager {
|
|
|
9209
8922
|
await this.disable();
|
|
9210
8923
|
}
|
|
9211
8924
|
};
|
|
9212
|
-
|
|
8925
|
+
const createTrackMuteHandler = (muted) => () => {
|
|
8926
|
+
if (!isMobile() || this.trackType !== TrackType.VIDEO)
|
|
8927
|
+
return;
|
|
8928
|
+
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
8929
|
+
this.logger('warn', 'Error while notifying track mute state', err);
|
|
8930
|
+
});
|
|
8931
|
+
};
|
|
8932
|
+
stream.getTracks().forEach((track) => {
|
|
8933
|
+
const muteHandler = createTrackMuteHandler(true);
|
|
8934
|
+
const unmuteHandler = createTrackMuteHandler(false);
|
|
8935
|
+
track.addEventListener('mute', muteHandler);
|
|
8936
|
+
track.addEventListener('unmute', unmuteHandler);
|
|
9213
8937
|
track.addEventListener('ended', handleTrackEnded);
|
|
9214
|
-
this.subscriptions.push(() =>
|
|
8938
|
+
this.subscriptions.push(() => {
|
|
8939
|
+
track.removeEventListener('mute', muteHandler);
|
|
8940
|
+
track.removeEventListener('unmute', unmuteHandler);
|
|
8941
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
8942
|
+
});
|
|
9215
8943
|
});
|
|
9216
8944
|
}
|
|
9217
8945
|
}
|
|
@@ -9235,8 +8963,8 @@ class InputMediaDeviceManager {
|
|
|
9235
8963
|
await this.statusChangeSettled();
|
|
9236
8964
|
let isDeviceDisconnected = false;
|
|
9237
8965
|
let isDeviceReplaced = false;
|
|
9238
|
-
const currentDevice = this.
|
|
9239
|
-
const prevDevice = this.
|
|
8966
|
+
const currentDevice = this.findDevice(currentDevices, deviceId);
|
|
8967
|
+
const prevDevice = this.findDevice(prevDevices, deviceId);
|
|
9240
8968
|
if (!currentDevice && prevDevice) {
|
|
9241
8969
|
isDeviceDisconnected = true;
|
|
9242
8970
|
}
|
|
@@ -9266,8 +8994,9 @@ class InputMediaDeviceManager {
|
|
|
9266
8994
|
}
|
|
9267
8995
|
}));
|
|
9268
8996
|
}
|
|
9269
|
-
|
|
9270
|
-
|
|
8997
|
+
findDevice(devices, deviceId) {
|
|
8998
|
+
const kind = this.mediaDeviceKind;
|
|
8999
|
+
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
9271
9000
|
}
|
|
9272
9001
|
}
|
|
9273
9002
|
|
|
@@ -9453,14 +9182,6 @@ class CameraManagerState extends InputMediaDeviceManagerState {
|
|
|
9453
9182
|
}
|
|
9454
9183
|
}
|
|
9455
9184
|
|
|
9456
|
-
/**
|
|
9457
|
-
* Checks if the current platform is a mobile device.
|
|
9458
|
-
*
|
|
9459
|
-
* See:
|
|
9460
|
-
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
|
9461
|
-
*/
|
|
9462
|
-
const isMobile = () => /Mobi/i.test(navigator.userAgent);
|
|
9463
|
-
|
|
9464
9185
|
class CameraManager extends InputMediaDeviceManager {
|
|
9465
9186
|
/**
|
|
9466
9187
|
* Constructs a new CameraManager.
|
|
@@ -9531,14 +9252,35 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9531
9252
|
}
|
|
9532
9253
|
}
|
|
9533
9254
|
/**
|
|
9534
|
-
*
|
|
9255
|
+
* Applies the video settings to the camera.
|
|
9535
9256
|
*
|
|
9536
|
-
* @
|
|
9537
|
-
* @
|
|
9538
|
-
* @param codec the codec to use for encoding the video.
|
|
9257
|
+
* @param settings the video settings to apply.
|
|
9258
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9539
9259
|
*/
|
|
9540
|
-
|
|
9541
|
-
this.call.
|
|
9260
|
+
async apply(settings, publish) {
|
|
9261
|
+
const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
|
|
9262
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9263
|
+
if (hasPublishedVideo || !hasPermission)
|
|
9264
|
+
return;
|
|
9265
|
+
// Wait for any in progress camera operation
|
|
9266
|
+
await this.statusChangeSettled();
|
|
9267
|
+
const { target_resolution, camera_facing, camera_default_on } = settings;
|
|
9268
|
+
await this.selectTargetResolution(target_resolution);
|
|
9269
|
+
// Set camera direction if it's not yet set
|
|
9270
|
+
if (!this.state.direction && !this.state.selectedDevice) {
|
|
9271
|
+
this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
|
|
9272
|
+
}
|
|
9273
|
+
if (!publish)
|
|
9274
|
+
return;
|
|
9275
|
+
const { mediaStream } = this.state;
|
|
9276
|
+
if (this.enabled && mediaStream) {
|
|
9277
|
+
// The camera is already enabled (e.g. lobby screen). Publish the stream
|
|
9278
|
+
await this.publishStream(mediaStream);
|
|
9279
|
+
}
|
|
9280
|
+
else if (this.state.status === undefined && camera_default_on) {
|
|
9281
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
9282
|
+
await this.enable();
|
|
9283
|
+
}
|
|
9542
9284
|
}
|
|
9543
9285
|
getDevices() {
|
|
9544
9286
|
return getVideoDevices();
|
|
@@ -9556,12 +9298,6 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9556
9298
|
}
|
|
9557
9299
|
return getVideoStream(constraints);
|
|
9558
9300
|
}
|
|
9559
|
-
publishStream(stream) {
|
|
9560
|
-
return this.call.publishVideoStream(stream);
|
|
9561
|
-
}
|
|
9562
|
-
stopPublishStream(stopTracks) {
|
|
9563
|
-
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
9564
|
-
}
|
|
9565
9301
|
}
|
|
9566
9302
|
|
|
9567
9303
|
class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
@@ -9889,18 +9625,37 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
9889
9625
|
this.speakingWhileMutedNotificationEnabled = false;
|
|
9890
9626
|
await this.stopSpeakingWhileMutedDetection();
|
|
9891
9627
|
}
|
|
9628
|
+
/**
|
|
9629
|
+
* Applies the audio settings to the microphone.
|
|
9630
|
+
* @param settings the audio settings to apply.
|
|
9631
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9632
|
+
*/
|
|
9633
|
+
async apply(settings, publish) {
|
|
9634
|
+
if (!publish)
|
|
9635
|
+
return;
|
|
9636
|
+
const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
|
|
9637
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9638
|
+
if (hasPublishedAudio || !hasPermission)
|
|
9639
|
+
return;
|
|
9640
|
+
// Wait for any in progress mic operation
|
|
9641
|
+
await this.statusChangeSettled();
|
|
9642
|
+
// Publish media stream that was set before we joined
|
|
9643
|
+
const { mediaStream } = this.state;
|
|
9644
|
+
if (this.enabled && mediaStream) {
|
|
9645
|
+
// The mic is already enabled (e.g. lobby screen). Publish the stream
|
|
9646
|
+
await this.publishStream(mediaStream);
|
|
9647
|
+
}
|
|
9648
|
+
else if (this.state.status === undefined && settings.mic_default_on) {
|
|
9649
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
9650
|
+
await this.enable();
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9892
9653
|
getDevices() {
|
|
9893
9654
|
return getAudioDevices();
|
|
9894
9655
|
}
|
|
9895
9656
|
getStream(constraints) {
|
|
9896
9657
|
return getAudioStream(constraints);
|
|
9897
9658
|
}
|
|
9898
|
-
publishStream(stream) {
|
|
9899
|
-
return this.call.publishAudioStream(stream);
|
|
9900
|
-
}
|
|
9901
|
-
stopPublishStream(stopTracks) {
|
|
9902
|
-
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
9903
|
-
}
|
|
9904
9659
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
9905
9660
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
9906
9661
|
await this.stopSpeakingWhileMutedDetection();
|
|
@@ -10020,7 +9775,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10020
9775
|
async disableScreenShareAudio() {
|
|
10021
9776
|
this.state.setAudioEnabled(false);
|
|
10022
9777
|
if (this.call.publisher?.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
10023
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO
|
|
9778
|
+
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO);
|
|
10024
9779
|
}
|
|
10025
9780
|
}
|
|
10026
9781
|
/**
|
|
@@ -10046,12 +9801,8 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10046
9801
|
}
|
|
10047
9802
|
return getScreenShareStream(constraints);
|
|
10048
9803
|
}
|
|
10049
|
-
|
|
10050
|
-
return this.call.
|
|
10051
|
-
}
|
|
10052
|
-
async stopPublishStream(stopTracks) {
|
|
10053
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE, stopTracks);
|
|
10054
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO, stopTracks);
|
|
9804
|
+
async stopPublishStream() {
|
|
9805
|
+
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
10055
9806
|
}
|
|
10056
9807
|
/**
|
|
10057
9808
|
* Overrides the default `select` method to throw an error.
|
|
@@ -10261,6 +10012,112 @@ class Call {
|
|
|
10261
10012
|
*/
|
|
10262
10013
|
this.leaveCallHooks = new Set();
|
|
10263
10014
|
this.streamClientEventHandlers = new Map();
|
|
10015
|
+
this.setup = async () => {
|
|
10016
|
+
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
10017
|
+
if (this.initialized)
|
|
10018
|
+
return;
|
|
10019
|
+
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
10020
|
+
// update state with the latest event data
|
|
10021
|
+
this.state.updateFromEvent(event);
|
|
10022
|
+
}));
|
|
10023
|
+
this.leaveCallHooks.add(this.on('changePublishOptions', (event) => {
|
|
10024
|
+
this.currentPublishOptions = event.publishOptions;
|
|
10025
|
+
}));
|
|
10026
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
10027
|
+
this.registerEffects();
|
|
10028
|
+
this.registerReconnectHandlers();
|
|
10029
|
+
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
10030
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10031
|
+
}
|
|
10032
|
+
this.initialized = true;
|
|
10033
|
+
});
|
|
10034
|
+
};
|
|
10035
|
+
this.registerEffects = () => {
|
|
10036
|
+
this.leaveCallHooks.add(
|
|
10037
|
+
// handles updating the permissions context when the settings change.
|
|
10038
|
+
createSubscription(this.state.settings$, (settings) => {
|
|
10039
|
+
if (!settings)
|
|
10040
|
+
return;
|
|
10041
|
+
this.permissionsContext.setCallSettings(settings);
|
|
10042
|
+
}));
|
|
10043
|
+
this.leaveCallHooks.add(
|
|
10044
|
+
// handle the case when the user permissions are modified.
|
|
10045
|
+
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
10046
|
+
this.leaveCallHooks.add(
|
|
10047
|
+
// handles the case when the user is blocked by the call owner.
|
|
10048
|
+
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
10049
|
+
if (!blockedUserIds || blockedUserIds.length === 0)
|
|
10050
|
+
return;
|
|
10051
|
+
const currentUserId = this.currentUserId;
|
|
10052
|
+
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
10053
|
+
this.logger('info', 'Leaving call because of being blocked');
|
|
10054
|
+
await this.leave({ reason: 'user blocked' }).catch((err) => {
|
|
10055
|
+
this.logger('error', 'Error leaving call after being blocked', err);
|
|
10056
|
+
});
|
|
10057
|
+
}
|
|
10058
|
+
}));
|
|
10059
|
+
this.leaveCallHooks.add(
|
|
10060
|
+
// cancel auto-drop when call is
|
|
10061
|
+
createSubscription(this.state.session$, (session) => {
|
|
10062
|
+
if (!this.ringing)
|
|
10063
|
+
return;
|
|
10064
|
+
const receiverId = this.clientStore.connectedUser?.id;
|
|
10065
|
+
if (!receiverId)
|
|
10066
|
+
return;
|
|
10067
|
+
const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
|
|
10068
|
+
const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
|
|
10069
|
+
if (isAcceptedByMe || isRejectedByMe) {
|
|
10070
|
+
this.cancelAutoDrop();
|
|
10071
|
+
}
|
|
10072
|
+
}));
|
|
10073
|
+
this.leaveCallHooks.add(
|
|
10074
|
+
// "ringing" mode effects and event handlers
|
|
10075
|
+
createSubscription(this.ringingSubject, (isRinging) => {
|
|
10076
|
+
if (!isRinging)
|
|
10077
|
+
return;
|
|
10078
|
+
const callSession = this.state.session;
|
|
10079
|
+
const receiver_id = this.clientStore.connectedUser?.id;
|
|
10080
|
+
const ended_at = callSession?.ended_at;
|
|
10081
|
+
const created_by_id = this.state.createdBy?.id;
|
|
10082
|
+
const rejected_by = callSession?.rejected_by;
|
|
10083
|
+
const accepted_by = callSession?.accepted_by;
|
|
10084
|
+
let leaveCallIdle = false;
|
|
10085
|
+
if (ended_at) {
|
|
10086
|
+
// call was ended before it was accepted or rejected so we should leave it to idle
|
|
10087
|
+
leaveCallIdle = true;
|
|
10088
|
+
}
|
|
10089
|
+
else if (created_by_id && rejected_by) {
|
|
10090
|
+
if (rejected_by[created_by_id]) {
|
|
10091
|
+
// call was cancelled by the caller
|
|
10092
|
+
leaveCallIdle = true;
|
|
10093
|
+
}
|
|
10094
|
+
}
|
|
10095
|
+
else if (receiver_id && rejected_by) {
|
|
10096
|
+
if (rejected_by[receiver_id]) {
|
|
10097
|
+
// call was rejected by the receiver in some other device
|
|
10098
|
+
leaveCallIdle = true;
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
else if (receiver_id && accepted_by) {
|
|
10102
|
+
if (accepted_by[receiver_id]) {
|
|
10103
|
+
// call was accepted by the receiver in some other device
|
|
10104
|
+
leaveCallIdle = true;
|
|
10105
|
+
}
|
|
10106
|
+
}
|
|
10107
|
+
if (leaveCallIdle) {
|
|
10108
|
+
if (this.state.callingState !== exports.CallingState.IDLE) {
|
|
10109
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10110
|
+
}
|
|
10111
|
+
}
|
|
10112
|
+
else {
|
|
10113
|
+
if (this.state.callingState === exports.CallingState.IDLE) {
|
|
10114
|
+
this.state.setCallingState(exports.CallingState.RINGING);
|
|
10115
|
+
}
|
|
10116
|
+
this.scheduleAutoDrop();
|
|
10117
|
+
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
10118
|
+
}
|
|
10119
|
+
}));
|
|
10120
|
+
};
|
|
10264
10121
|
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
10265
10122
|
// update the permission context.
|
|
10266
10123
|
this.permissionsContext.setPermissions(ownCapabilities);
|
|
@@ -10373,9 +10230,9 @@ class Call {
|
|
|
10373
10230
|
this.statsReporter = undefined;
|
|
10374
10231
|
this.sfuStatsReporter?.stop();
|
|
10375
10232
|
this.sfuStatsReporter = undefined;
|
|
10376
|
-
this.subscriber?.
|
|
10233
|
+
this.subscriber?.dispose();
|
|
10377
10234
|
this.subscriber = undefined;
|
|
10378
|
-
this.publisher?.
|
|
10235
|
+
this.publisher?.dispose();
|
|
10379
10236
|
this.publisher = undefined;
|
|
10380
10237
|
await this.sfuClient?.leaveAndClose(reason);
|
|
10381
10238
|
this.sfuClient = undefined;
|
|
@@ -10413,7 +10270,8 @@ class Call {
|
|
|
10413
10270
|
// call.ring event excludes the call creator in the members list
|
|
10414
10271
|
// as the creator does not get the ring event
|
|
10415
10272
|
// so update the member list accordingly
|
|
10416
|
-
const
|
|
10273
|
+
const { created_by, settings } = event.call;
|
|
10274
|
+
const creator = this.state.members.find((m) => m.user.id === created_by.id);
|
|
10417
10275
|
if (!creator) {
|
|
10418
10276
|
this.state.setMembers(event.members);
|
|
10419
10277
|
}
|
|
@@ -10428,7 +10286,7 @@ class Call {
|
|
|
10428
10286
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
10429
10287
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
10430
10288
|
this.clientStore.setCalls([this, ...calls]);
|
|
10431
|
-
await this.applyDeviceConfig(false);
|
|
10289
|
+
await this.applyDeviceConfig(settings, false);
|
|
10432
10290
|
};
|
|
10433
10291
|
/**
|
|
10434
10292
|
* Loads the information about the call.
|
|
@@ -10451,7 +10309,7 @@ class Call {
|
|
|
10451
10309
|
this.watching = true;
|
|
10452
10310
|
this.clientStore.registerCall(this);
|
|
10453
10311
|
}
|
|
10454
|
-
await this.applyDeviceConfig(false);
|
|
10312
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10455
10313
|
return response;
|
|
10456
10314
|
};
|
|
10457
10315
|
/**
|
|
@@ -10473,7 +10331,7 @@ class Call {
|
|
|
10473
10331
|
this.watching = true;
|
|
10474
10332
|
this.clientStore.registerCall(this);
|
|
10475
10333
|
}
|
|
10476
|
-
await this.applyDeviceConfig(false);
|
|
10334
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10477
10335
|
return response;
|
|
10478
10336
|
};
|
|
10479
10337
|
/**
|
|
@@ -10575,19 +10433,32 @@ class Call {
|
|
|
10575
10433
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
10576
10434
|
if (previousSfuClient !== sfuClient) {
|
|
10577
10435
|
// prepare a generic SDP and send it to the SFU.
|
|
10578
|
-
//
|
|
10436
|
+
// these are throw-away SDPs that the SFU will use to determine
|
|
10579
10437
|
// the capabilities of the client (codec support, etc.)
|
|
10580
|
-
const
|
|
10581
|
-
|
|
10438
|
+
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
10439
|
+
getGenericSdp('recvonly'),
|
|
10440
|
+
getGenericSdp('sendonly'),
|
|
10441
|
+
]);
|
|
10442
|
+
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10443
|
+
const reconnectDetails = isReconnecting
|
|
10582
10444
|
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
10583
10445
|
: undefined;
|
|
10584
|
-
const
|
|
10585
|
-
|
|
10586
|
-
|
|
10446
|
+
const preferredPublishOptions = !isReconnecting
|
|
10447
|
+
? this.getPreferredPublishOptions()
|
|
10448
|
+
: this.currentPublishOptions || [];
|
|
10449
|
+
const preferredSubscribeOptions = !isReconnecting
|
|
10450
|
+
? this.getPreferredSubscribeOptions()
|
|
10451
|
+
: [];
|
|
10452
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
10453
|
+
subscriberSdp,
|
|
10454
|
+
publisherSdp,
|
|
10587
10455
|
clientDetails,
|
|
10588
10456
|
fastReconnect: performingFastReconnect,
|
|
10589
10457
|
reconnectDetails,
|
|
10458
|
+
preferredPublishOptions,
|
|
10459
|
+
preferredSubscribeOptions,
|
|
10590
10460
|
});
|
|
10461
|
+
this.currentPublishOptions = publishOptions;
|
|
10591
10462
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
10592
10463
|
if (callState) {
|
|
10593
10464
|
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
@@ -10612,17 +10483,13 @@ class Call {
|
|
|
10612
10483
|
connectionConfig,
|
|
10613
10484
|
clientDetails,
|
|
10614
10485
|
statsOptions,
|
|
10486
|
+
publishOptions: this.currentPublishOptions || [],
|
|
10615
10487
|
closePreviousInstances: !performingMigration,
|
|
10616
10488
|
});
|
|
10617
10489
|
}
|
|
10618
10490
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
10619
10491
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
10620
|
-
this.sfuStatsReporter?.
|
|
10621
|
-
data: {
|
|
10622
|
-
oneofKind: 'connectionTimeSeconds',
|
|
10623
|
-
connectionTimeSeconds: (Date.now() - connectStartTime) / 1000,
|
|
10624
|
-
},
|
|
10625
|
-
});
|
|
10492
|
+
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
10626
10493
|
}
|
|
10627
10494
|
if (performingRejoin) {
|
|
10628
10495
|
const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
@@ -10633,8 +10500,8 @@ class Call {
|
|
|
10633
10500
|
}
|
|
10634
10501
|
// device settings should be applied only once, we don't have to
|
|
10635
10502
|
// re-apply them on later reconnections or server-side data fetches
|
|
10636
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
10637
|
-
await this.applyDeviceConfig(true);
|
|
10503
|
+
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
10504
|
+
await this.applyDeviceConfig(this.state.settings, true);
|
|
10638
10505
|
this.deviceSettingsAppliedOnce = true;
|
|
10639
10506
|
}
|
|
10640
10507
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -10643,6 +10510,8 @@ class Call {
|
|
|
10643
10510
|
// we will spam the other participants with push notifications and `call.ring` events.
|
|
10644
10511
|
delete this.joinCallData?.ring;
|
|
10645
10512
|
delete this.joinCallData?.notify;
|
|
10513
|
+
// reset the reconnect strategy to unspecified after a successful reconnection
|
|
10514
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10646
10515
|
this.logger('info', `Joined call ${this.cid}`);
|
|
10647
10516
|
};
|
|
10648
10517
|
/**
|
|
@@ -10652,7 +10521,7 @@ class Call {
|
|
|
10652
10521
|
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
10653
10522
|
const strategy = this.reconnectStrategy;
|
|
10654
10523
|
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
10655
|
-
const announcedTracks = this.publisher?.
|
|
10524
|
+
const announcedTracks = this.publisher?.getAnnouncedTracksForReconnect() || [];
|
|
10656
10525
|
return {
|
|
10657
10526
|
strategy,
|
|
10658
10527
|
announcedTracks,
|
|
@@ -10662,6 +10531,54 @@ class Call {
|
|
|
10662
10531
|
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
10663
10532
|
};
|
|
10664
10533
|
};
|
|
10534
|
+
/**
|
|
10535
|
+
* Prepares the preferred codec for the call.
|
|
10536
|
+
* This is an experimental client feature and subject to change.
|
|
10537
|
+
* @internal
|
|
10538
|
+
*/
|
|
10539
|
+
this.getPreferredPublishOptions = () => {
|
|
10540
|
+
const { preferredCodec, fmtpLine, preferredBitrate, maxSimulcastLayers } = this.clientPublishOptions || {};
|
|
10541
|
+
if (!preferredCodec && !preferredBitrate && !maxSimulcastLayers)
|
|
10542
|
+
return [];
|
|
10543
|
+
const codec = preferredCodec
|
|
10544
|
+
? Codec.create({ name: preferredCodec.split('/').pop(), fmtp: fmtpLine })
|
|
10545
|
+
: undefined;
|
|
10546
|
+
const preferredPublishOptions = [
|
|
10547
|
+
PublishOption.create({
|
|
10548
|
+
trackType: TrackType.VIDEO,
|
|
10549
|
+
codec,
|
|
10550
|
+
bitrate: preferredBitrate,
|
|
10551
|
+
maxSpatialLayers: maxSimulcastLayers,
|
|
10552
|
+
}),
|
|
10553
|
+
];
|
|
10554
|
+
const screenShareSettings = this.screenShare.getSettings();
|
|
10555
|
+
if (screenShareSettings) {
|
|
10556
|
+
preferredPublishOptions.push(PublishOption.create({
|
|
10557
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
10558
|
+
fps: screenShareSettings.maxFramerate,
|
|
10559
|
+
bitrate: screenShareSettings.maxBitrate,
|
|
10560
|
+
}));
|
|
10561
|
+
}
|
|
10562
|
+
return preferredPublishOptions;
|
|
10563
|
+
};
|
|
10564
|
+
/**
|
|
10565
|
+
* Prepares the preferred options for subscribing to tracks.
|
|
10566
|
+
* This is an experimental client feature and subject to change.
|
|
10567
|
+
* @internal
|
|
10568
|
+
*/
|
|
10569
|
+
this.getPreferredSubscribeOptions = () => {
|
|
10570
|
+
const { subscriberCodec, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
10571
|
+
if (!subscriberCodec || !subscriberFmtpLine)
|
|
10572
|
+
return [];
|
|
10573
|
+
return [
|
|
10574
|
+
SubscribeOption.create({
|
|
10575
|
+
trackType: TrackType.VIDEO,
|
|
10576
|
+
codecs: [
|
|
10577
|
+
{ name: subscriberCodec.split('/').pop(), fmtp: subscriberFmtpLine },
|
|
10578
|
+
],
|
|
10579
|
+
}),
|
|
10580
|
+
];
|
|
10581
|
+
};
|
|
10665
10582
|
/**
|
|
10666
10583
|
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
10667
10584
|
* Uses the provided SFU client to restore the ICE connection.
|
|
@@ -10692,9 +10609,9 @@ class Call {
|
|
|
10692
10609
|
* @internal
|
|
10693
10610
|
*/
|
|
10694
10611
|
this.initPublisherAndSubscriber = (opts) => {
|
|
10695
|
-
const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
|
|
10612
|
+
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, } = opts;
|
|
10696
10613
|
if (closePreviousInstances && this.subscriber) {
|
|
10697
|
-
this.subscriber.
|
|
10614
|
+
this.subscriber.dispose();
|
|
10698
10615
|
}
|
|
10699
10616
|
this.subscriber = new Subscriber({
|
|
10700
10617
|
sfuClient,
|
|
@@ -10713,18 +10630,14 @@ class Call {
|
|
|
10713
10630
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
10714
10631
|
if (!isAnonymous) {
|
|
10715
10632
|
if (closePreviousInstances && this.publisher) {
|
|
10716
|
-
this.publisher.
|
|
10633
|
+
this.publisher.dispose();
|
|
10717
10634
|
}
|
|
10718
|
-
const audioSettings = this.state.settings?.audio;
|
|
10719
|
-
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
10720
|
-
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
10721
10635
|
this.publisher = new Publisher({
|
|
10722
10636
|
sfuClient,
|
|
10723
10637
|
dispatcher: this.dispatcher,
|
|
10724
10638
|
state: this.state,
|
|
10725
10639
|
connectionConfig,
|
|
10726
|
-
|
|
10727
|
-
isRedEnabled,
|
|
10640
|
+
publishOptions,
|
|
10728
10641
|
logTag: String(this.sfuClientTag),
|
|
10729
10642
|
onUnrecoverableError: () => {
|
|
10730
10643
|
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
@@ -10871,47 +10784,31 @@ class Call {
|
|
|
10871
10784
|
* @internal
|
|
10872
10785
|
*/
|
|
10873
10786
|
this.reconnectFast = async () => {
|
|
10874
|
-
|
|
10787
|
+
const reconnectStartTime = Date.now();
|
|
10875
10788
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
10876
10789
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10877
10790
|
await this.join(this.joinCallData);
|
|
10878
|
-
this.sfuStatsReporter?.
|
|
10879
|
-
data: {
|
|
10880
|
-
oneofKind: 'reconnection',
|
|
10881
|
-
reconnection: {
|
|
10882
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10883
|
-
strategy: WebsocketReconnectStrategy.FAST,
|
|
10884
|
-
},
|
|
10885
|
-
},
|
|
10886
|
-
});
|
|
10791
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
|
|
10887
10792
|
};
|
|
10888
10793
|
/**
|
|
10889
10794
|
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
10890
10795
|
* @internal
|
|
10891
10796
|
*/
|
|
10892
10797
|
this.reconnectRejoin = async () => {
|
|
10893
|
-
|
|
10798
|
+
const reconnectStartTime = Date.now();
|
|
10894
10799
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
10895
10800
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10896
10801
|
await this.join(this.joinCallData);
|
|
10897
10802
|
await this.restorePublishedTracks();
|
|
10898
10803
|
this.restoreSubscribedTracks();
|
|
10899
|
-
this.sfuStatsReporter?.
|
|
10900
|
-
data: {
|
|
10901
|
-
oneofKind: 'reconnection',
|
|
10902
|
-
reconnection: {
|
|
10903
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10904
|
-
strategy: WebsocketReconnectStrategy.REJOIN,
|
|
10905
|
-
},
|
|
10906
|
-
},
|
|
10907
|
-
});
|
|
10804
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
10908
10805
|
};
|
|
10909
10806
|
/**
|
|
10910
10807
|
* Initiates the reconnection flow with the "migrate" strategy.
|
|
10911
10808
|
* @internal
|
|
10912
10809
|
*/
|
|
10913
10810
|
this.reconnectMigrate = async () => {
|
|
10914
|
-
|
|
10811
|
+
const reconnectStartTime = Date.now();
|
|
10915
10812
|
const currentSfuClient = this.sfuClient;
|
|
10916
10813
|
if (!currentSfuClient) {
|
|
10917
10814
|
throw new Error('Cannot migrate without an active SFU client');
|
|
@@ -10945,20 +10842,12 @@ class Call {
|
|
|
10945
10842
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
10946
10843
|
}
|
|
10947
10844
|
finally {
|
|
10948
|
-
currentSubscriber?.
|
|
10949
|
-
currentPublisher?.
|
|
10845
|
+
currentSubscriber?.dispose();
|
|
10846
|
+
currentPublisher?.dispose();
|
|
10950
10847
|
// and close the previous SFU client, without specifying close code
|
|
10951
10848
|
currentSfuClient.close();
|
|
10952
10849
|
}
|
|
10953
|
-
this.sfuStatsReporter?.
|
|
10954
|
-
data: {
|
|
10955
|
-
oneofKind: 'reconnection',
|
|
10956
|
-
reconnection: {
|
|
10957
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10958
|
-
strategy: WebsocketReconnectStrategy.MIGRATE,
|
|
10959
|
-
},
|
|
10960
|
-
},
|
|
10961
|
-
});
|
|
10850
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.MIGRATE, (Date.now() - reconnectStartTime) / 1000);
|
|
10962
10851
|
};
|
|
10963
10852
|
/**
|
|
10964
10853
|
* Registers the various event handlers for reconnection.
|
|
@@ -11035,23 +10924,16 @@ class Call {
|
|
|
11035
10924
|
// the tracks need to be restored in their original order of publishing
|
|
11036
10925
|
// otherwise, we might get `m-lines order mismatch` errors
|
|
11037
10926
|
for (const trackType of this.trackPublishOrder) {
|
|
10927
|
+
let mediaStream;
|
|
11038
10928
|
switch (trackType) {
|
|
11039
10929
|
case TrackType.AUDIO:
|
|
11040
|
-
|
|
11041
|
-
if (audioStream) {
|
|
11042
|
-
await this.publishAudioStream(audioStream);
|
|
11043
|
-
}
|
|
10930
|
+
mediaStream = this.microphone.state.mediaStream;
|
|
11044
10931
|
break;
|
|
11045
10932
|
case TrackType.VIDEO:
|
|
11046
|
-
|
|
11047
|
-
if (videoStream)
|
|
11048
|
-
await this.publishVideoStream(videoStream);
|
|
10933
|
+
mediaStream = this.camera.state.mediaStream;
|
|
11049
10934
|
break;
|
|
11050
10935
|
case TrackType.SCREEN_SHARE:
|
|
11051
|
-
|
|
11052
|
-
if (screenShareStream) {
|
|
11053
|
-
await this.publishScreenShareStream(screenShareStream);
|
|
11054
|
-
}
|
|
10936
|
+
mediaStream = this.screenShare.state.mediaStream;
|
|
11055
10937
|
break;
|
|
11056
10938
|
// screen share audio can't exist without a screen share, so we handle it there
|
|
11057
10939
|
case TrackType.SCREEN_SHARE_AUDIO:
|
|
@@ -11061,6 +10943,8 @@ class Call {
|
|
|
11061
10943
|
ensureExhausted(trackType, 'Unknown track type');
|
|
11062
10944
|
break;
|
|
11063
10945
|
}
|
|
10946
|
+
if (mediaStream)
|
|
10947
|
+
await this.publish(mediaStream, trackType);
|
|
11064
10948
|
}
|
|
11065
10949
|
};
|
|
11066
10950
|
/**
|
|
@@ -11075,105 +10959,111 @@ class Call {
|
|
|
11075
10959
|
};
|
|
11076
10960
|
/**
|
|
11077
10961
|
* Starts publishing the given video stream to the call.
|
|
11078
|
-
*
|
|
11079
|
-
*
|
|
11080
|
-
* Consecutive calls to this method will replace the previously published stream.
|
|
11081
|
-
* The previous video stream will be stopped.
|
|
11082
|
-
*
|
|
11083
|
-
* @param videoStream the video stream to publish.
|
|
10962
|
+
* @deprecated use `call.publish()`.
|
|
11084
10963
|
*/
|
|
11085
10964
|
this.publishVideoStream = async (videoStream) => {
|
|
11086
|
-
|
|
11087
|
-
throw new Error(`Call not joined yet.`);
|
|
11088
|
-
// joining is in progress, and we should wait until the client is ready
|
|
11089
|
-
await this.sfuClient.joinTask;
|
|
11090
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_VIDEO)) {
|
|
11091
|
-
throw new Error('No permission to publish video');
|
|
11092
|
-
}
|
|
11093
|
-
if (!this.publisher)
|
|
11094
|
-
throw new Error('Publisher is not initialized');
|
|
11095
|
-
const [videoTrack] = videoStream.getVideoTracks();
|
|
11096
|
-
if (!videoTrack)
|
|
11097
|
-
throw new Error('There is no video track in the stream');
|
|
11098
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
11099
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
11100
|
-
}
|
|
11101
|
-
await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, this.publishOptions);
|
|
10965
|
+
await this.publish(videoStream, TrackType.VIDEO);
|
|
11102
10966
|
};
|
|
11103
10967
|
/**
|
|
11104
10968
|
* Starts publishing the given audio stream to the call.
|
|
11105
|
-
*
|
|
11106
|
-
*
|
|
11107
|
-
* Consecutive calls to this method will replace the audio stream that is currently being published.
|
|
11108
|
-
* The previous audio stream will be stopped.
|
|
11109
|
-
*
|
|
11110
|
-
* @param audioStream the audio stream to publish.
|
|
10969
|
+
* @deprecated use `call.publish()`
|
|
11111
10970
|
*/
|
|
11112
10971
|
this.publishAudioStream = async (audioStream) => {
|
|
11113
|
-
|
|
11114
|
-
throw new Error(`Call not joined yet.`);
|
|
11115
|
-
// joining is in progress, and we should wait until the client is ready
|
|
11116
|
-
await this.sfuClient.joinTask;
|
|
11117
|
-
if (!this.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO)) {
|
|
11118
|
-
throw new Error('No permission to publish audio');
|
|
11119
|
-
}
|
|
11120
|
-
if (!this.publisher)
|
|
11121
|
-
throw new Error('Publisher is not initialized');
|
|
11122
|
-
const [audioTrack] = audioStream.getAudioTracks();
|
|
11123
|
-
if (!audioTrack)
|
|
11124
|
-
throw new Error('There is no audio track in the stream');
|
|
11125
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
11126
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
11127
|
-
}
|
|
11128
|
-
await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
|
|
10972
|
+
await this.publish(audioStream, TrackType.AUDIO);
|
|
11129
10973
|
};
|
|
11130
10974
|
/**
|
|
11131
10975
|
* Starts publishing the given screen-share stream to the call.
|
|
11132
|
-
*
|
|
11133
|
-
* Consecutive calls to this method will replace the previous screen-share stream.
|
|
11134
|
-
* The previous screen-share stream will be stopped.
|
|
11135
|
-
*
|
|
11136
|
-
* @param screenShareStream the screen-share stream to publish.
|
|
10976
|
+
* @deprecated use `call.publish()`
|
|
11137
10977
|
*/
|
|
11138
10978
|
this.publishScreenShareStream = async (screenShareStream) => {
|
|
10979
|
+
await this.publish(screenShareStream, TrackType.SCREEN_SHARE);
|
|
10980
|
+
};
|
|
10981
|
+
/**
|
|
10982
|
+
* Publishes the given media stream.
|
|
10983
|
+
*
|
|
10984
|
+
* @param mediaStream the media stream to publish.
|
|
10985
|
+
* @param trackType the type of the track to announce.
|
|
10986
|
+
*/
|
|
10987
|
+
this.publish = async (mediaStream, trackType) => {
|
|
11139
10988
|
if (!this.sfuClient)
|
|
11140
10989
|
throw new Error(`Call not joined yet.`);
|
|
11141
10990
|
// joining is in progress, and we should wait until the client is ready
|
|
11142
10991
|
await this.sfuClient.joinTask;
|
|
11143
|
-
if (!this.permissionsContext.
|
|
11144
|
-
throw new Error(
|
|
10992
|
+
if (!this.permissionsContext.canPublish(trackType)) {
|
|
10993
|
+
throw new Error(`No permission to publish ${TrackType[trackType]}`);
|
|
11145
10994
|
}
|
|
11146
10995
|
if (!this.publisher)
|
|
11147
10996
|
throw new Error('Publisher is not initialized');
|
|
11148
|
-
const [
|
|
11149
|
-
|
|
11150
|
-
|
|
10997
|
+
const [track] = isAudioTrackType(trackType)
|
|
10998
|
+
? mediaStream.getAudioTracks()
|
|
10999
|
+
: mediaStream.getVideoTracks();
|
|
11000
|
+
if (!track) {
|
|
11001
|
+
throw new Error(`There is no ${TrackType[trackType]} track in the stream`);
|
|
11151
11002
|
}
|
|
11152
|
-
if (
|
|
11153
|
-
|
|
11154
|
-
}
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
this.
|
|
11003
|
+
if (track.readyState === 'ended') {
|
|
11004
|
+
throw new Error(`Can't publish ended tracks.`);
|
|
11005
|
+
}
|
|
11006
|
+
pushToIfMissing(this.trackPublishOrder, trackType);
|
|
11007
|
+
await this.publisher.publish(track, trackType);
|
|
11008
|
+
const trackTypes = [trackType];
|
|
11009
|
+
if (trackType === TrackType.SCREEN_SHARE) {
|
|
11010
|
+
const [audioTrack] = mediaStream.getAudioTracks();
|
|
11011
|
+
if (audioTrack) {
|
|
11012
|
+
pushToIfMissing(this.trackPublishOrder, TrackType.SCREEN_SHARE_AUDIO);
|
|
11013
|
+
await this.publisher.publish(audioTrack, TrackType.SCREEN_SHARE_AUDIO);
|
|
11014
|
+
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
11163
11015
|
}
|
|
11164
|
-
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
11165
11016
|
}
|
|
11017
|
+
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
11166
11018
|
};
|
|
11167
11019
|
/**
|
|
11168
11020
|
* Stops publishing the given track type to the call, if it is currently being published.
|
|
11169
|
-
* Underlying track will be stopped and removed from the publisher.
|
|
11170
11021
|
*
|
|
11171
|
-
* @param
|
|
11172
|
-
|
|
11022
|
+
* @param trackTypes the track types to stop publishing.
|
|
11023
|
+
*/
|
|
11024
|
+
this.stopPublish = async (...trackTypes) => {
|
|
11025
|
+
if (!this.sfuClient || !this.publisher)
|
|
11026
|
+
return;
|
|
11027
|
+
this.publisher.stopTracks(...trackTypes);
|
|
11028
|
+
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
11029
|
+
};
|
|
11030
|
+
/**
|
|
11031
|
+
* Updates the call state with the new stream.
|
|
11032
|
+
*
|
|
11033
|
+
* @param mediaStream the new stream to update the call state with.
|
|
11034
|
+
* If undefined, the stream will be removed from the call state.
|
|
11035
|
+
* @param trackTypes the track types to update the call state with.
|
|
11036
|
+
*/
|
|
11037
|
+
this.updateLocalStreamState = async (mediaStream, ...trackTypes) => {
|
|
11038
|
+
if (!this.sfuClient || !this.sfuClient.sessionId)
|
|
11039
|
+
return;
|
|
11040
|
+
await this.notifyTrackMuteState(!mediaStream, ...trackTypes);
|
|
11041
|
+
const { sessionId } = this.sfuClient;
|
|
11042
|
+
for (const trackType of trackTypes) {
|
|
11043
|
+
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
11044
|
+
if (!streamStateProp)
|
|
11045
|
+
continue;
|
|
11046
|
+
this.state.updateParticipant(sessionId, (p) => ({
|
|
11047
|
+
publishedTracks: mediaStream
|
|
11048
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
11049
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
11050
|
+
[streamStateProp]: mediaStream,
|
|
11051
|
+
}));
|
|
11052
|
+
}
|
|
11053
|
+
};
|
|
11054
|
+
/**
|
|
11055
|
+
* Updates the preferred publishing options
|
|
11056
|
+
*
|
|
11057
|
+
* @internal
|
|
11058
|
+
* @param options the options to use.
|
|
11173
11059
|
*/
|
|
11174
|
-
this.
|
|
11175
|
-
this.logger('
|
|
11176
|
-
|
|
11060
|
+
this.updatePublishOptions = (options) => {
|
|
11061
|
+
this.logger('warn', '[call.updatePublishOptions]: You are manually overriding the publish options for this call. ' +
|
|
11062
|
+
'This is not recommended, and it can cause call stability/compatibility issues. Use with caution.');
|
|
11063
|
+
if (this.state.callingState === exports.CallingState.JOINED) {
|
|
11064
|
+
this.logger('warn', 'Updating publish options after joining the call does not have an effect');
|
|
11065
|
+
}
|
|
11066
|
+
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
11177
11067
|
};
|
|
11178
11068
|
/**
|
|
11179
11069
|
* Notifies the SFU that a noise cancellation process has started.
|
|
@@ -11195,6 +11085,15 @@ class Call {
|
|
|
11195
11085
|
this.logger('warn', 'Failed to notify stop of noise cancellation', err);
|
|
11196
11086
|
});
|
|
11197
11087
|
};
|
|
11088
|
+
/**
|
|
11089
|
+
* Notifies the SFU about the mute state of the given track types.
|
|
11090
|
+
* @internal
|
|
11091
|
+
*/
|
|
11092
|
+
this.notifyTrackMuteState = async (muted, ...trackTypes) => {
|
|
11093
|
+
if (!this.sfuClient)
|
|
11094
|
+
return;
|
|
11095
|
+
await this.sfuClient.updateMuteStates(trackTypes.map((trackType) => ({ trackType, muted })));
|
|
11096
|
+
};
|
|
11198
11097
|
/**
|
|
11199
11098
|
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
|
|
11200
11099
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
@@ -11658,70 +11557,14 @@ class Call {
|
|
|
11658
11557
|
*
|
|
11659
11558
|
* @internal
|
|
11660
11559
|
*/
|
|
11661
|
-
this.applyDeviceConfig = async (
|
|
11662
|
-
await this.
|
|
11560
|
+
this.applyDeviceConfig = async (settings, publish) => {
|
|
11561
|
+
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
11663
11562
|
this.logger('warn', 'Camera init failed', err);
|
|
11664
11563
|
});
|
|
11665
|
-
await this.
|
|
11564
|
+
await this.microphone.apply(settings.audio, publish).catch((err) => {
|
|
11666
11565
|
this.logger('warn', 'Mic init failed', err);
|
|
11667
11566
|
});
|
|
11668
11567
|
};
|
|
11669
|
-
this.initCamera = async (options) => {
|
|
11670
|
-
// Wait for any in progress camera operation
|
|
11671
|
-
await this.camera.statusChangeSettled();
|
|
11672
|
-
if (this.state.localParticipant?.videoStream ||
|
|
11673
|
-
!this.permissionsContext.hasPermission('send-video')) {
|
|
11674
|
-
return;
|
|
11675
|
-
}
|
|
11676
|
-
// Set camera direction if it's not yet set
|
|
11677
|
-
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
11678
|
-
let defaultDirection = 'front';
|
|
11679
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
11680
|
-
if (backendSetting) {
|
|
11681
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
11682
|
-
}
|
|
11683
|
-
this.camera.state.setDirection(defaultDirection);
|
|
11684
|
-
}
|
|
11685
|
-
// Set target resolution
|
|
11686
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
11687
|
-
if (targetResolution) {
|
|
11688
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
11689
|
-
}
|
|
11690
|
-
if (options.setStatus) {
|
|
11691
|
-
// Publish already that was set before we joined
|
|
11692
|
-
if (this.camera.enabled &&
|
|
11693
|
-
this.camera.state.mediaStream &&
|
|
11694
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
11695
|
-
await this.publishVideoStream(this.camera.state.mediaStream);
|
|
11696
|
-
}
|
|
11697
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
11698
|
-
if (this.camera.state.status === undefined &&
|
|
11699
|
-
this.state.settings?.video.camera_default_on) {
|
|
11700
|
-
await this.camera.enable();
|
|
11701
|
-
}
|
|
11702
|
-
}
|
|
11703
|
-
};
|
|
11704
|
-
this.initMic = async (options) => {
|
|
11705
|
-
// Wait for any in progress mic operation
|
|
11706
|
-
await this.microphone.statusChangeSettled();
|
|
11707
|
-
if (this.state.localParticipant?.audioStream ||
|
|
11708
|
-
!this.permissionsContext.hasPermission('send-audio')) {
|
|
11709
|
-
return;
|
|
11710
|
-
}
|
|
11711
|
-
if (options.setStatus) {
|
|
11712
|
-
// Publish media stream that was set before we joined
|
|
11713
|
-
if (this.microphone.enabled &&
|
|
11714
|
-
this.microphone.state.mediaStream &&
|
|
11715
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
11716
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
11717
|
-
}
|
|
11718
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
11719
|
-
if (this.microphone.state.status === undefined &&
|
|
11720
|
-
this.state.settings?.audio.mic_default_on) {
|
|
11721
|
-
await this.microphone.enable();
|
|
11722
|
-
}
|
|
11723
|
-
}
|
|
11724
|
-
};
|
|
11725
11568
|
/**
|
|
11726
11569
|
* Will begin tracking the given element for visibility changes within the
|
|
11727
11570
|
* configured viewport element (`call.setViewport`).
|
|
@@ -11870,109 +11713,6 @@ class Call {
|
|
|
11870
11713
|
this.screenShare = new ScreenShareManager(this);
|
|
11871
11714
|
this.dynascaleManager = new DynascaleManager(this.state, this.speaker);
|
|
11872
11715
|
}
|
|
11873
|
-
async setup() {
|
|
11874
|
-
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
11875
|
-
if (this.initialized)
|
|
11876
|
-
return;
|
|
11877
|
-
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
11878
|
-
// update state with the latest event data
|
|
11879
|
-
this.state.updateFromEvent(event);
|
|
11880
|
-
}));
|
|
11881
|
-
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
11882
|
-
this.registerEffects();
|
|
11883
|
-
this.registerReconnectHandlers();
|
|
11884
|
-
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
11885
|
-
this.state.setCallingState(exports.CallingState.IDLE);
|
|
11886
|
-
}
|
|
11887
|
-
this.initialized = true;
|
|
11888
|
-
});
|
|
11889
|
-
}
|
|
11890
|
-
registerEffects() {
|
|
11891
|
-
this.leaveCallHooks.add(
|
|
11892
|
-
// handles updating the permissions context when the settings change.
|
|
11893
|
-
createSubscription(this.state.settings$, (settings) => {
|
|
11894
|
-
if (!settings)
|
|
11895
|
-
return;
|
|
11896
|
-
this.permissionsContext.setCallSettings(settings);
|
|
11897
|
-
}));
|
|
11898
|
-
this.leaveCallHooks.add(
|
|
11899
|
-
// handle the case when the user permissions are modified.
|
|
11900
|
-
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
11901
|
-
this.leaveCallHooks.add(
|
|
11902
|
-
// handles the case when the user is blocked by the call owner.
|
|
11903
|
-
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
11904
|
-
if (!blockedUserIds || blockedUserIds.length === 0)
|
|
11905
|
-
return;
|
|
11906
|
-
const currentUserId = this.currentUserId;
|
|
11907
|
-
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
11908
|
-
this.logger('info', 'Leaving call because of being blocked');
|
|
11909
|
-
await this.leave({ reason: 'user blocked' }).catch((err) => {
|
|
11910
|
-
this.logger('error', 'Error leaving call after being blocked', err);
|
|
11911
|
-
});
|
|
11912
|
-
}
|
|
11913
|
-
}));
|
|
11914
|
-
this.leaveCallHooks.add(
|
|
11915
|
-
// cancel auto-drop when call is
|
|
11916
|
-
createSubscription(this.state.session$, (session) => {
|
|
11917
|
-
if (!this.ringing)
|
|
11918
|
-
return;
|
|
11919
|
-
const receiverId = this.clientStore.connectedUser?.id;
|
|
11920
|
-
if (!receiverId)
|
|
11921
|
-
return;
|
|
11922
|
-
const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
|
|
11923
|
-
const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
|
|
11924
|
-
if (isAcceptedByMe || isRejectedByMe) {
|
|
11925
|
-
this.cancelAutoDrop();
|
|
11926
|
-
}
|
|
11927
|
-
}));
|
|
11928
|
-
this.leaveCallHooks.add(
|
|
11929
|
-
// "ringing" mode effects and event handlers
|
|
11930
|
-
createSubscription(this.ringingSubject, (isRinging) => {
|
|
11931
|
-
if (!isRinging)
|
|
11932
|
-
return;
|
|
11933
|
-
const callSession = this.state.session;
|
|
11934
|
-
const receiver_id = this.clientStore.connectedUser?.id;
|
|
11935
|
-
const ended_at = callSession?.ended_at;
|
|
11936
|
-
const created_by_id = this.state.createdBy?.id;
|
|
11937
|
-
const rejected_by = callSession?.rejected_by;
|
|
11938
|
-
const accepted_by = callSession?.accepted_by;
|
|
11939
|
-
let leaveCallIdle = false;
|
|
11940
|
-
if (ended_at) {
|
|
11941
|
-
// call was ended before it was accepted or rejected so we should leave it to idle
|
|
11942
|
-
leaveCallIdle = true;
|
|
11943
|
-
}
|
|
11944
|
-
else if (created_by_id && rejected_by) {
|
|
11945
|
-
if (rejected_by[created_by_id]) {
|
|
11946
|
-
// call was cancelled by the caller
|
|
11947
|
-
leaveCallIdle = true;
|
|
11948
|
-
}
|
|
11949
|
-
}
|
|
11950
|
-
else if (receiver_id && rejected_by) {
|
|
11951
|
-
if (rejected_by[receiver_id]) {
|
|
11952
|
-
// call was rejected by the receiver in some other device
|
|
11953
|
-
leaveCallIdle = true;
|
|
11954
|
-
}
|
|
11955
|
-
}
|
|
11956
|
-
else if (receiver_id && accepted_by) {
|
|
11957
|
-
if (accepted_by[receiver_id]) {
|
|
11958
|
-
// call was accepted by the receiver in some other device
|
|
11959
|
-
leaveCallIdle = true;
|
|
11960
|
-
}
|
|
11961
|
-
}
|
|
11962
|
-
if (leaveCallIdle) {
|
|
11963
|
-
if (this.state.callingState !== exports.CallingState.IDLE) {
|
|
11964
|
-
this.state.setCallingState(exports.CallingState.IDLE);
|
|
11965
|
-
}
|
|
11966
|
-
}
|
|
11967
|
-
else {
|
|
11968
|
-
if (this.state.callingState === exports.CallingState.IDLE) {
|
|
11969
|
-
this.state.setCallingState(exports.CallingState.RINGING);
|
|
11970
|
-
}
|
|
11971
|
-
this.scheduleAutoDrop();
|
|
11972
|
-
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
11973
|
-
}
|
|
11974
|
-
}));
|
|
11975
|
-
}
|
|
11976
11716
|
/**
|
|
11977
11717
|
* A flag indicating whether the call is "ringing" type of call.
|
|
11978
11718
|
*/
|
|
@@ -11991,15 +11731,6 @@ class Call {
|
|
|
11991
11731
|
get isCreatedByMe() {
|
|
11992
11732
|
return this.state.createdBy?.id === this.currentUserId;
|
|
11993
11733
|
}
|
|
11994
|
-
/**
|
|
11995
|
-
* Updates the preferred publishing options
|
|
11996
|
-
*
|
|
11997
|
-
* @internal
|
|
11998
|
-
* @param options the options to use.
|
|
11999
|
-
*/
|
|
12000
|
-
updatePublishOptions(options) {
|
|
12001
|
-
this.publishOptions = { ...this.publishOptions, ...options };
|
|
12002
|
-
}
|
|
12003
11734
|
}
|
|
12004
11735
|
|
|
12005
11736
|
/**
|
|
@@ -13107,7 +12838,7 @@ class StreamClient {
|
|
|
13107
12838
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
13108
12839
|
};
|
|
13109
12840
|
this.getUserAgent = () => {
|
|
13110
|
-
const version = "1.
|
|
12841
|
+
const version = "1.15.1";
|
|
13111
12842
|
return (this.userAgent ||
|
|
13112
12843
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
13113
12844
|
};
|
|
@@ -13407,7 +13138,7 @@ class StreamVideoClient {
|
|
|
13407
13138
|
clientStore: this.writeableStateStore,
|
|
13408
13139
|
});
|
|
13409
13140
|
call.state.updateFromCallResponse(c.call);
|
|
13410
|
-
await call.applyDeviceConfig(false);
|
|
13141
|
+
await call.applyDeviceConfig(c.call.settings, false);
|
|
13411
13142
|
if (data.watch) {
|
|
13412
13143
|
this.writeableStateStore.registerCall(call);
|
|
13413
13144
|
}
|