@stream-io/video-client 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.browser.es.js +1532 -1784
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1512 -1783
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1532 -1784
- 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 +36 -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,284 +3539,34 @@ 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
|
};
|
|
3775
|
-
|
|
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,
|
|
3556
|
+
this.dispose = () => {
|
|
3557
|
+
this.subscriberCandidates.complete();
|
|
3558
|
+
this.publisherCandidates.complete();
|
|
3901
3559
|
};
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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;
|
|
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;
|
|
4042
3570
|
}
|
|
4043
3571
|
};
|
|
4044
3572
|
|
|
@@ -5606,198 +5134,446 @@ class CallState {
|
|
|
5606
5134
|
}
|
|
5607
5135
|
}
|
|
5608
5136
|
|
|
5609
|
-
const getRtpMap = (line) => {
|
|
5610
|
-
// Example: a=rtpmap:110 opus/48000/2
|
|
5611
|
-
const rtpRegex = /^a=rtpmap:(\d*) ([\w\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/;
|
|
5612
|
-
// The first captured group is the payload type number, the second captured group is the encoding name, the third captured group is the clock rate, and the fourth captured group is any additional parameters.
|
|
5613
|
-
const rtpMatch = rtpRegex.exec(line);
|
|
5614
|
-
if (rtpMatch) {
|
|
5615
|
-
return {
|
|
5616
|
-
original: rtpMatch[0],
|
|
5617
|
-
payload: rtpMatch[1],
|
|
5618
|
-
codec: rtpMatch[2],
|
|
5619
|
-
};
|
|
5620
|
-
}
|
|
5621
|
-
};
|
|
5622
|
-
const getFmtp = (line) => {
|
|
5623
|
-
// Example: a=fmtp:111 minptime=10; useinbandfec=1
|
|
5624
|
-
const fmtpRegex = /^a=fmtp:(\d*) (.*)/;
|
|
5625
|
-
const fmtpMatch = fmtpRegex.exec(line);
|
|
5626
|
-
// The first captured group is the payload type number, the second captured group is any additional parameters.
|
|
5627
|
-
if (fmtpMatch) {
|
|
5628
|
-
return {
|
|
5629
|
-
original: fmtpMatch[0],
|
|
5630
|
-
payload: fmtpMatch[1],
|
|
5631
|
-
config: fmtpMatch[2],
|
|
5632
|
-
};
|
|
5633
|
-
}
|
|
5634
|
-
};
|
|
5635
5137
|
/**
|
|
5636
|
-
*
|
|
5637
|
-
*
|
|
5638
|
-
* Example: m=video 9 UDP/TLS/RTP/SAVPF 100 101 96 97 35 36 102 125 127
|
|
5138
|
+
* A base class for the `Publisher` and `Subscriber` classes.
|
|
5139
|
+
* @internal
|
|
5639
5140
|
*/
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
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();
|
|
5648
5154
|
};
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
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;
|
|
5670
5205
|
}
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
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 });
|
|
5677
5221
|
}
|
|
5678
|
-
|
|
5679
|
-
|
|
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);
|
|
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);
|
|
5280
|
+
}
|
|
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;
|
|
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 });
|
|
5680
5366
|
}
|
|
5681
|
-
}
|
|
5682
|
-
});
|
|
5683
|
-
if (media) {
|
|
5684
|
-
return {
|
|
5685
|
-
media,
|
|
5686
|
-
rtpMap,
|
|
5687
|
-
fmtp,
|
|
5688
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;
|
|
5689
5425
|
}
|
|
5690
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
|
+
};
|
|
5434
|
+
/**
|
|
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.
|
|
5440
|
+
*/
|
|
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' }];
|
|
5450
|
+
};
|
|
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
|
+
};
|
|
5691
5461
|
/**
|
|
5692
|
-
*
|
|
5462
|
+
* Converts the given video layers to SFU video layers.
|
|
5693
5463
|
*/
|
|
5694
|
-
const
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
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);
|
|
5507
|
+
}
|
|
5508
|
+
else {
|
|
5509
|
+
// for non-SVC codecs, we need to downscale proportionally (simulcast)
|
|
5510
|
+
layer.scaleResolutionDownBy = downscaleFactor;
|
|
5511
|
+
}
|
|
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);
|
|
5700
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);
|
|
5701
5522
|
};
|
|
5702
5523
|
/**
|
|
5703
|
-
*
|
|
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.
|
|
5704
5534
|
*/
|
|
5705
|
-
const
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
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);
|
|
5544
|
+
}
|
|
5545
|
+
return bitrate;
|
|
5715
5546
|
};
|
|
5716
5547
|
/**
|
|
5717
|
-
*
|
|
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();
|
|
6044
5729
|
}
|
|
6045
|
-
|
|
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.`);
|
|
5739
|
+
}
|
|
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;
|
|
6388
|
-
};
|
|
6389
|
-
/**
|
|
6390
|
-
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
6391
|
-
*/
|
|
6392
|
-
this.close = () => {
|
|
6393
|
-
this.detachEventHandlers();
|
|
6394
|
-
this.pc.close();
|
|
5846
|
+
return tracks;
|
|
6395
5847
|
};
|
|
6396
5848
|
/**
|
|
6397
|
-
*
|
|
6398
|
-
*
|
|
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,18 +7328,152 @@ 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
|
+
}, {});
|
|
7337
|
+
}
|
|
7338
|
+
const qualityLimitationReason = [
|
|
7339
|
+
qualityLimitationReasons.has('cpu') && 'cpu',
|
|
7340
|
+
qualityLimitationReasons.has('bandwidth') && 'bandwidth',
|
|
7341
|
+
qualityLimitationReasons.has('other') && 'other',
|
|
7342
|
+
]
|
|
7343
|
+
.filter(Boolean)
|
|
7344
|
+
.join(', ');
|
|
7345
|
+
if (qualityLimitationReason) {
|
|
7346
|
+
report.qualityLimitationReasons = qualityLimitationReason;
|
|
7347
|
+
}
|
|
7348
|
+
return report;
|
|
7349
|
+
};
|
|
7350
|
+
|
|
7351
|
+
const version = "1.15.0";
|
|
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
|
+
};
|
|
7805
7443
|
}
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
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
|
+
};
|
|
7815
7456
|
}
|
|
7816
|
-
|
|
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.
|
|
@@ -9056,36 +8755,42 @@ class InputMediaDeviceManager {
|
|
|
9056
8755
|
}
|
|
9057
8756
|
});
|
|
9058
8757
|
}
|
|
8758
|
+
publishStream(stream) {
|
|
8759
|
+
return this.call.publish(stream, this.trackType);
|
|
8760
|
+
}
|
|
8761
|
+
stopPublishStream() {
|
|
8762
|
+
return this.call.stopPublish(this.trackType);
|
|
8763
|
+
}
|
|
9059
8764
|
getTracks() {
|
|
9060
8765
|
return this.state.mediaStream?.getTracks() ?? [];
|
|
9061
8766
|
}
|
|
9062
8767
|
async muteStream(stopTracks = true) {
|
|
9063
|
-
|
|
8768
|
+
const mediaStream = this.state.mediaStream;
|
|
8769
|
+
if (!mediaStream)
|
|
9064
8770
|
return;
|
|
9065
8771
|
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
9066
8772
|
if (this.call.state.callingState === exports.CallingState.JOINED) {
|
|
9067
|
-
await this.stopPublishStream(
|
|
8773
|
+
await this.stopPublishStream();
|
|
9068
8774
|
}
|
|
9069
8775
|
this.muteLocalStream(stopTracks);
|
|
9070
8776
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
9071
8777
|
if (allEnded) {
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
typeof this.state.mediaStream.release === 'function') {
|
|
8778
|
+
// @ts-expect-error release() is present in react-native-webrtc
|
|
8779
|
+
if (typeof mediaStream.release === 'function') {
|
|
9075
8780
|
// @ts-expect-error called to dispose the stream in RN
|
|
9076
|
-
|
|
8781
|
+
mediaStream.release();
|
|
9077
8782
|
}
|
|
9078
8783
|
this.state.setMediaStream(undefined, undefined);
|
|
9079
8784
|
this.filters.forEach((entry) => entry.stop?.());
|
|
9080
8785
|
}
|
|
9081
8786
|
}
|
|
9082
|
-
|
|
8787
|
+
disableTracks() {
|
|
9083
8788
|
this.getTracks().forEach((track) => {
|
|
9084
8789
|
if (track.enabled)
|
|
9085
8790
|
track.enabled = false;
|
|
9086
8791
|
});
|
|
9087
8792
|
}
|
|
9088
|
-
|
|
8793
|
+
enableTracks() {
|
|
9089
8794
|
this.getTracks().forEach((track) => {
|
|
9090
8795
|
if (!track.enabled)
|
|
9091
8796
|
track.enabled = true;
|
|
@@ -9105,7 +8810,7 @@ class InputMediaDeviceManager {
|
|
|
9105
8810
|
this.stopTracks();
|
|
9106
8811
|
}
|
|
9107
8812
|
else {
|
|
9108
|
-
this.
|
|
8813
|
+
this.disableTracks();
|
|
9109
8814
|
}
|
|
9110
8815
|
}
|
|
9111
8816
|
async unmuteStream() {
|
|
@@ -9115,7 +8820,7 @@ class InputMediaDeviceManager {
|
|
|
9115
8820
|
if (this.state.mediaStream &&
|
|
9116
8821
|
this.getTracks().every((t) => t.readyState === 'live')) {
|
|
9117
8822
|
stream = this.state.mediaStream;
|
|
9118
|
-
this.
|
|
8823
|
+
this.enableTracks();
|
|
9119
8824
|
}
|
|
9120
8825
|
else {
|
|
9121
8826
|
const defaultConstraints = this.state.defaultConstraints;
|
|
@@ -9209,9 +8914,22 @@ class InputMediaDeviceManager {
|
|
|
9209
8914
|
await this.disable();
|
|
9210
8915
|
}
|
|
9211
8916
|
};
|
|
9212
|
-
|
|
8917
|
+
const createTrackMuteHandler = (muted) => () => {
|
|
8918
|
+
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
8919
|
+
this.logger('warn', 'Error while notifying track mute state', err);
|
|
8920
|
+
});
|
|
8921
|
+
};
|
|
8922
|
+
stream.getTracks().forEach((track) => {
|
|
8923
|
+
const muteHandler = createTrackMuteHandler(true);
|
|
8924
|
+
const unmuteHandler = createTrackMuteHandler(false);
|
|
8925
|
+
track.addEventListener('mute', muteHandler);
|
|
8926
|
+
track.addEventListener('unmute', unmuteHandler);
|
|
9213
8927
|
track.addEventListener('ended', handleTrackEnded);
|
|
9214
|
-
this.subscriptions.push(() =>
|
|
8928
|
+
this.subscriptions.push(() => {
|
|
8929
|
+
track.removeEventListener('mute', muteHandler);
|
|
8930
|
+
track.removeEventListener('unmute', unmuteHandler);
|
|
8931
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
8932
|
+
});
|
|
9215
8933
|
});
|
|
9216
8934
|
}
|
|
9217
8935
|
}
|
|
@@ -9235,8 +8953,8 @@ class InputMediaDeviceManager {
|
|
|
9235
8953
|
await this.statusChangeSettled();
|
|
9236
8954
|
let isDeviceDisconnected = false;
|
|
9237
8955
|
let isDeviceReplaced = false;
|
|
9238
|
-
const currentDevice = this.
|
|
9239
|
-
const prevDevice = this.
|
|
8956
|
+
const currentDevice = this.findDevice(currentDevices, deviceId);
|
|
8957
|
+
const prevDevice = this.findDevice(prevDevices, deviceId);
|
|
9240
8958
|
if (!currentDevice && prevDevice) {
|
|
9241
8959
|
isDeviceDisconnected = true;
|
|
9242
8960
|
}
|
|
@@ -9266,8 +8984,9 @@ class InputMediaDeviceManager {
|
|
|
9266
8984
|
}
|
|
9267
8985
|
}));
|
|
9268
8986
|
}
|
|
9269
|
-
|
|
9270
|
-
|
|
8987
|
+
findDevice(devices, deviceId) {
|
|
8988
|
+
const kind = this.mediaDeviceKind;
|
|
8989
|
+
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
9271
8990
|
}
|
|
9272
8991
|
}
|
|
9273
8992
|
|
|
@@ -9531,14 +9250,35 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9531
9250
|
}
|
|
9532
9251
|
}
|
|
9533
9252
|
/**
|
|
9534
|
-
*
|
|
9253
|
+
* Applies the video settings to the camera.
|
|
9535
9254
|
*
|
|
9536
|
-
* @
|
|
9537
|
-
* @
|
|
9538
|
-
* @param codec the codec to use for encoding the video.
|
|
9255
|
+
* @param settings the video settings to apply.
|
|
9256
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9539
9257
|
*/
|
|
9540
|
-
|
|
9541
|
-
this.call.
|
|
9258
|
+
async apply(settings, publish) {
|
|
9259
|
+
const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
|
|
9260
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9261
|
+
if (hasPublishedVideo || !hasPermission)
|
|
9262
|
+
return;
|
|
9263
|
+
// Wait for any in progress camera operation
|
|
9264
|
+
await this.statusChangeSettled();
|
|
9265
|
+
const { target_resolution, camera_facing, camera_default_on } = settings;
|
|
9266
|
+
await this.selectTargetResolution(target_resolution);
|
|
9267
|
+
// Set camera direction if it's not yet set
|
|
9268
|
+
if (!this.state.direction && !this.state.selectedDevice) {
|
|
9269
|
+
this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
|
|
9270
|
+
}
|
|
9271
|
+
if (!publish)
|
|
9272
|
+
return;
|
|
9273
|
+
const { mediaStream } = this.state;
|
|
9274
|
+
if (this.enabled && mediaStream) {
|
|
9275
|
+
// The camera is already enabled (e.g. lobby screen). Publish the stream
|
|
9276
|
+
await this.publishStream(mediaStream);
|
|
9277
|
+
}
|
|
9278
|
+
else if (this.state.status === undefined && camera_default_on) {
|
|
9279
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
9280
|
+
await this.enable();
|
|
9281
|
+
}
|
|
9542
9282
|
}
|
|
9543
9283
|
getDevices() {
|
|
9544
9284
|
return getVideoDevices();
|
|
@@ -9556,12 +9296,6 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
9556
9296
|
}
|
|
9557
9297
|
return getVideoStream(constraints);
|
|
9558
9298
|
}
|
|
9559
|
-
publishStream(stream) {
|
|
9560
|
-
return this.call.publishVideoStream(stream);
|
|
9561
|
-
}
|
|
9562
|
-
stopPublishStream(stopTracks) {
|
|
9563
|
-
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
9564
|
-
}
|
|
9565
9299
|
}
|
|
9566
9300
|
|
|
9567
9301
|
class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
@@ -9889,18 +9623,37 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
9889
9623
|
this.speakingWhileMutedNotificationEnabled = false;
|
|
9890
9624
|
await this.stopSpeakingWhileMutedDetection();
|
|
9891
9625
|
}
|
|
9626
|
+
/**
|
|
9627
|
+
* Applies the audio settings to the microphone.
|
|
9628
|
+
* @param settings the audio settings to apply.
|
|
9629
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
9630
|
+
*/
|
|
9631
|
+
async apply(settings, publish) {
|
|
9632
|
+
if (!publish)
|
|
9633
|
+
return;
|
|
9634
|
+
const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
|
|
9635
|
+
const hasPermission = this.call.permissionsContext.hasPermission(OwnCapability.SEND_AUDIO);
|
|
9636
|
+
if (hasPublishedAudio || !hasPermission)
|
|
9637
|
+
return;
|
|
9638
|
+
// Wait for any in progress mic operation
|
|
9639
|
+
await this.statusChangeSettled();
|
|
9640
|
+
// Publish media stream that was set before we joined
|
|
9641
|
+
const { mediaStream } = this.state;
|
|
9642
|
+
if (this.enabled && mediaStream) {
|
|
9643
|
+
// The mic is already enabled (e.g. lobby screen). Publish the stream
|
|
9644
|
+
await this.publishStream(mediaStream);
|
|
9645
|
+
}
|
|
9646
|
+
else if (this.state.status === undefined && settings.mic_default_on) {
|
|
9647
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
9648
|
+
await this.enable();
|
|
9649
|
+
}
|
|
9650
|
+
}
|
|
9892
9651
|
getDevices() {
|
|
9893
9652
|
return getAudioDevices();
|
|
9894
9653
|
}
|
|
9895
9654
|
getStream(constraints) {
|
|
9896
9655
|
return getAudioStream(constraints);
|
|
9897
9656
|
}
|
|
9898
|
-
publishStream(stream) {
|
|
9899
|
-
return this.call.publishAudioStream(stream);
|
|
9900
|
-
}
|
|
9901
|
-
stopPublishStream(stopTracks) {
|
|
9902
|
-
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
9903
|
-
}
|
|
9904
9657
|
async startSpeakingWhileMutedDetection(deviceId) {
|
|
9905
9658
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
9906
9659
|
await this.stopSpeakingWhileMutedDetection();
|
|
@@ -10020,7 +9773,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10020
9773
|
async disableScreenShareAudio() {
|
|
10021
9774
|
this.state.setAudioEnabled(false);
|
|
10022
9775
|
if (this.call.publisher?.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
10023
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO
|
|
9776
|
+
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO);
|
|
10024
9777
|
}
|
|
10025
9778
|
}
|
|
10026
9779
|
/**
|
|
@@ -10046,12 +9799,8 @@ class ScreenShareManager extends InputMediaDeviceManager {
|
|
|
10046
9799
|
}
|
|
10047
9800
|
return getScreenShareStream(constraints);
|
|
10048
9801
|
}
|
|
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);
|
|
9802
|
+
async stopPublishStream() {
|
|
9803
|
+
return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
|
|
10055
9804
|
}
|
|
10056
9805
|
/**
|
|
10057
9806
|
* Overrides the default `select` method to throw an error.
|
|
@@ -10261,6 +10010,112 @@ class Call {
|
|
|
10261
10010
|
*/
|
|
10262
10011
|
this.leaveCallHooks = new Set();
|
|
10263
10012
|
this.streamClientEventHandlers = new Map();
|
|
10013
|
+
this.setup = async () => {
|
|
10014
|
+
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
10015
|
+
if (this.initialized)
|
|
10016
|
+
return;
|
|
10017
|
+
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
10018
|
+
// update state with the latest event data
|
|
10019
|
+
this.state.updateFromEvent(event);
|
|
10020
|
+
}));
|
|
10021
|
+
this.leaveCallHooks.add(this.on('changePublishOptions', (event) => {
|
|
10022
|
+
this.currentPublishOptions = event.publishOptions;
|
|
10023
|
+
}));
|
|
10024
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
10025
|
+
this.registerEffects();
|
|
10026
|
+
this.registerReconnectHandlers();
|
|
10027
|
+
if (this.state.callingState === exports.CallingState.LEFT) {
|
|
10028
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10029
|
+
}
|
|
10030
|
+
this.initialized = true;
|
|
10031
|
+
});
|
|
10032
|
+
};
|
|
10033
|
+
this.registerEffects = () => {
|
|
10034
|
+
this.leaveCallHooks.add(
|
|
10035
|
+
// handles updating the permissions context when the settings change.
|
|
10036
|
+
createSubscription(this.state.settings$, (settings) => {
|
|
10037
|
+
if (!settings)
|
|
10038
|
+
return;
|
|
10039
|
+
this.permissionsContext.setCallSettings(settings);
|
|
10040
|
+
}));
|
|
10041
|
+
this.leaveCallHooks.add(
|
|
10042
|
+
// handle the case when the user permissions are modified.
|
|
10043
|
+
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
10044
|
+
this.leaveCallHooks.add(
|
|
10045
|
+
// handles the case when the user is blocked by the call owner.
|
|
10046
|
+
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
10047
|
+
if (!blockedUserIds || blockedUserIds.length === 0)
|
|
10048
|
+
return;
|
|
10049
|
+
const currentUserId = this.currentUserId;
|
|
10050
|
+
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
10051
|
+
this.logger('info', 'Leaving call because of being blocked');
|
|
10052
|
+
await this.leave({ reason: 'user blocked' }).catch((err) => {
|
|
10053
|
+
this.logger('error', 'Error leaving call after being blocked', err);
|
|
10054
|
+
});
|
|
10055
|
+
}
|
|
10056
|
+
}));
|
|
10057
|
+
this.leaveCallHooks.add(
|
|
10058
|
+
// cancel auto-drop when call is
|
|
10059
|
+
createSubscription(this.state.session$, (session) => {
|
|
10060
|
+
if (!this.ringing)
|
|
10061
|
+
return;
|
|
10062
|
+
const receiverId = this.clientStore.connectedUser?.id;
|
|
10063
|
+
if (!receiverId)
|
|
10064
|
+
return;
|
|
10065
|
+
const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
|
|
10066
|
+
const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
|
|
10067
|
+
if (isAcceptedByMe || isRejectedByMe) {
|
|
10068
|
+
this.cancelAutoDrop();
|
|
10069
|
+
}
|
|
10070
|
+
}));
|
|
10071
|
+
this.leaveCallHooks.add(
|
|
10072
|
+
// "ringing" mode effects and event handlers
|
|
10073
|
+
createSubscription(this.ringingSubject, (isRinging) => {
|
|
10074
|
+
if (!isRinging)
|
|
10075
|
+
return;
|
|
10076
|
+
const callSession = this.state.session;
|
|
10077
|
+
const receiver_id = this.clientStore.connectedUser?.id;
|
|
10078
|
+
const ended_at = callSession?.ended_at;
|
|
10079
|
+
const created_by_id = this.state.createdBy?.id;
|
|
10080
|
+
const rejected_by = callSession?.rejected_by;
|
|
10081
|
+
const accepted_by = callSession?.accepted_by;
|
|
10082
|
+
let leaveCallIdle = false;
|
|
10083
|
+
if (ended_at) {
|
|
10084
|
+
// call was ended before it was accepted or rejected so we should leave it to idle
|
|
10085
|
+
leaveCallIdle = true;
|
|
10086
|
+
}
|
|
10087
|
+
else if (created_by_id && rejected_by) {
|
|
10088
|
+
if (rejected_by[created_by_id]) {
|
|
10089
|
+
// call was cancelled by the caller
|
|
10090
|
+
leaveCallIdle = true;
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
else if (receiver_id && rejected_by) {
|
|
10094
|
+
if (rejected_by[receiver_id]) {
|
|
10095
|
+
// call was rejected by the receiver in some other device
|
|
10096
|
+
leaveCallIdle = true;
|
|
10097
|
+
}
|
|
10098
|
+
}
|
|
10099
|
+
else if (receiver_id && accepted_by) {
|
|
10100
|
+
if (accepted_by[receiver_id]) {
|
|
10101
|
+
// call was accepted by the receiver in some other device
|
|
10102
|
+
leaveCallIdle = true;
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
if (leaveCallIdle) {
|
|
10106
|
+
if (this.state.callingState !== exports.CallingState.IDLE) {
|
|
10107
|
+
this.state.setCallingState(exports.CallingState.IDLE);
|
|
10108
|
+
}
|
|
10109
|
+
}
|
|
10110
|
+
else {
|
|
10111
|
+
if (this.state.callingState === exports.CallingState.IDLE) {
|
|
10112
|
+
this.state.setCallingState(exports.CallingState.RINGING);
|
|
10113
|
+
}
|
|
10114
|
+
this.scheduleAutoDrop();
|
|
10115
|
+
this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
|
|
10116
|
+
}
|
|
10117
|
+
}));
|
|
10118
|
+
};
|
|
10264
10119
|
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
10265
10120
|
// update the permission context.
|
|
10266
10121
|
this.permissionsContext.setPermissions(ownCapabilities);
|
|
@@ -10373,9 +10228,9 @@ class Call {
|
|
|
10373
10228
|
this.statsReporter = undefined;
|
|
10374
10229
|
this.sfuStatsReporter?.stop();
|
|
10375
10230
|
this.sfuStatsReporter = undefined;
|
|
10376
|
-
this.subscriber?.
|
|
10231
|
+
this.subscriber?.dispose();
|
|
10377
10232
|
this.subscriber = undefined;
|
|
10378
|
-
this.publisher?.
|
|
10233
|
+
this.publisher?.dispose();
|
|
10379
10234
|
this.publisher = undefined;
|
|
10380
10235
|
await this.sfuClient?.leaveAndClose(reason);
|
|
10381
10236
|
this.sfuClient = undefined;
|
|
@@ -10413,7 +10268,8 @@ class Call {
|
|
|
10413
10268
|
// call.ring event excludes the call creator in the members list
|
|
10414
10269
|
// as the creator does not get the ring event
|
|
10415
10270
|
// so update the member list accordingly
|
|
10416
|
-
const
|
|
10271
|
+
const { created_by, settings } = event.call;
|
|
10272
|
+
const creator = this.state.members.find((m) => m.user.id === created_by.id);
|
|
10417
10273
|
if (!creator) {
|
|
10418
10274
|
this.state.setMembers(event.members);
|
|
10419
10275
|
}
|
|
@@ -10428,7 +10284,7 @@ class Call {
|
|
|
10428
10284
|
// const calls = useCalls().filter((c) => c.ringing);
|
|
10429
10285
|
const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
|
|
10430
10286
|
this.clientStore.setCalls([this, ...calls]);
|
|
10431
|
-
await this.applyDeviceConfig(false);
|
|
10287
|
+
await this.applyDeviceConfig(settings, false);
|
|
10432
10288
|
};
|
|
10433
10289
|
/**
|
|
10434
10290
|
* Loads the information about the call.
|
|
@@ -10451,7 +10307,7 @@ class Call {
|
|
|
10451
10307
|
this.watching = true;
|
|
10452
10308
|
this.clientStore.registerCall(this);
|
|
10453
10309
|
}
|
|
10454
|
-
await this.applyDeviceConfig(false);
|
|
10310
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10455
10311
|
return response;
|
|
10456
10312
|
};
|
|
10457
10313
|
/**
|
|
@@ -10473,7 +10329,7 @@ class Call {
|
|
|
10473
10329
|
this.watching = true;
|
|
10474
10330
|
this.clientStore.registerCall(this);
|
|
10475
10331
|
}
|
|
10476
|
-
await this.applyDeviceConfig(false);
|
|
10332
|
+
await this.applyDeviceConfig(response.call.settings, false);
|
|
10477
10333
|
return response;
|
|
10478
10334
|
};
|
|
10479
10335
|
/**
|
|
@@ -10575,19 +10431,32 @@ class Call {
|
|
|
10575
10431
|
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
10576
10432
|
if (previousSfuClient !== sfuClient) {
|
|
10577
10433
|
// prepare a generic SDP and send it to the SFU.
|
|
10578
|
-
//
|
|
10434
|
+
// these are throw-away SDPs that the SFU will use to determine
|
|
10579
10435
|
// the capabilities of the client (codec support, etc.)
|
|
10580
|
-
const
|
|
10581
|
-
|
|
10436
|
+
const [subscriberSdp, publisherSdp] = await Promise.all([
|
|
10437
|
+
getGenericSdp('recvonly'),
|
|
10438
|
+
getGenericSdp('sendonly'),
|
|
10439
|
+
]);
|
|
10440
|
+
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10441
|
+
const reconnectDetails = isReconnecting
|
|
10582
10442
|
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
10583
10443
|
: undefined;
|
|
10584
|
-
const
|
|
10585
|
-
|
|
10586
|
-
|
|
10444
|
+
const preferredPublishOptions = !isReconnecting
|
|
10445
|
+
? this.getPreferredPublishOptions()
|
|
10446
|
+
: this.currentPublishOptions || [];
|
|
10447
|
+
const preferredSubscribeOptions = !isReconnecting
|
|
10448
|
+
? this.getPreferredSubscribeOptions()
|
|
10449
|
+
: [];
|
|
10450
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
10451
|
+
subscriberSdp,
|
|
10452
|
+
publisherSdp,
|
|
10587
10453
|
clientDetails,
|
|
10588
10454
|
fastReconnect: performingFastReconnect,
|
|
10589
10455
|
reconnectDetails,
|
|
10456
|
+
preferredPublishOptions,
|
|
10457
|
+
preferredSubscribeOptions,
|
|
10590
10458
|
});
|
|
10459
|
+
this.currentPublishOptions = publishOptions;
|
|
10591
10460
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
10592
10461
|
if (callState) {
|
|
10593
10462
|
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
@@ -10612,17 +10481,13 @@ class Call {
|
|
|
10612
10481
|
connectionConfig,
|
|
10613
10482
|
clientDetails,
|
|
10614
10483
|
statsOptions,
|
|
10484
|
+
publishOptions: this.currentPublishOptions || [],
|
|
10615
10485
|
closePreviousInstances: !performingMigration,
|
|
10616
10486
|
});
|
|
10617
10487
|
}
|
|
10618
10488
|
// make sure we only track connection timing if we are not calling this method as part of a reconnection flow
|
|
10619
10489
|
if (!performingRejoin && !performingFastReconnect && !performingMigration) {
|
|
10620
|
-
this.sfuStatsReporter?.
|
|
10621
|
-
data: {
|
|
10622
|
-
oneofKind: 'connectionTimeSeconds',
|
|
10623
|
-
connectionTimeSeconds: (Date.now() - connectStartTime) / 1000,
|
|
10624
|
-
},
|
|
10625
|
-
});
|
|
10490
|
+
this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
|
|
10626
10491
|
}
|
|
10627
10492
|
if (performingRejoin) {
|
|
10628
10493
|
const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
@@ -10633,8 +10498,8 @@ class Call {
|
|
|
10633
10498
|
}
|
|
10634
10499
|
// device settings should be applied only once, we don't have to
|
|
10635
10500
|
// re-apply them on later reconnections or server-side data fetches
|
|
10636
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
10637
|
-
await this.applyDeviceConfig(true);
|
|
10501
|
+
if (!this.deviceSettingsAppliedOnce && this.state.settings) {
|
|
10502
|
+
await this.applyDeviceConfig(this.state.settings, true);
|
|
10638
10503
|
this.deviceSettingsAppliedOnce = true;
|
|
10639
10504
|
}
|
|
10640
10505
|
// We shouldn't persist the `ring` and `notify` state after joining the call
|
|
@@ -10643,6 +10508,8 @@ class Call {
|
|
|
10643
10508
|
// we will spam the other participants with push notifications and `call.ring` events.
|
|
10644
10509
|
delete this.joinCallData?.ring;
|
|
10645
10510
|
delete this.joinCallData?.notify;
|
|
10511
|
+
// reset the reconnect strategy to unspecified after a successful reconnection
|
|
10512
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
10646
10513
|
this.logger('info', `Joined call ${this.cid}`);
|
|
10647
10514
|
};
|
|
10648
10515
|
/**
|
|
@@ -10652,7 +10519,7 @@ class Call {
|
|
|
10652
10519
|
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
10653
10520
|
const strategy = this.reconnectStrategy;
|
|
10654
10521
|
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
10655
|
-
const announcedTracks = this.publisher?.
|
|
10522
|
+
const announcedTracks = this.publisher?.getAnnouncedTracksForReconnect() || [];
|
|
10656
10523
|
return {
|
|
10657
10524
|
strategy,
|
|
10658
10525
|
announcedTracks,
|
|
@@ -10662,6 +10529,54 @@ class Call {
|
|
|
10662
10529
|
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
10663
10530
|
};
|
|
10664
10531
|
};
|
|
10532
|
+
/**
|
|
10533
|
+
* Prepares the preferred codec for the call.
|
|
10534
|
+
* This is an experimental client feature and subject to change.
|
|
10535
|
+
* @internal
|
|
10536
|
+
*/
|
|
10537
|
+
this.getPreferredPublishOptions = () => {
|
|
10538
|
+
const { preferredCodec, fmtpLine, preferredBitrate, maxSimulcastLayers } = this.clientPublishOptions || {};
|
|
10539
|
+
if (!preferredCodec && !preferredBitrate && !maxSimulcastLayers)
|
|
10540
|
+
return [];
|
|
10541
|
+
const codec = preferredCodec
|
|
10542
|
+
? Codec.create({ name: preferredCodec.split('/').pop(), fmtp: fmtpLine })
|
|
10543
|
+
: undefined;
|
|
10544
|
+
const preferredPublishOptions = [
|
|
10545
|
+
PublishOption.create({
|
|
10546
|
+
trackType: TrackType.VIDEO,
|
|
10547
|
+
codec,
|
|
10548
|
+
bitrate: preferredBitrate,
|
|
10549
|
+
maxSpatialLayers: maxSimulcastLayers,
|
|
10550
|
+
}),
|
|
10551
|
+
];
|
|
10552
|
+
const screenShareSettings = this.screenShare.getSettings();
|
|
10553
|
+
if (screenShareSettings) {
|
|
10554
|
+
preferredPublishOptions.push(PublishOption.create({
|
|
10555
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
10556
|
+
fps: screenShareSettings.maxFramerate,
|
|
10557
|
+
bitrate: screenShareSettings.maxBitrate,
|
|
10558
|
+
}));
|
|
10559
|
+
}
|
|
10560
|
+
return preferredPublishOptions;
|
|
10561
|
+
};
|
|
10562
|
+
/**
|
|
10563
|
+
* Prepares the preferred options for subscribing to tracks.
|
|
10564
|
+
* This is an experimental client feature and subject to change.
|
|
10565
|
+
* @internal
|
|
10566
|
+
*/
|
|
10567
|
+
this.getPreferredSubscribeOptions = () => {
|
|
10568
|
+
const { subscriberCodec, subscriberFmtpLine } = this.clientPublishOptions || {};
|
|
10569
|
+
if (!subscriberCodec || !subscriberFmtpLine)
|
|
10570
|
+
return [];
|
|
10571
|
+
return [
|
|
10572
|
+
SubscribeOption.create({
|
|
10573
|
+
trackType: TrackType.VIDEO,
|
|
10574
|
+
codecs: [
|
|
10575
|
+
{ name: subscriberCodec.split('/').pop(), fmtp: subscriberFmtpLine },
|
|
10576
|
+
],
|
|
10577
|
+
}),
|
|
10578
|
+
];
|
|
10579
|
+
};
|
|
10665
10580
|
/**
|
|
10666
10581
|
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
10667
10582
|
* Uses the provided SFU client to restore the ICE connection.
|
|
@@ -10692,9 +10607,9 @@ class Call {
|
|
|
10692
10607
|
* @internal
|
|
10693
10608
|
*/
|
|
10694
10609
|
this.initPublisherAndSubscriber = (opts) => {
|
|
10695
|
-
const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
|
|
10610
|
+
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, } = opts;
|
|
10696
10611
|
if (closePreviousInstances && this.subscriber) {
|
|
10697
|
-
this.subscriber.
|
|
10612
|
+
this.subscriber.dispose();
|
|
10698
10613
|
}
|
|
10699
10614
|
this.subscriber = new Subscriber({
|
|
10700
10615
|
sfuClient,
|
|
@@ -10713,18 +10628,14 @@ class Call {
|
|
|
10713
10628
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
10714
10629
|
if (!isAnonymous) {
|
|
10715
10630
|
if (closePreviousInstances && this.publisher) {
|
|
10716
|
-
this.publisher.
|
|
10631
|
+
this.publisher.dispose();
|
|
10717
10632
|
}
|
|
10718
|
-
const audioSettings = this.state.settings?.audio;
|
|
10719
|
-
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
10720
|
-
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
10721
10633
|
this.publisher = new Publisher({
|
|
10722
10634
|
sfuClient,
|
|
10723
10635
|
dispatcher: this.dispatcher,
|
|
10724
10636
|
state: this.state,
|
|
10725
10637
|
connectionConfig,
|
|
10726
|
-
|
|
10727
|
-
isRedEnabled,
|
|
10638
|
+
publishOptions,
|
|
10728
10639
|
logTag: String(this.sfuClientTag),
|
|
10729
10640
|
onUnrecoverableError: () => {
|
|
10730
10641
|
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
@@ -10871,47 +10782,31 @@ class Call {
|
|
|
10871
10782
|
* @internal
|
|
10872
10783
|
*/
|
|
10873
10784
|
this.reconnectFast = async () => {
|
|
10874
|
-
|
|
10785
|
+
const reconnectStartTime = Date.now();
|
|
10875
10786
|
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
10876
10787
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10877
10788
|
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
|
-
});
|
|
10789
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
|
|
10887
10790
|
};
|
|
10888
10791
|
/**
|
|
10889
10792
|
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
10890
10793
|
* @internal
|
|
10891
10794
|
*/
|
|
10892
10795
|
this.reconnectRejoin = async () => {
|
|
10893
|
-
|
|
10796
|
+
const reconnectStartTime = Date.now();
|
|
10894
10797
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
10895
10798
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
10896
10799
|
await this.join(this.joinCallData);
|
|
10897
10800
|
await this.restorePublishedTracks();
|
|
10898
10801
|
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
|
-
});
|
|
10802
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
10908
10803
|
};
|
|
10909
10804
|
/**
|
|
10910
10805
|
* Initiates the reconnection flow with the "migrate" strategy.
|
|
10911
10806
|
* @internal
|
|
10912
10807
|
*/
|
|
10913
10808
|
this.reconnectMigrate = async () => {
|
|
10914
|
-
|
|
10809
|
+
const reconnectStartTime = Date.now();
|
|
10915
10810
|
const currentSfuClient = this.sfuClient;
|
|
10916
10811
|
if (!currentSfuClient) {
|
|
10917
10812
|
throw new Error('Cannot migrate without an active SFU client');
|
|
@@ -10945,20 +10840,12 @@ class Call {
|
|
|
10945
10840
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
10946
10841
|
}
|
|
10947
10842
|
finally {
|
|
10948
|
-
currentSubscriber?.
|
|
10949
|
-
currentPublisher?.
|
|
10843
|
+
currentSubscriber?.dispose();
|
|
10844
|
+
currentPublisher?.dispose();
|
|
10950
10845
|
// and close the previous SFU client, without specifying close code
|
|
10951
10846
|
currentSfuClient.close();
|
|
10952
10847
|
}
|
|
10953
|
-
this.sfuStatsReporter?.
|
|
10954
|
-
data: {
|
|
10955
|
-
oneofKind: 'reconnection',
|
|
10956
|
-
reconnection: {
|
|
10957
|
-
timeSeconds: (Date.now() - reconnectStartTime) / 1000,
|
|
10958
|
-
strategy: WebsocketReconnectStrategy.MIGRATE,
|
|
10959
|
-
},
|
|
10960
|
-
},
|
|
10961
|
-
});
|
|
10848
|
+
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.MIGRATE, (Date.now() - reconnectStartTime) / 1000);
|
|
10962
10849
|
};
|
|
10963
10850
|
/**
|
|
10964
10851
|
* Registers the various event handlers for reconnection.
|
|
@@ -11035,23 +10922,16 @@ class Call {
|
|
|
11035
10922
|
// the tracks need to be restored in their original order of publishing
|
|
11036
10923
|
// otherwise, we might get `m-lines order mismatch` errors
|
|
11037
10924
|
for (const trackType of this.trackPublishOrder) {
|
|
10925
|
+
let mediaStream;
|
|
11038
10926
|
switch (trackType) {
|
|
11039
10927
|
case TrackType.AUDIO:
|
|
11040
|
-
|
|
11041
|
-
if (audioStream) {
|
|
11042
|
-
await this.publishAudioStream(audioStream);
|
|
11043
|
-
}
|
|
10928
|
+
mediaStream = this.microphone.state.mediaStream;
|
|
11044
10929
|
break;
|
|
11045
10930
|
case TrackType.VIDEO:
|
|
11046
|
-
|
|
11047
|
-
if (videoStream)
|
|
11048
|
-
await this.publishVideoStream(videoStream);
|
|
10931
|
+
mediaStream = this.camera.state.mediaStream;
|
|
11049
10932
|
break;
|
|
11050
10933
|
case TrackType.SCREEN_SHARE:
|
|
11051
|
-
|
|
11052
|
-
if (screenShareStream) {
|
|
11053
|
-
await this.publishScreenShareStream(screenShareStream);
|
|
11054
|
-
}
|
|
10934
|
+
mediaStream = this.screenShare.state.mediaStream;
|
|
11055
10935
|
break;
|
|
11056
10936
|
// screen share audio can't exist without a screen share, so we handle it there
|
|
11057
10937
|
case TrackType.SCREEN_SHARE_AUDIO:
|
|
@@ -11061,6 +10941,8 @@ class Call {
|
|
|
11061
10941
|
ensureExhausted(trackType, 'Unknown track type');
|
|
11062
10942
|
break;
|
|
11063
10943
|
}
|
|
10944
|
+
if (mediaStream)
|
|
10945
|
+
await this.publish(mediaStream, trackType);
|
|
11064
10946
|
}
|
|
11065
10947
|
};
|
|
11066
10948
|
/**
|
|
@@ -11075,105 +10957,111 @@ class Call {
|
|
|
11075
10957
|
};
|
|
11076
10958
|
/**
|
|
11077
10959
|
* 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.
|
|
10960
|
+
* @deprecated use `call.publish()`.
|
|
11084
10961
|
*/
|
|
11085
10962
|
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);
|
|
10963
|
+
await this.publish(videoStream, TrackType.VIDEO);
|
|
11102
10964
|
};
|
|
11103
10965
|
/**
|
|
11104
10966
|
* 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.
|
|
10967
|
+
* @deprecated use `call.publish()`
|
|
11111
10968
|
*/
|
|
11112
10969
|
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);
|
|
10970
|
+
await this.publish(audioStream, TrackType.AUDIO);
|
|
11129
10971
|
};
|
|
11130
10972
|
/**
|
|
11131
10973
|
* 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.
|
|
10974
|
+
* @deprecated use `call.publish()`
|
|
11137
10975
|
*/
|
|
11138
10976
|
this.publishScreenShareStream = async (screenShareStream) => {
|
|
10977
|
+
await this.publish(screenShareStream, TrackType.SCREEN_SHARE);
|
|
10978
|
+
};
|
|
10979
|
+
/**
|
|
10980
|
+
* Publishes the given media stream.
|
|
10981
|
+
*
|
|
10982
|
+
* @param mediaStream the media stream to publish.
|
|
10983
|
+
* @param trackType the type of the track to announce.
|
|
10984
|
+
*/
|
|
10985
|
+
this.publish = async (mediaStream, trackType) => {
|
|
11139
10986
|
if (!this.sfuClient)
|
|
11140
10987
|
throw new Error(`Call not joined yet.`);
|
|
11141
10988
|
// joining is in progress, and we should wait until the client is ready
|
|
11142
10989
|
await this.sfuClient.joinTask;
|
|
11143
|
-
if (!this.permissionsContext.
|
|
11144
|
-
throw new Error(
|
|
10990
|
+
if (!this.permissionsContext.canPublish(trackType)) {
|
|
10991
|
+
throw new Error(`No permission to publish ${TrackType[trackType]}`);
|
|
11145
10992
|
}
|
|
11146
10993
|
if (!this.publisher)
|
|
11147
10994
|
throw new Error('Publisher is not initialized');
|
|
11148
|
-
const [
|
|
11149
|
-
|
|
11150
|
-
|
|
10995
|
+
const [track] = isAudioTrackType(trackType)
|
|
10996
|
+
? mediaStream.getAudioTracks()
|
|
10997
|
+
: mediaStream.getVideoTracks();
|
|
10998
|
+
if (!track) {
|
|
10999
|
+
throw new Error(`There is no ${TrackType[trackType]} track in the stream`);
|
|
11151
11000
|
}
|
|
11152
|
-
if (
|
|
11153
|
-
|
|
11154
|
-
}
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
this.
|
|
11001
|
+
if (track.readyState === 'ended') {
|
|
11002
|
+
throw new Error(`Can't publish ended tracks.`);
|
|
11003
|
+
}
|
|
11004
|
+
pushToIfMissing(this.trackPublishOrder, trackType);
|
|
11005
|
+
await this.publisher.publish(track, trackType);
|
|
11006
|
+
const trackTypes = [trackType];
|
|
11007
|
+
if (trackType === TrackType.SCREEN_SHARE) {
|
|
11008
|
+
const [audioTrack] = mediaStream.getAudioTracks();
|
|
11009
|
+
if (audioTrack) {
|
|
11010
|
+
pushToIfMissing(this.trackPublishOrder, TrackType.SCREEN_SHARE_AUDIO);
|
|
11011
|
+
await this.publisher.publish(audioTrack, TrackType.SCREEN_SHARE_AUDIO);
|
|
11012
|
+
trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
11163
11013
|
}
|
|
11164
|
-
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
11165
11014
|
}
|
|
11015
|
+
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
11166
11016
|
};
|
|
11167
11017
|
/**
|
|
11168
11018
|
* 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
11019
|
*
|
|
11171
|
-
* @param
|
|
11172
|
-
|
|
11020
|
+
* @param trackTypes the track types to stop publishing.
|
|
11021
|
+
*/
|
|
11022
|
+
this.stopPublish = async (...trackTypes) => {
|
|
11023
|
+
if (!this.sfuClient || !this.publisher)
|
|
11024
|
+
return;
|
|
11025
|
+
this.publisher.stopTracks(...trackTypes);
|
|
11026
|
+
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
11027
|
+
};
|
|
11028
|
+
/**
|
|
11029
|
+
* Updates the call state with the new stream.
|
|
11030
|
+
*
|
|
11031
|
+
* @param mediaStream the new stream to update the call state with.
|
|
11032
|
+
* If undefined, the stream will be removed from the call state.
|
|
11033
|
+
* @param trackTypes the track types to update the call state with.
|
|
11034
|
+
*/
|
|
11035
|
+
this.updateLocalStreamState = async (mediaStream, ...trackTypes) => {
|
|
11036
|
+
if (!this.sfuClient || !this.sfuClient.sessionId)
|
|
11037
|
+
return;
|
|
11038
|
+
await this.notifyTrackMuteState(!mediaStream, ...trackTypes);
|
|
11039
|
+
const { sessionId } = this.sfuClient;
|
|
11040
|
+
for (const trackType of trackTypes) {
|
|
11041
|
+
const streamStateProp = trackTypeToParticipantStreamKey(trackType);
|
|
11042
|
+
if (!streamStateProp)
|
|
11043
|
+
continue;
|
|
11044
|
+
this.state.updateParticipant(sessionId, (p) => ({
|
|
11045
|
+
publishedTracks: mediaStream
|
|
11046
|
+
? pushToIfMissing([...p.publishedTracks], trackType)
|
|
11047
|
+
: p.publishedTracks.filter((t) => t !== trackType),
|
|
11048
|
+
[streamStateProp]: mediaStream,
|
|
11049
|
+
}));
|
|
11050
|
+
}
|
|
11051
|
+
};
|
|
11052
|
+
/**
|
|
11053
|
+
* Updates the preferred publishing options
|
|
11054
|
+
*
|
|
11055
|
+
* @internal
|
|
11056
|
+
* @param options the options to use.
|
|
11173
11057
|
*/
|
|
11174
|
-
this.
|
|
11175
|
-
this.logger('
|
|
11176
|
-
|
|
11058
|
+
this.updatePublishOptions = (options) => {
|
|
11059
|
+
this.logger('warn', '[call.updatePublishOptions]: You are manually overriding the publish options for this call. ' +
|
|
11060
|
+
'This is not recommended, and it can cause call stability/compatibility issues. Use with caution.');
|
|
11061
|
+
if (this.state.callingState === exports.CallingState.JOINED) {
|
|
11062
|
+
this.logger('warn', 'Updating publish options after joining the call does not have an effect');
|
|
11063
|
+
}
|
|
11064
|
+
this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
|
|
11177
11065
|
};
|
|
11178
11066
|
/**
|
|
11179
11067
|
* Notifies the SFU that a noise cancellation process has started.
|
|
@@ -11195,6 +11083,15 @@ class Call {
|
|
|
11195
11083
|
this.logger('warn', 'Failed to notify stop of noise cancellation', err);
|
|
11196
11084
|
});
|
|
11197
11085
|
};
|
|
11086
|
+
/**
|
|
11087
|
+
* Notifies the SFU about the mute state of the given track types.
|
|
11088
|
+
* @internal
|
|
11089
|
+
*/
|
|
11090
|
+
this.notifyTrackMuteState = async (muted, ...trackTypes) => {
|
|
11091
|
+
if (!this.sfuClient)
|
|
11092
|
+
return;
|
|
11093
|
+
await this.sfuClient.updateMuteStates(trackTypes.map((trackType) => ({ trackType, muted })));
|
|
11094
|
+
};
|
|
11198
11095
|
/**
|
|
11199
11096
|
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
|
|
11200
11097
|
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
@@ -11658,70 +11555,14 @@ class Call {
|
|
|
11658
11555
|
*
|
|
11659
11556
|
* @internal
|
|
11660
11557
|
*/
|
|
11661
|
-
this.applyDeviceConfig = async (
|
|
11662
|
-
await this.
|
|
11558
|
+
this.applyDeviceConfig = async (settings, publish) => {
|
|
11559
|
+
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
11663
11560
|
this.logger('warn', 'Camera init failed', err);
|
|
11664
11561
|
});
|
|
11665
|
-
await this.
|
|
11562
|
+
await this.microphone.apply(settings.audio, publish).catch((err) => {
|
|
11666
11563
|
this.logger('warn', 'Mic init failed', err);
|
|
11667
11564
|
});
|
|
11668
11565
|
};
|
|
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
11566
|
/**
|
|
11726
11567
|
* Will begin tracking the given element for visibility changes within the
|
|
11727
11568
|
* configured viewport element (`call.setViewport`).
|
|
@@ -11870,109 +11711,6 @@ class Call {
|
|
|
11870
11711
|
this.screenShare = new ScreenShareManager(this);
|
|
11871
11712
|
this.dynascaleManager = new DynascaleManager(this.state, this.speaker);
|
|
11872
11713
|
}
|
|
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
11714
|
/**
|
|
11977
11715
|
* A flag indicating whether the call is "ringing" type of call.
|
|
11978
11716
|
*/
|
|
@@ -11991,15 +11729,6 @@ class Call {
|
|
|
11991
11729
|
get isCreatedByMe() {
|
|
11992
11730
|
return this.state.createdBy?.id === this.currentUserId;
|
|
11993
11731
|
}
|
|
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
11732
|
}
|
|
12004
11733
|
|
|
12005
11734
|
/**
|
|
@@ -13107,7 +12836,7 @@ class StreamClient {
|
|
|
13107
12836
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
13108
12837
|
};
|
|
13109
12838
|
this.getUserAgent = () => {
|
|
13110
|
-
const version = "1.
|
|
12839
|
+
const version = "1.15.0";
|
|
13111
12840
|
return (this.userAgent ||
|
|
13112
12841
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
13113
12842
|
};
|
|
@@ -13407,7 +13136,7 @@ class StreamVideoClient {
|
|
|
13407
13136
|
clientStore: this.writeableStateStore,
|
|
13408
13137
|
});
|
|
13409
13138
|
call.state.updateFromCallResponse(c.call);
|
|
13410
|
-
await call.applyDeviceConfig(false);
|
|
13139
|
+
await call.applyDeviceConfig(c.call.settings, false);
|
|
13411
13140
|
if (data.watch) {
|
|
13412
13141
|
this.writeableStateStore.registerCall(call);
|
|
13413
13142
|
}
|