@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.browser.es.js +1532 -1784
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1512 -1783
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1532 -1784
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +43 -28
  9. package/dist/src/StreamSfuClient.d.ts +4 -5
  10. package/dist/src/devices/CameraManager.d.ts +5 -8
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
  12. package/dist/src/devices/MicrophoneManager.d.ts +7 -2
  13. package/dist/src/devices/ScreenShareManager.d.ts +1 -2
  14. package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
  15. package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
  16. package/dist/src/helpers/array.d.ts +7 -0
  17. package/dist/src/permissions/PermissionsContext.d.ts +6 -0
  18. package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
  19. package/dist/src/rtc/Dispatcher.d.ts +0 -1
  20. package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
  21. package/dist/src/rtc/Publisher.d.ts +32 -86
  22. package/dist/src/rtc/Subscriber.d.ts +4 -56
  23. package/dist/src/rtc/TransceiverCache.d.ts +55 -0
  24. package/dist/src/rtc/codecs.d.ts +1 -15
  25. package/dist/src/rtc/helpers/sdp.d.ts +8 -0
  26. package/dist/src/rtc/helpers/tracks.d.ts +1 -0
  27. package/dist/src/rtc/index.d.ts +3 -0
  28. package/dist/src/rtc/videoLayers.d.ts +11 -25
  29. package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
  30. package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
  31. package/dist/src/stats/index.d.ts +1 -1
  32. package/dist/src/stats/types.d.ts +8 -0
  33. package/dist/src/types.d.ts +12 -22
  34. package/package.json +1 -1
  35. package/src/Call.ts +254 -268
  36. package/src/StreamSfuClient.ts +9 -14
  37. package/src/StreamVideoClient.ts +1 -1
  38. package/src/__tests__/Call.publishing.test.ts +306 -0
  39. package/src/devices/CameraManager.ts +33 -16
  40. package/src/devices/InputMediaDeviceManager.ts +36 -27
  41. package/src/devices/MicrophoneManager.ts +29 -8
  42. package/src/devices/ScreenShareManager.ts +6 -8
  43. package/src/devices/__tests__/CameraManager.test.ts +111 -14
  44. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
  45. package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
  46. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
  47. package/src/devices/__tests__/mocks.ts +1 -0
  48. package/src/events/__tests__/internal.test.ts +132 -0
  49. package/src/events/__tests__/mutes.test.ts +0 -3
  50. package/src/events/__tests__/speaker.test.ts +92 -0
  51. package/src/events/participant.ts +3 -4
  52. package/src/gen/video/sfu/event/events.ts +91 -30
  53. package/src/gen/video/sfu/models/models.ts +105 -13
  54. package/src/helpers/array.ts +14 -0
  55. package/src/permissions/PermissionsContext.ts +22 -0
  56. package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
  57. package/src/rpc/__tests__/createClient.test.ts +38 -0
  58. package/src/rpc/createClient.ts +11 -5
  59. package/src/rtc/BasePeerConnection.ts +240 -0
  60. package/src/rtc/Dispatcher.ts +0 -9
  61. package/src/rtc/IceTrickleBuffer.ts +24 -4
  62. package/src/rtc/Publisher.ts +210 -528
  63. package/src/rtc/Subscriber.ts +26 -200
  64. package/src/rtc/TransceiverCache.ts +120 -0
  65. package/src/rtc/__tests__/Publisher.test.ts +407 -210
  66. package/src/rtc/__tests__/Subscriber.test.ts +88 -36
  67. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
  68. package/src/rtc/__tests__/videoLayers.test.ts +161 -54
  69. package/src/rtc/codecs.ts +1 -131
  70. package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
  71. package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
  72. package/src/rtc/helpers/sdp.ts +30 -0
  73. package/src/rtc/helpers/tracks.ts +3 -0
  74. package/src/rtc/index.ts +4 -0
  75. package/src/rtc/videoLayers.ts +68 -76
  76. package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
  77. package/src/stats/SfuStatsReporter.ts +31 -3
  78. package/src/stats/index.ts +1 -1
  79. package/src/stats/types.ts +12 -0
  80. package/src/types.ts +12 -22
  81. package/dist/src/helpers/sdp-munging.d.ts +0 -24
  82. package/dist/src/rtc/bitrateLookup.d.ts +0 -2
  83. package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
  84. package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
  85. package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
  86. package/src/helpers/sdp-munging.ts +0 -265
  87. package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
  88. package/src/rtc/__tests__/codecs.test.ts +0 -145
  89. package/src/rtc/bitrateLookup.ts +0 -61
  90. package/src/rtc/helpers/iceCandidate.ts +0 -16
  91. /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
  92. /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 SDP = require('sdp-transform');
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 PublishOptions$Type extends runtime.MessageType {
1229
+ class SubscribeOption$Type extends runtime.MessageType {
1249
1230
  constructor() {
1250
- super('stream.video.sfu.models.PublishOptions', [
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: () => PublishOption,
1247
+ T: () => Codec,
1257
1248
  },
1258
1249
  ]);
1259
1250
  }
1260
1251
  }
1261
1252
  /**
1262
- * @generated MessageType for protobuf message stream.video.sfu.models.PublishOptions
1253
+ * @generated MessageType for protobuf message stream.video.sfu.models.SubscribeOption
1263
1254
  */
1264
- const PublishOptions = new PublishOptions$Type();
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: 11,
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: 13,
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: 'publish_option',
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 CodecNegotiationComplete$Type extends runtime.MessageType {
2328
+ class ChangePublishOptionsComplete$Type extends runtime.MessageType {
2329
2329
  constructor() {
2330
- super('stream.video.sfu.event.CodecNegotiationComplete', []);
2330
+ super('stream.video.sfu.event.ChangePublishOptionsComplete', []);
2331
2331
  }
2332
2332
  }
2333
2333
  /**
2334
- * @generated MessageType for protobuf message stream.video.sfu.event.CodecNegotiationComplete
2334
+ * @generated MessageType for protobuf message stream.video.sfu.event.ChangePublishOptionsComplete
2335
2335
  */
2336
- const CodecNegotiationComplete = new CodecNegotiationComplete$Type();
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
- T: () => PublishOptions,
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
- logger(level, `Calling SFU RPC method ${method.name}`, {
3163
- input,
3164
- options,
3165
- });
3166
- return next(method, input, options);
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
- const version = "1.14.0";
3384
- const [major, minor, patch] = version.split('.');
3385
- let sdkInfo = {
3386
- type: SdkType.PLAIN_JAVASCRIPT,
3387
- major,
3388
- minor,
3389
- patch,
3390
- };
3391
- let osInfo;
3392
- let deviceInfo;
3393
- let webRtcInfo;
3394
- let deviceState = { oneofKind: undefined };
3395
- const setSdkInfo = (info) => {
3396
- sdkInfo = info;
3397
- };
3398
- const getSdkInfo = () => {
3399
- return sdkInfo;
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
- const getDeviceInfo = () => {
3411
- return deviceInfo;
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
- const getWebRTCInfo = () => {
3414
- return webRtcInfo;
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 setWebRTCInfo = (info) => {
3417
- webRtcInfo = info;
3495
+ const isSfuEvent = (eventName) => {
3496
+ return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
3418
3497
  };
3419
- const setThermalState = (state) => {
3420
- if (!osInfo) {
3421
- deviceState = { oneofKind: undefined };
3422
- return;
3423
- }
3424
- if (osInfo.name === 'android') {
3425
- const thermalState = AndroidThermalState[state] ||
3426
- AndroidThermalState.UNSPECIFIED;
3427
- deviceState = {
3428
- oneofKind: 'android',
3429
- android: {
3430
- thermalState,
3431
- isPowerSaverMode: deviceState?.oneofKind === 'android' &&
3432
- deviceState.android.isPowerSaverMode,
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
- if (osInfo.name.toLowerCase() === 'ios') {
3437
- const thermalState = AppleThermalState[state] ||
3438
- AppleThermalState.UNSPECIFIED;
3439
- deviceState = {
3440
- oneofKind: 'apple',
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(iceTrickle);
3546
+ this.subscriberCandidates.next(iceCandidate);
3766
3547
  }
3767
3548
  else if (iceTrickle.peerType === PeerType.PUBLISHER_UNSPECIFIED) {
3768
- this.publisherCandidates.next(iceTrickle);
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
- if (svcCodec) {
3903
- // for SVC codecs, we need to set the scalability mode, and the
3904
- // codec will handle the rest (layers, temporal layers, etc.)
3905
- layer.scalabilityMode = scalabilityMode || 'L3T2_KEY';
3906
- }
3907
- else {
3908
- // for non-SVC codecs, we need to downscale proportionally (simulcast)
3909
- layer.scaleResolutionDownBy = downscaleFactor;
3910
- }
3911
- downscaleFactor *= 2;
3912
- bitrateFactor *= bitrateDownscaleFactor;
3913
- // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
3914
- // when deciding which layer to disable when CPU or bandwidth is constrained.
3915
- // Encodings should be ordered in increasing spatial resolution order.
3916
- optimalVideoLayers.unshift(layer);
3917
- }
3918
- // for simplicity, we start with all layers enabled, then this function
3919
- // will clear/reassign the layers that are not needed
3920
- return withSimulcastConstraints(settings, optimalVideoLayers);
3921
- };
3922
- /**
3923
- * Computes the maximum bitrate for a given resolution.
3924
- * If the current resolution is lower than the target resolution,
3925
- * we want to proportionally reduce the target bitrate.
3926
- * If the current resolution is higher than the target resolution,
3927
- * we want to use the target bitrate.
3928
- *
3929
- * @param targetResolution the target resolution.
3930
- * @param currentWidth the current width of the track.
3931
- * @param currentHeight the current height of the track.
3932
- * @param codecInUse the codec in use.
3933
- * @param publishOptions the publish options.
3934
- */
3935
- const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, codecInUse, publishOptions) => {
3936
- // if the current resolution is lower than the target resolution,
3937
- // we want to proportionally reduce the target bitrate
3938
- const { width: targetWidth, height: targetHeight, bitrate: targetBitrate, } = targetResolution;
3939
- const { preferredBitrate } = publishOptions || {};
3940
- const frameHeight = currentWidth > currentHeight ? currentHeight : currentWidth;
3941
- const bitrate = preferredBitrate ||
3942
- (codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
3943
- if (currentWidth < targetWidth || currentHeight < targetHeight) {
3944
- const currentPixels = currentWidth * currentHeight;
3945
- const targetPixels = targetWidth * targetHeight;
3946
- const reductionFactor = currentPixels / targetPixels;
3947
- return Math.round(bitrate * reductionFactor);
3948
- }
3949
- return bitrate;
3950
- };
3951
- /**
3952
- * Browsers have different simulcast constraints for different video resolutions.
3953
- *
3954
- * This function modifies the provided list of video layers according to the
3955
- * current implementation of simulcast constraints in the Chromium based browsers.
3956
- *
3957
- * https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
3958
- */
3959
- const withSimulcastConstraints = (settings, optimalVideoLayers) => {
3960
- let layers;
3961
- const size = Math.max(settings.width || 0, settings.height || 0);
3962
- if (size <= 320) {
3963
- // provide only one layer 320x240 (q), the one with the highest quality
3964
- layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
3965
- }
3966
- else if (size <= 640) {
3967
- // provide two layers, 160x120 (q) and 640x480 (h)
3968
- layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
3969
- }
3970
- else {
3971
- // provide three layers for sizes > 640x480
3972
- layers = optimalVideoLayers;
3973
- }
3974
- const ridMapping = ['q', 'h', 'f'];
3975
- return layers.map((layer, index) => ({
3976
- ...layer,
3977
- rid: ridMapping[index], // reassign rid
3978
- }));
3979
- };
3980
- const findOptimalScreenSharingLayers = (videoTrack, publishOptions, defaultMaxBitrate = 3000000) => {
3981
- const { screenShareSettings: preferences } = publishOptions || {};
3982
- const settings = videoTrack.getSettings();
3983
- return [
3984
- {
3985
- active: true,
3986
- rid: 'q', // single track, start from 'q'
3987
- width: settings.width || 0,
3988
- height: settings.height || 0,
3989
- scaleResolutionDownBy: 1,
3990
- maxBitrate: preferences?.maxBitrate ?? defaultMaxBitrate,
3991
- maxFramerate: preferences?.maxFramerate ?? 30,
3992
- },
3993
- ];
3994
- };
3995
-
3996
- const ensureExhausted = (x, message) => {
3997
- getLogger(['helpers'])('warn', message, x);
3998
- };
3999
-
4000
- const trackTypeToParticipantStreamKey = (trackType) => {
4001
- switch (trackType) {
4002
- case TrackType.SCREEN_SHARE:
4003
- return 'screenShareStream';
4004
- case TrackType.SCREEN_SHARE_AUDIO:
4005
- return 'screenShareAudioStream';
4006
- case TrackType.VIDEO:
4007
- return 'videoStream';
4008
- case TrackType.AUDIO:
4009
- return 'audioStream';
4010
- case TrackType.UNSPECIFIED:
4011
- throw new Error('Track type is unspecified');
4012
- default:
4013
- ensureExhausted(trackType, 'Unknown track type');
4014
- }
4015
- };
4016
- const muteTypeToTrackType = (muteType) => {
4017
- switch (muteType) {
4018
- case 'audio':
4019
- return TrackType.AUDIO;
4020
- case 'video':
4021
- return TrackType.VIDEO;
4022
- case 'screenshare':
4023
- return TrackType.SCREEN_SHARE;
4024
- case 'screenshare_audio':
4025
- return TrackType.SCREEN_SHARE_AUDIO;
4026
- default:
4027
- ensureExhausted(muteType, 'Unknown mute type');
4028
- }
4029
- };
4030
- const toTrackType = (trackType) => {
4031
- switch (trackType) {
4032
- case 'TRACK_TYPE_AUDIO':
4033
- return TrackType.AUDIO;
4034
- case 'TRACK_TYPE_VIDEO':
4035
- return TrackType.VIDEO;
4036
- case 'TRACK_TYPE_SCREEN_SHARE':
4037
- return TrackType.SCREEN_SHARE;
4038
- case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
4039
- return TrackType.SCREEN_SHARE_AUDIO;
4040
- default:
4041
- return undefined;
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
- * gets the media section for the specified media type.
5637
- * The media section contains the media type, port, codec, and payload type.
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
- const getMedia = (line, mediaType) => {
5641
- const regex = new RegExp(`(m=${mediaType} \\d+ [\\w/]+) ([\\d\\s]+)`);
5642
- const match = regex.exec(line);
5643
- if (match) {
5644
- return {
5645
- original: match[0],
5646
- mediaWithPorts: match[1],
5647
- codecOrder: match[2],
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
- const getMediaSection = (sdp, mediaType) => {
5652
- let media;
5653
- const rtpMap = [];
5654
- const fmtp = [];
5655
- let isTheRequiredMediaSection = false;
5656
- sdp.split(/(\r\n|\r|\n)/).forEach((line) => {
5657
- const isValidLine = /^([a-z])=(.*)/.test(line);
5658
- if (!isValidLine)
5659
- return;
5660
- /*
5661
- NOTE: according to https://www.rfc-editor.org/rfc/rfc8866.pdf
5662
- Each media description starts with an "m=" line and continues to the next media description or the end of the whole session description, whichever comes first
5663
- */
5664
- const type = line[0];
5665
- if (type === 'm') {
5666
- const _media = getMedia(line, mediaType);
5667
- isTheRequiredMediaSection = !!_media;
5668
- if (_media) {
5669
- media = _media;
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
- else if (isTheRequiredMediaSection && type === 'a') {
5673
- const rtpMapLine = getRtpMap(line);
5674
- const fmtpLine = getFmtp(line);
5675
- if (rtpMapLine) {
5676
- rtpMap.push(rtpMapLine);
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
- else if (fmtpLine) {
5679
- fmtp.push(fmtpLine);
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
- * Gets the fmtp line corresponding to opus
5462
+ * Converts the given video layers to SFU video layers.
5693
5463
  */
5694
- const getOpusFmtp = (sdp) => {
5695
- const section = getMediaSection(sdp, 'audio');
5696
- const rtpMap = section?.rtpMap.find((r) => r.codec.toLowerCase() === 'opus');
5697
- const codecId = rtpMap?.payload;
5698
- if (codecId) {
5699
- return section?.fmtp.find((f) => f.payload === codecId);
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
- * Returns an SDP with DTX enabled or disabled.
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 toggleDtx = (sdp, enable) => {
5706
- const opusFmtp = getOpusFmtp(sdp);
5707
- if (!opusFmtp)
5708
- return sdp;
5709
- const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
5710
- const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
5711
- const newFmtp = matchDtx
5712
- ? opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig)
5713
- : `${opusFmtp.original};${requiredDtxConfig}`;
5714
- return sdp.replace(opusFmtp.original, newFmtp);
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
- * Returns and SDP with all the codecs except the given codec removed.
5718
- */
5719
- const preserveCodec = (sdp, mid, codec) => {
5720
- const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
5721
- const toSet = (fmtpLine) => new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
5722
- const equal = (a, b) => {
5723
- if (a.size !== b.size)
5724
- return false;
5725
- for (const item of a)
5726
- if (!b.has(item))
5727
- return false;
5728
- return true;
5729
- };
5730
- const codecFmtp = toSet(codec.sdpFmtpLine || '');
5731
- const parsedSdp = SDP__namespace.parse(sdp);
5732
- for (const media of parsedSdp.media) {
5733
- if (media.type !== kind || String(media.mid) !== mid)
5734
- continue;
5735
- // find the payload id of the desired codec
5736
- const payloads = new Set();
5737
- for (const rtp of media.rtp) {
5738
- if (rtp.codec.toLowerCase() !== codecName)
5739
- continue;
5740
- const match =
5741
- // vp8 doesn't have any fmtp, we preserve it without any additional checks
5742
- codecName === 'vp8'
5743
- ? true
5744
- : media.fmtp.some((f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp));
5745
- if (match) {
5746
- payloads.add(rtp.payload);
5747
- }
5748
- }
5749
- // find the corresponding rtx codec by matching apt=<preserved-codec-payload>
5750
- for (const fmtp of media.fmtp) {
5751
- const match = fmtp.config.match(/(apt)=(\d+)/);
5752
- if (!match)
5753
- continue;
5754
- const [, , preservedCodecPayload] = match;
5755
- if (payloads.has(Number(preservedCodecPayload))) {
5756
- payloads.add(fmtp.payload);
5757
- }
5758
- }
5759
- media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
5760
- media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
5761
- media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
5762
- media.payloads = Array.from(payloads).join(' ');
5763
- }
5764
- return SDP__namespace.write(parsedSdp);
5765
- };
5766
- /**
5767
- * Enables high-quality audio through SDP munging for the given trackMid.
5548
+ * Browsers have different simulcast constraints for different video resolutions.
5768
5549
  *
5769
- * @param sdp the SDP to munge.
5770
- * @param trackMid the trackMid.
5771
- * @param maxBitrate the max bitrate to set.
5772
- */
5773
- const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
5774
- maxBitrate = Math.max(Math.min(maxBitrate, 510000), 96000);
5775
- const parsedSdp = SDP__namespace.parse(sdp);
5776
- const audioMedia = parsedSdp.media.find((m) => m.type === 'audio' && String(m.mid) === trackMid);
5777
- if (!audioMedia)
5778
- return sdp;
5779
- const opusRtp = audioMedia.rtp.find((r) => r.codec === 'opus');
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
- // set maxaveragebitrate, to the given value
5793
- if (opusFmtp.config.match(/maxaveragebitrate=(\d*)/)) {
5794
- opusFmtp.config = opusFmtp.config.replace(/maxaveragebitrate=(\d*)/, `maxaveragebitrate=${maxBitrate}`);
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
- opusFmtp.config = `${opusFmtp.config};maxaveragebitrate=${maxBitrate}`;
5567
+ // provide three layers for sizes > 640x480
5568
+ layers = optimalVideoLayers;
5798
5569
  }
5799
- return SDP__namespace.write(parsedSdp);
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 = SDP__namespace.parse(sdp);
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 === -1)
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({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, onUnrecoverableError, logTag, }) {
5837
- this.transceiverCache = new Map();
5838
- this.trackLayersCache = new Map();
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.publishStream = async (mediaStream, track, trackType, opts = {}) => {
5898
- if (track.readyState === 'ended') {
5899
- throw new Error(`Can't publish a track that has ended already.`);
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
- else {
5918
- await this.updateTransceiver(transceiver, track);
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 = (trackType, track, opts, mediaStream) => {
5928
- const { forceCodec, preferredCodec } = opts;
5929
- const codecInUse = forceCodec || getOptimalVideoCodec(preferredCodec);
5930
- const videoEncodings = this.computeLayers(trackType, track, opts);
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
- streams: trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
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.transceiverInitOrder.push(trackType);
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
- * Stops publishing the given track type to the SFU, if it is currently being published.
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.unpublishStream = async (trackType, stopTrack) => {
5977
- const transceiver = this.transceiverCache.get(trackType);
5978
- if (transceiver &&
5979
- transceiver.sender.track &&
5980
- (stopTrack
5981
- ? transceiver.sender.track.readyState === 'live'
5982
- : transceiver.sender.track.enabled)) {
5983
- stopTrack
5984
- ? transceiver.sender.track.stop()
5985
- : (transceiver.sender.track.enabled = false);
5986
- // We don't need to notify SFU if unpublishing in response to remote soft mute
5987
- if (this.state.localParticipant?.publishedTracks.includes(trackType)) {
5988
- await this.notifyTrackMuteStateChanged(undefined, trackType, true);
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 transceiver = this.transceiverCache.get(trackType);
5999
- if (!transceiver || !transceiver.sender)
6000
- return false;
6001
- const track = transceiver.sender.track;
6002
- return !!track && track.readyState === 'live' && track.enabled;
6003
- };
6004
- this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
6005
- await this.sfuClient.updateMuteState(trackType, isMuted);
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
- * Stops publishing all tracks and stop all tracks.
5709
+ * Maps the given track ID to the corresponding track type.
6028
5710
  */
6029
- this.stopPublishing = () => {
6030
- this.logger('debug', 'Stopping publishing all tracks');
6031
- this.pc.getSenders().forEach((s) => {
6032
- s.track?.stop();
6033
- if (this.pc.signalingState !== 'closed') {
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
- this.changePublishQuality = async (enabledLayers) => {
6039
- this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
6040
- const videoSender = this.transceiverCache.get(TrackType.VIDEO)?.sender;
6041
- if (!videoSender) {
6042
- this.logger('warn', 'Update publish quality, no video sender found.');
6043
- return;
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
- const params = videoSender.getParameters();
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', 'Update publish quality, No suitable video encoding quality found');
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 activeLayers = params.encodings.filter((e) => e.active);
5784
+ const activeEncoders = params.encodings.filter((e) => e.active);
6091
5785
  if (!changed) {
6092
- this.logger('info', `Update publish quality, no change:`, activeLayers);
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
- this.sfuClient
6123
- .iceTrickle({
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.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
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.getAnnouncedTracks = (sdp) => {
6219
- sdp = sdp || this.pc.localDescription?.sdp;
6220
- return this.pc
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
- let optimalLayers;
6231
- const isTrackLive = track.readyState === 'live';
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
- * Detaches the event handlers from the `RTCPeerConnection`.
6398
- * This is useful when we want to replace the `RTCPeerConnection`
6399
- * instance with a new one (in case of migration).
5849
+ * Returns a list of tracks that are currently being published.
5850
+ * @param sdp an optional SDP to extract the `mid` from.
6400
5851
  */
6401
- this.detachEventHandlers = () => {
6402
- this.unregisterOnSubscriberOffer();
6403
- this.unregisterOnIceRestart();
6404
- this.pc.removeEventListener('icecandidate', this.onIceCandidate);
6405
- this.pc.removeEventListener('track', this.handleOnTrack);
6406
- this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
6407
- this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6408
- this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
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 the result of the `RTCPeerConnection.getStats()` method
6412
- * @param selector
6413
- * @returns
6414
- */
6415
- this.getStats = (selector) => {
6416
- return this.pc.getStats(selector);
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
- * Sets the SFU client to use.
6420
- *
6421
- * @param sfuClient the SFU client to use.
5880
+ * Converts the given transceiver to a `TrackInfo` object.
6422
5881
  */
6423
- this.setSfuClient = (sfuClient) => {
6424
- this.sfuClient = sfuClient;
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.sfuClient.iceTrickleBuffer.subscriberCandidates.subscribe(async (candidate) => {
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.onIceConnectionStateChange = () => {
6539
- const state = this.pc.iceConnectionState;
6540
- this.logger('debug', `ICE connection state changed`, state);
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
- const iceRestartConcurrencyTag = Symbol('iceRestart');
6578
- this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
6579
- withoutConcurrency(iceRestartConcurrencyTag, async () => {
6580
- if (iceRestart.peerType !== PeerType.SUBSCRIBER)
6581
- return;
6582
- await this.restartIce();
6583
- }).catch((err) => {
6584
- this.logger('error', `ICERestart failed`, err);
6585
- this.onUnrecoverableError?.();
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.updateMuteState = async (trackType, muted) => {
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({ ...data, sessionId: this.sessionId }), this.abortController.signal);
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].filter(unique),
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, mediaStream) => {
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 (let track of mediaStream.getTracks()) {
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
- const sessionIds = new Set(sessionIdsToTrack);
7636
- if (sessionIds.size > 0) {
7637
- for (let participant of state.participants) {
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 kind = participant.isLocalParticipant
7641
- ? 'publisher'
7642
- : 'subscriber';
7153
+ const { audioStream, isLocalParticipant, sessionId, userId, videoStream, } = participant;
7154
+ const kind = isLocalParticipant ? 'publisher' : 'subscriber';
7643
7155
  try {
7644
- const mergedStream = new MediaStream([
7645
- ...(participant.videoStream?.getVideoTracks() || []),
7646
- ...(participant.audioStream?.getAudioTracks() || []),
7647
- ]);
7648
- participantStats[participant.sessionId] = await getStatsForStream(kind, mergedStream);
7649
- mergedStream.getTracks().forEach((t) => {
7650
- mergedStream.removeTrack(t);
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('error', `Failed to collect stats for ${kind} of ${participant.userId}`, e);
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
- const qualityLimitationReason = [
7807
- qualityLimitationReasons.has('cpu') && 'cpu',
7808
- qualityLimitationReasons.has('bandwidth') && 'bandwidth',
7809
- qualityLimitationReasons.has('other') && 'other',
7810
- ]
7811
- .filter(Boolean)
7812
- .join(', ');
7813
- if (qualityLimitationReason) {
7814
- report.qualityLimitationReasons = qualityLimitationReason;
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
- return report;
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.sendTelemetryData = async (telemetryData) => {
7853
- return this.run(telemetryData);
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
- if (!this.state.mediaStream)
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(stopTracks);
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
- if (this.state.mediaStream &&
9073
- // @ts-expect-error release() is present in react-native-webrtc
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
- this.state.mediaStream.release();
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
- muteTracks() {
8787
+ disableTracks() {
9083
8788
  this.getTracks().forEach((track) => {
9084
8789
  if (track.enabled)
9085
8790
  track.enabled = false;
9086
8791
  });
9087
8792
  }
9088
- unmuteTracks() {
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.muteTracks();
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.unmuteTracks();
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
- this.getTracks().forEach((track) => {
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(() => track.removeEventListener('ended', handleTrackEnded));
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.findDeviceInList(currentDevices, deviceId);
9239
- const prevDevice = this.findDeviceInList(prevDevices, deviceId);
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
- findDeviceInList(devices, deviceId) {
9270
- return devices.find((d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind);
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
- * Sets the preferred codec for encoding the video.
9253
+ * Applies the video settings to the camera.
9535
9254
  *
9536
- * @internal internal use only, not part of the public API.
9537
- * @deprecated use {@link call.updatePublishOptions} instead.
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
- setPreferredCodec(codec) {
9541
- this.call.updatePublishOptions({ preferredCodec: codec });
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, true);
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
- publishStream(stream) {
10050
- return this.call.publishScreenShareStream(stream);
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?.close();
10231
+ this.subscriber?.dispose();
10377
10232
  this.subscriber = undefined;
10378
- this.publisher?.close({ stopTracks: true });
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 creator = this.state.members.find((m) => m.user.id === event.call.created_by.id);
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
- // this is a throw-away SDP that the SFU will use to determine
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 receivingCapabilitiesSdp = await getGenericSdp('recvonly');
10581
- const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
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 { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
10585
- subscriberSdp: receivingCapabilitiesSdp,
10586
- publisherSdp: '',
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?.sendTelemetryData({
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?.getAnnouncedTracks() || [];
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.close();
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.close({ stopTracks: false });
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
- isDtxEnabled,
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
- let reconnectStartTime = Date.now();
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?.sendTelemetryData({
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
- let reconnectStartTime = Date.now();
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?.sendTelemetryData({
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
- let reconnectStartTime = Date.now();
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?.close();
10949
- currentPublisher?.close({ stopTracks: false });
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?.sendTelemetryData({
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
- const audioStream = this.microphone.state.mediaStream;
11041
- if (audioStream) {
11042
- await this.publishAudioStream(audioStream);
11043
- }
10928
+ mediaStream = this.microphone.state.mediaStream;
11044
10929
  break;
11045
10930
  case TrackType.VIDEO:
11046
- const videoStream = this.camera.state.mediaStream;
11047
- if (videoStream)
11048
- await this.publishVideoStream(videoStream);
10931
+ mediaStream = this.camera.state.mediaStream;
11049
10932
  break;
11050
10933
  case TrackType.SCREEN_SHARE:
11051
- const screenShareStream = this.screenShare.state.mediaStream;
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
- * The stream will be stopped if the user changes an input device, or if the user leaves the call.
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
- if (!this.sfuClient)
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
- * The stream will be stopped if the user changes an input device, or if the user leaves the call.
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
- if (!this.sfuClient)
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.hasPermission(OwnCapability.SCREENSHARE)) {
11144
- throw new Error('No permission to publish screen share');
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 [screenShareTrack] = screenShareStream.getVideoTracks();
11149
- if (!screenShareTrack) {
11150
- throw new Error('There is no screen share track in the stream');
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 (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
11153
- this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
11154
- }
11155
- const opts = {
11156
- screenShareSettings: this.screenShare.getSettings(),
11157
- };
11158
- await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
11159
- const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
11160
- if (screenShareAudioTrack) {
11161
- if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
11162
- this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
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 trackType the track type to stop publishing.
11172
- * @param stopTrack if `true` the track will be stopped, else it will be just disabled
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.stopPublish = async (trackType, stopTrack = true) => {
11175
- this.logger('info', `stopPublish ${TrackType[trackType]}, stop tracks: ${stopTrack}`);
11176
- await this.publisher?.unpublishStream(trackType, stopTrack);
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 (status) => {
11662
- await this.initCamera({ setStatus: status }).catch((err) => {
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.initMic({ setStatus: status }).catch((err) => {
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.14.0";
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
  }