@stream-io/video-client 1.8.3 → 1.9.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +434 -449
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +434 -449
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +434 -449
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +10 -12
  9. package/dist/src/compatibility.d.ts +7 -0
  10. package/dist/src/devices/CameraManager.d.ts +2 -22
  11. package/dist/src/events/internal.d.ts +0 -4
  12. package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
  13. package/dist/src/helpers/sdp-munging.d.ts +8 -0
  14. package/dist/src/rtc/Publisher.d.ts +18 -23
  15. package/dist/src/rtc/bitrateLookup.d.ts +2 -0
  16. package/dist/src/rtc/codecs.d.ts +9 -2
  17. package/dist/src/rtc/videoLayers.d.ts +31 -4
  18. package/dist/src/types.d.ts +30 -2
  19. package/package.json +1 -1
  20. package/src/Call.ts +21 -38
  21. package/src/compatibility.ts +7 -0
  22. package/src/devices/CameraManager.ts +18 -47
  23. package/src/devices/ScreenShareManager.ts +1 -3
  24. package/src/devices/__tests__/CameraManager.test.ts +7 -15
  25. package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
  26. package/src/events/callEventHandlers.ts +0 -2
  27. package/src/events/internal.ts +0 -16
  28. package/src/gen/video/sfu/event/events.ts +8 -120
  29. package/src/helpers/sdp-munging.ts +38 -15
  30. package/src/rtc/Publisher.ts +211 -317
  31. package/src/rtc/__tests__/Publisher.test.ts +196 -7
  32. package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
  33. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
  34. package/src/rtc/__tests__/videoLayers.test.ts +51 -36
  35. package/src/rtc/bitrateLookup.ts +61 -0
  36. package/src/rtc/codecs.ts +56 -9
  37. package/src/rtc/videoLayers.ts +74 -23
  38. package/src/types.ts +30 -2
package/dist/index.cjs.js CHANGED
@@ -1733,28 +1733,6 @@ const SignalServer = new runtimeRpc.ServiceType('stream.video.sfu.signal.SignalS
1733
1733
  // @generated by protobuf-ts 2.9.4 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
1734
1734
  // @generated from protobuf file "video/sfu/event/events.proto" (package "stream.video.sfu.event", syntax proto3)
1735
1735
  // tslint:disable
1736
- /**
1737
- * @generated from protobuf enum stream.video.sfu.event.VideoLayerSetting.Priority
1738
- */
1739
- var VideoLayerSetting_Priority;
1740
- (function (VideoLayerSetting_Priority) {
1741
- /**
1742
- * @generated from protobuf enum value: PRIORITY_HIGH_UNSPECIFIED = 0;
1743
- */
1744
- VideoLayerSetting_Priority[VideoLayerSetting_Priority["HIGH_UNSPECIFIED"] = 0] = "HIGH_UNSPECIFIED";
1745
- /**
1746
- * @generated from protobuf enum value: PRIORITY_LOW = 1;
1747
- */
1748
- VideoLayerSetting_Priority[VideoLayerSetting_Priority["LOW"] = 1] = "LOW";
1749
- /**
1750
- * @generated from protobuf enum value: PRIORITY_MEDIUM = 2;
1751
- */
1752
- VideoLayerSetting_Priority[VideoLayerSetting_Priority["MEDIUM"] = 2] = "MEDIUM";
1753
- /**
1754
- * @generated from protobuf enum value: PRIORITY_VERY_LOW = 3;
1755
- */
1756
- VideoLayerSetting_Priority[VideoLayerSetting_Priority["VERY_LOW"] = 3] = "VERY_LOW";
1757
- })(VideoLayerSetting_Priority || (VideoLayerSetting_Priority = {}));
1758
1736
  // @generated message type with reflection information, may provide speed optimized methods
1759
1737
  class SfuEvent$Type extends runtime.MessageType {
1760
1738
  constructor() {
@@ -2426,32 +2404,9 @@ class AudioLevelChanged$Type extends runtime.MessageType {
2426
2404
  */
2427
2405
  const AudioLevelChanged = new AudioLevelChanged$Type();
2428
2406
  // @generated message type with reflection information, may provide speed optimized methods
2429
- class AudioMediaRequest$Type extends runtime.MessageType {
2430
- constructor() {
2431
- super('stream.video.sfu.event.AudioMediaRequest', [
2432
- {
2433
- no: 1,
2434
- name: 'channel_count',
2435
- kind: 'scalar',
2436
- T: 5 /*ScalarType.INT32*/,
2437
- },
2438
- ]);
2439
- }
2440
- }
2441
- /**
2442
- * @generated MessageType for protobuf message stream.video.sfu.event.AudioMediaRequest
2443
- */
2444
- const AudioMediaRequest = new AudioMediaRequest$Type();
2445
- // @generated message type with reflection information, may provide speed optimized methods
2446
2407
  class AudioSender$Type extends runtime.MessageType {
2447
2408
  constructor() {
2448
2409
  super('stream.video.sfu.event.AudioSender', [
2449
- {
2450
- no: 1,
2451
- name: 'media_request',
2452
- kind: 'message',
2453
- T: () => AudioMediaRequest,
2454
- },
2455
2410
  { no: 2, name: 'codec', kind: 'message', T: () => Codec },
2456
2411
  ]);
2457
2412
  }
@@ -2461,30 +2416,6 @@ class AudioSender$Type extends runtime.MessageType {
2461
2416
  */
2462
2417
  const AudioSender = new AudioSender$Type();
2463
2418
  // @generated message type with reflection information, may provide speed optimized methods
2464
- class VideoMediaRequest$Type extends runtime.MessageType {
2465
- constructor() {
2466
- super('stream.video.sfu.event.VideoMediaRequest', [
2467
- {
2468
- no: 1,
2469
- name: 'ideal_height',
2470
- kind: 'scalar',
2471
- T: 5 /*ScalarType.INT32*/,
2472
- },
2473
- { no: 2, name: 'ideal_width', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
2474
- {
2475
- no: 3,
2476
- name: 'ideal_frame_rate',
2477
- kind: 'scalar',
2478
- T: 5 /*ScalarType.INT32*/,
2479
- },
2480
- ]);
2481
- }
2482
- }
2483
- /**
2484
- * @generated MessageType for protobuf message stream.video.sfu.event.VideoMediaRequest
2485
- */
2486
- const VideoMediaRequest = new VideoMediaRequest$Type();
2487
- // @generated message type with reflection information, may provide speed optimized methods
2488
2419
  class VideoLayerSetting$Type extends runtime.MessageType {
2489
2420
  constructor() {
2490
2421
  super('stream.video.sfu.event.VideoLayerSetting', [
@@ -2497,16 +2428,6 @@ class VideoLayerSetting$Type extends runtime.MessageType {
2497
2428
  kind: 'scalar',
2498
2429
  T: 2 /*ScalarType.FLOAT*/,
2499
2430
  },
2500
- {
2501
- no: 5,
2502
- name: 'priority',
2503
- kind: 'enum',
2504
- T: () => [
2505
- 'stream.video.sfu.event.VideoLayerSetting.Priority',
2506
- VideoLayerSetting_Priority,
2507
- 'PRIORITY_',
2508
- ],
2509
- },
2510
2431
  { no: 6, name: 'codec', kind: 'message', T: () => Codec },
2511
2432
  {
2512
2433
  no: 7,
@@ -2514,6 +2435,12 @@ class VideoLayerSetting$Type extends runtime.MessageType {
2514
2435
  kind: 'scalar',
2515
2436
  T: 13 /*ScalarType.UINT32*/,
2516
2437
  },
2438
+ {
2439
+ no: 8,
2440
+ name: 'scalability_mode',
2441
+ kind: 'scalar',
2442
+ T: 9 /*ScalarType.STRING*/,
2443
+ },
2517
2444
  ]);
2518
2445
  }
2519
2446
  }
@@ -2525,12 +2452,6 @@ const VideoLayerSetting = new VideoLayerSetting$Type();
2525
2452
  class VideoSender$Type extends runtime.MessageType {
2526
2453
  constructor() {
2527
2454
  super('stream.video.sfu.event.VideoSender', [
2528
- {
2529
- no: 1,
2530
- name: 'media_request',
2531
- kind: 'message',
2532
- T: () => VideoMediaRequest,
2533
- },
2534
2455
  { no: 2, name: 'codec', kind: 'message', T: () => Codec },
2535
2456
  {
2536
2457
  no: 3,
@@ -2631,7 +2552,6 @@ var events = /*#__PURE__*/Object.freeze({
2631
2552
  __proto__: null,
2632
2553
  AudioLevel: AudioLevel,
2633
2554
  AudioLevelChanged: AudioLevelChanged,
2634
- AudioMediaRequest: AudioMediaRequest,
2635
2555
  AudioSender: AudioSender,
2636
2556
  CallEnded: CallEnded,
2637
2557
  CallGrantsUpdated: CallGrantsUpdated,
@@ -2662,8 +2582,6 @@ var events = /*#__PURE__*/Object.freeze({
2662
2582
  TrackPublished: TrackPublished,
2663
2583
  TrackUnpublished: TrackUnpublished,
2664
2584
  VideoLayerSetting: VideoLayerSetting,
2665
- get VideoLayerSetting_Priority () { return VideoLayerSetting_Priority; },
2666
- VideoMediaRequest: VideoMediaRequest,
2667
2585
  VideoSender: VideoSender
2668
2586
  });
2669
2587
 
@@ -3041,7 +2959,7 @@ const retryable = async (rpc, signal) => {
3041
2959
  return result;
3042
2960
  };
3043
2961
 
3044
- const version = "1.8.3";
2962
+ const version = "1.9.0";
3045
2963
  const [major, minor, patch] = version.split('.');
3046
2964
  let sdkInfo = {
3047
2965
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -3107,6 +3025,38 @@ const getClientDetails = () => {
3107
3025
  };
3108
3026
  };
3109
3027
 
3028
+ /**
3029
+ * Checks whether the current browser is Safari.
3030
+ */
3031
+ const isSafari = () => {
3032
+ if (typeof navigator === 'undefined')
3033
+ return false;
3034
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
3035
+ };
3036
+ /**
3037
+ * Checks whether the current browser is Firefox.
3038
+ */
3039
+ const isFirefox = () => {
3040
+ if (typeof navigator === 'undefined')
3041
+ return false;
3042
+ return navigator.userAgent?.includes('Firefox');
3043
+ };
3044
+ /**
3045
+ * Checks whether the current browser is Google Chrome.
3046
+ */
3047
+ const isChrome = () => {
3048
+ if (typeof navigator === 'undefined')
3049
+ return false;
3050
+ return navigator.userAgent?.includes('Chrome');
3051
+ };
3052
+
3053
+ var browsers = /*#__PURE__*/Object.freeze({
3054
+ __proto__: null,
3055
+ isChrome: isChrome,
3056
+ isFirefox: isFirefox,
3057
+ isSafari: isSafari
3058
+ });
3059
+
3110
3060
  /**
3111
3061
  * Returns back a list of sorted codecs, with the preferred codec first.
3112
3062
  *
@@ -3182,18 +3132,58 @@ const getGenericSdp = async (direction) => {
3182
3132
  return sdp;
3183
3133
  };
3184
3134
  /**
3185
- * Returns the optimal codec for RN.
3135
+ * Returns the optimal video codec for the device.
3186
3136
  */
3187
- const getRNOptimalCodec = () => {
3188
- const osName = getOSInfo()?.name.toLowerCase();
3189
- // in ipads it was noticed that if vp8 codec is used
3190
- // then the bytes sent is 0 in the outbound-rtp
3191
- // so we are forcing h264 codec for ipads
3192
- if (osName === 'ipados')
3137
+ const getOptimalVideoCodec = (preferredCodec) => {
3138
+ if (isReactNative()) {
3139
+ const os = getOSInfo()?.name.toLowerCase();
3140
+ if (os === 'android')
3141
+ return preferredOr(preferredCodec, 'vp8');
3142
+ if (os === 'ios' || os === 'ipados')
3143
+ return 'h264';
3144
+ return preferredOr(preferredCodec, 'h264');
3145
+ }
3146
+ if (isSafari())
3193
3147
  return 'h264';
3194
- if (osName === 'android')
3148
+ if (isFirefox())
3195
3149
  return 'vp8';
3196
- return undefined;
3150
+ return preferredOr(preferredCodec, 'vp8');
3151
+ };
3152
+ /**
3153
+ * Determines if the platform supports the preferred codec.
3154
+ * If not, it returns the fallback codec.
3155
+ */
3156
+ const preferredOr = (codec, fallback) => {
3157
+ if (!codec)
3158
+ return fallback;
3159
+ if (!('getCapabilities' in RTCRtpSender))
3160
+ return fallback;
3161
+ const capabilities = RTCRtpSender.getCapabilities('video');
3162
+ if (!capabilities)
3163
+ return fallback;
3164
+ // Safari and Firefox do not have a good support encoding to SVC codecs,
3165
+ // so we disable it for them.
3166
+ if (isSvcCodec(codec) && (isSafari() || isFirefox()))
3167
+ return fallback;
3168
+ const { codecs } = capabilities;
3169
+ const codecMimeType = `video/${codec}`.toLowerCase();
3170
+ return codecs.some((c) => c.mimeType.toLowerCase() === codecMimeType)
3171
+ ? codec
3172
+ : fallback;
3173
+ };
3174
+ /**
3175
+ * Returns whether the codec is an SVC codec.
3176
+ *
3177
+ * @param codecOrMimeType the codec to check.
3178
+ */
3179
+ const isSvcCodec = (codecOrMimeType) => {
3180
+ if (!codecOrMimeType)
3181
+ return false;
3182
+ codecOrMimeType = codecOrMimeType.toLowerCase();
3183
+ return (codecOrMimeType === 'vp9' ||
3184
+ codecOrMimeType === 'av1' ||
3185
+ codecOrMimeType === 'video/vp9' ||
3186
+ codecOrMimeType === 'video/av1');
3197
3187
  };
3198
3188
 
3199
3189
  const sfuEventKinds = {
@@ -3301,6 +3291,57 @@ function getIceCandidate(candidate) {
3301
3291
  }
3302
3292
  }
3303
3293
 
3294
+ const bitrateLookupTable = {
3295
+ h264: {
3296
+ 2160: 5000000,
3297
+ 1440: 3500000,
3298
+ 1080: 2750000,
3299
+ 720: 1250000,
3300
+ 540: 750000,
3301
+ 360: 400000,
3302
+ default: 1250000,
3303
+ },
3304
+ vp8: {
3305
+ 2160: 5000000,
3306
+ 1440: 2750000,
3307
+ 1080: 2000000,
3308
+ 720: 1250000,
3309
+ 540: 600000,
3310
+ 360: 350000,
3311
+ default: 1250000,
3312
+ },
3313
+ vp9: {
3314
+ 2160: 3000000,
3315
+ 1440: 2000000,
3316
+ 1080: 1500000,
3317
+ 720: 1250000,
3318
+ 540: 500000,
3319
+ 360: 275000,
3320
+ default: 1250000,
3321
+ },
3322
+ av1: {
3323
+ 2160: 2000000,
3324
+ 1440: 1550000,
3325
+ 1080: 1000000,
3326
+ 720: 600000,
3327
+ 540: 350000,
3328
+ 360: 200000,
3329
+ default: 600000,
3330
+ },
3331
+ };
3332
+ const getOptimalBitrate = (codec, frameHeight) => {
3333
+ const codecLookup = bitrateLookupTable[codec];
3334
+ if (!codecLookup)
3335
+ throw new Error(`Unknown codec: ${codec}`);
3336
+ let bitrate = codecLookup[frameHeight];
3337
+ if (!bitrate) {
3338
+ const keys = Object.keys(codecLookup).map(Number);
3339
+ const nearest = keys.reduce((a, b) => Math.abs(b - frameHeight) < Math.abs(a - frameHeight) ? b : a);
3340
+ bitrate = codecLookup[nearest];
3341
+ }
3342
+ return bitrate ?? codecLookup.default;
3343
+ };
3344
+
3304
3345
  const DEFAULT_BITRATE = 1250000;
3305
3346
  const defaultTargetResolution = {
3306
3347
  bitrate: DEFAULT_BITRATE,
@@ -3312,38 +3353,74 @@ const defaultBitratePerRid = {
3312
3353
  h: 750000,
3313
3354
  f: DEFAULT_BITRATE,
3314
3355
  };
3356
+ /**
3357
+ * In SVC, we need to send only one video encoding (layer).
3358
+ * this layer will have the additional spatial and temporal layers
3359
+ * defined via the scalabilityMode property.
3360
+ *
3361
+ * @param layers the layers to process.
3362
+ */
3363
+ const toSvcEncodings = (layers) => {
3364
+ // we take the `f` layer, and we rename it to `q`.
3365
+ return layers?.filter((l) => l.rid === 'f').map((l) => ({ ...l, rid: 'q' }));
3366
+ };
3367
+ /**
3368
+ * Converts the rid to a video quality.
3369
+ */
3370
+ const ridToVideoQuality = (rid) => {
3371
+ return rid === 'q'
3372
+ ? VideoQuality.LOW_UNSPECIFIED
3373
+ : rid === 'h'
3374
+ ? VideoQuality.MID
3375
+ : VideoQuality.HIGH; // default to HIGH
3376
+ };
3315
3377
  /**
3316
3378
  * Determines the most optimal video layers for simulcasting
3317
3379
  * for the given track.
3318
3380
  *
3319
3381
  * @param videoTrack the video track to find optimal layers for.
3320
3382
  * @param targetResolution the expected target resolution.
3383
+ * @param codecInUse the codec in use.
3321
3384
  * @param publishOptions the publish options for the track.
3322
3385
  */
3323
- const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetResolution, publishOptions) => {
3386
+ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetResolution, codecInUse, publishOptions) => {
3324
3387
  const optimalVideoLayers = [];
3325
3388
  const settings = videoTrack.getSettings();
3326
- const { width: w = 0, height: h = 0 } = settings;
3327
- const { preferredBitrate, bitrateDownscaleFactor = 2 } = publishOptions || {};
3328
- const maxBitrate = getComputedMaxBitrate(targetResolution, w, h, preferredBitrate);
3389
+ const { width = 0, height = 0 } = settings;
3390
+ const { scalabilityMode, bitrateDownscaleFactor = 2 } = publishOptions || {};
3391
+ const maxBitrate = getComputedMaxBitrate(targetResolution, width, height, codecInUse, publishOptions);
3329
3392
  let downscaleFactor = 1;
3330
3393
  let bitrateFactor = 1;
3331
- ['f', 'h', 'q'].forEach((rid) => {
3332
- // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
3333
- // when deciding which layer to disable when CPU or bandwidth is constrained.
3334
- // Encodings should be ordered in increasing spatial resolution order.
3335
- optimalVideoLayers.unshift({
3394
+ const svcCodec = isSvcCodec(codecInUse);
3395
+ for (const rid of ['f', 'h', 'q']) {
3396
+ const layer = {
3336
3397
  active: true,
3337
3398
  rid,
3338
- width: Math.round(w / downscaleFactor),
3339
- height: Math.round(h / downscaleFactor),
3340
- maxBitrate: Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
3341
- scaleResolutionDownBy: downscaleFactor,
3399
+ width,
3400
+ height,
3401
+ maxBitrate,
3342
3402
  maxFramerate: 30,
3343
- });
3344
- downscaleFactor *= 2;
3345
- bitrateFactor *= bitrateDownscaleFactor;
3346
- });
3403
+ };
3404
+ if (svcCodec) {
3405
+ // for SVC codecs, we need to set the scalability mode, and the
3406
+ // codec will handle the rest (layers, temporal layers, etc.)
3407
+ layer.scalabilityMode = scalabilityMode || 'L3T2_KEY';
3408
+ }
3409
+ else {
3410
+ // for non-SVC codecs, we need to downscale proportionally (simulcast)
3411
+ layer.width = Math.round(width / downscaleFactor);
3412
+ layer.height = Math.round(height / downscaleFactor);
3413
+ const bitrate = Math.round(maxBitrate / bitrateFactor);
3414
+ layer.maxBitrate = bitrate || defaultBitratePerRid[rid];
3415
+ layer.scaleResolutionDownBy = downscaleFactor;
3416
+ downscaleFactor *= 2;
3417
+ bitrateFactor *= bitrateDownscaleFactor;
3418
+ }
3419
+ // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
3420
+ // when deciding which layer to disable when CPU or bandwidth is constrained.
3421
+ // Encodings should be ordered in increasing spatial resolution order.
3422
+ optimalVideoLayers.unshift(layer);
3423
+ }
3347
3424
  // for simplicity, we start with all layers enabled, then this function
3348
3425
  // will clear/reassign the layers that are not needed
3349
3426
  return withSimulcastConstraints(settings, optimalVideoLayers);
@@ -3358,13 +3435,17 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
3358
3435
  * @param targetResolution the target resolution.
3359
3436
  * @param currentWidth the current width of the track.
3360
3437
  * @param currentHeight the current height of the track.
3361
- * @param preferredBitrate the preferred bitrate for the track.
3438
+ * @param codecInUse the codec in use.
3439
+ * @param publishOptions the publish options.
3362
3440
  */
3363
- const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, preferredBitrate) => {
3441
+ const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, codecInUse, publishOptions) => {
3364
3442
  // if the current resolution is lower than the target resolution,
3365
3443
  // we want to proportionally reduce the target bitrate
3366
3444
  const { width: targetWidth, height: targetHeight, bitrate: targetBitrate, } = targetResolution;
3367
- const bitrate = preferredBitrate || targetBitrate;
3445
+ const { preferredBitrate } = publishOptions || {};
3446
+ const frameHeight = currentWidth > currentHeight ? currentHeight : currentWidth;
3447
+ const bitrate = preferredBitrate ||
3448
+ (codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
3368
3449
  if (currentWidth < targetWidth || currentHeight < targetHeight) {
3369
3450
  const currentPixels = currentWidth * currentHeight;
3370
3451
  const targetPixels = targetWidth * targetHeight;
@@ -5034,19 +5115,14 @@ const getOpusFmtp = (sdp) => {
5034
5115
  */
5035
5116
  const toggleDtx = (sdp, enable) => {
5036
5117
  const opusFmtp = getOpusFmtp(sdp);
5037
- if (opusFmtp) {
5038
- const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
5039
- const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
5040
- if (matchDtx) {
5041
- const newFmtp = opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig);
5042
- return sdp.replace(opusFmtp.original, newFmtp);
5043
- }
5044
- else {
5045
- const newFmtp = `${opusFmtp.original};${requiredDtxConfig}`;
5046
- return sdp.replace(opusFmtp.original, newFmtp);
5047
- }
5048
- }
5049
- return sdp;
5118
+ if (!opusFmtp)
5119
+ return sdp;
5120
+ const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
5121
+ const requiredDtxConfig = `usedtx=${enable ? '1' : '0'}`;
5122
+ const newFmtp = matchDtx
5123
+ ? opusFmtp.original.replace(/usedtx=(\d)/, requiredDtxConfig)
5124
+ : `${opusFmtp.original};${requiredDtxConfig}`;
5125
+ return sdp.replace(opusFmtp.original, newFmtp);
5050
5126
  };
5051
5127
  /**
5052
5128
  * Enables high-quality audio through SDP munging for the given trackMid.
@@ -5083,6 +5159,31 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
5083
5159
  }
5084
5160
  return SDP__namespace.write(parsedSdp);
5085
5161
  };
5162
+ /**
5163
+ * Extracts the mid from the transceiver or the SDP.
5164
+ *
5165
+ * @param transceiver the transceiver.
5166
+ * @param transceiverInitIndex the index of the transceiver in the transceiver's init array.
5167
+ * @param sdp the SDP.
5168
+ */
5169
+ const extractMid = (transceiver, transceiverInitIndex, sdp) => {
5170
+ if (transceiver.mid)
5171
+ return transceiver.mid;
5172
+ if (!sdp)
5173
+ return '';
5174
+ const track = transceiver.sender.track;
5175
+ const parsedSdp = SDP__namespace.parse(sdp);
5176
+ const media = parsedSdp.media.find((m) => {
5177
+ return (m.type === track.kind &&
5178
+ // if `msid` is not present, we assume that the track is the first one
5179
+ (m.msid?.includes(track.id) ?? true));
5180
+ });
5181
+ if (typeof media?.mid !== 'undefined')
5182
+ return String(media.mid);
5183
+ if (transceiverInitIndex === -1)
5184
+ return '';
5185
+ return String(transceiverInitIndex);
5186
+ };
5086
5187
 
5087
5188
  /**
5088
5189
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -5092,26 +5193,11 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
5092
5193
  class Publisher {
5093
5194
  /**
5094
5195
  * Constructs a new `Publisher` instance.
5095
- *
5096
- * @param connectionConfig the connection configuration to use.
5097
- * @param sfuClient the SFU client to use.
5098
- * @param state the call state to use.
5099
- * @param dispatcher the dispatcher to use.
5100
- * @param isDtxEnabled whether DTX is enabled.
5101
- * @param isRedEnabled whether RED is enabled.
5102
- * @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
5103
- * @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
5104
- * @param logTag the log tag to use.
5105
5196
  */
5106
5197
  constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, onUnrecoverableError, logTag, }) {
5107
- this.transceiverRegistry = {
5108
- [TrackType.AUDIO]: undefined,
5109
- [TrackType.VIDEO]: undefined,
5110
- [TrackType.SCREEN_SHARE]: undefined,
5111
- [TrackType.SCREEN_SHARE_AUDIO]: undefined,
5112
- [TrackType.UNSPECIFIED]: undefined,
5113
- };
5114
- this.publishOptionsPerTrackType = new Map();
5198
+ this.transceiverCache = new Map();
5199
+ this.trackLayersCache = new Map();
5200
+ this.publishOptsForTrack = new Map();
5115
5201
  /**
5116
5202
  * An array maintaining the order how transceivers were added to the peer connection.
5117
5203
  * This is needed because some browsers (Firefox) don't reliably report
@@ -5120,20 +5206,6 @@ class Publisher {
5120
5206
  * @internal
5121
5207
  */
5122
5208
  this.transceiverInitOrder = [];
5123
- this.trackKindMapping = {
5124
- [TrackType.AUDIO]: 'audio',
5125
- [TrackType.VIDEO]: 'video',
5126
- [TrackType.SCREEN_SHARE]: 'video',
5127
- [TrackType.SCREEN_SHARE_AUDIO]: 'audio',
5128
- [TrackType.UNSPECIFIED]: undefined,
5129
- };
5130
- this.trackLayersCache = {
5131
- [TrackType.AUDIO]: undefined,
5132
- [TrackType.VIDEO]: undefined,
5133
- [TrackType.SCREEN_SHARE]: undefined,
5134
- [TrackType.SCREEN_SHARE_AUDIO]: undefined,
5135
- [TrackType.UNSPECIFIED]: undefined,
5136
- };
5137
5209
  this.isIceRestarting = false;
5138
5210
  this.createPeerConnection = (connectionConfig) => {
5139
5211
  const pc = new RTCPeerConnection(connectionConfig);
@@ -5151,14 +5223,8 @@ class Publisher {
5151
5223
  this.close = ({ stopTracks }) => {
5152
5224
  if (stopTracks) {
5153
5225
  this.stopPublishing();
5154
- Object.keys(this.transceiverRegistry).forEach((trackType) => {
5155
- // @ts-ignore
5156
- this.transceiverRegistry[trackType] = undefined;
5157
- });
5158
- Object.keys(this.trackLayersCache).forEach((trackType) => {
5159
- // @ts-ignore
5160
- this.trackLayersCache[trackType] = undefined;
5161
- });
5226
+ this.transceiverCache.clear();
5227
+ this.trackLayersCache.clear();
5162
5228
  }
5163
5229
  this.detachEventHandlers();
5164
5230
  this.pc.close();
@@ -5170,6 +5236,7 @@ class Publisher {
5170
5236
  */
5171
5237
  this.detachEventHandlers = () => {
5172
5238
  this.unsubscribeOnIceRestart();
5239
+ this.unsubscribeChangePublishQuality();
5173
5240
  this.pc.removeEventListener('icecandidate', this.onIceCandidate);
5174
5241
  this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
5175
5242
  this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
@@ -5192,81 +5259,75 @@ class Publisher {
5192
5259
  if (track.readyState === 'ended') {
5193
5260
  throw new Error(`Can't publish a track that has ended already.`);
5194
5261
  }
5195
- let transceiver = this.pc
5196
- .getTransceivers()
5197
- .find((t) => t === this.transceiverRegistry[trackType] &&
5198
- t.sender.track &&
5199
- t.sender.track?.kind === this.trackKindMapping[trackType]);
5200
- /**
5201
- * An event handler which listens for the 'ended' event on the track.
5202
- * Once the track has ended, it will notify the SFU and update the state.
5203
- */
5204
- const handleTrackEnded = () => {
5205
- this.logger('info', `Track ${TrackType[trackType]} has ended abruptly, notifying the SFU`);
5206
- // cleanup, this event listener needs to run only once.
5207
- track.removeEventListener('ended', handleTrackEnded);
5208
- this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch((err) => this.logger('warn', `Couldn't notify track mute state`, err));
5209
- };
5210
- if (!transceiver) {
5211
- const { settings } = this.state;
5212
- const targetResolution = settings?.video
5213
- .target_resolution;
5214
- const screenShareBitrate = settings?.screensharing.target_resolution?.bitrate;
5215
- const videoEncodings = trackType === TrackType.VIDEO
5216
- ? findOptimalVideoLayers(track, targetResolution, opts)
5217
- : trackType === TrackType.SCREEN_SHARE
5218
- ? findOptimalScreenSharingLayers(track, opts, screenShareBitrate)
5219
- : undefined;
5262
+ // enable the track if it is disabled
5263
+ if (!track.enabled)
5264
+ track.enabled = true;
5265
+ const transceiver = this.transceiverCache.get(trackType);
5266
+ if (!transceiver || !transceiver.sender.track) {
5220
5267
  // listen for 'ended' event on the track as it might be ended abruptly
5221
- // by an external factor as permission revokes, device disconnected, etc.
5268
+ // by an external factors such as permission revokes, a disconnected device, etc.
5222
5269
  // keep in mind that `track.stop()` doesn't trigger this event.
5270
+ const handleTrackEnded = () => {
5271
+ this.logger('info', `Track ${TrackType[trackType]} has ended abruptly`);
5272
+ track.removeEventListener('ended', handleTrackEnded);
5273
+ this.notifyTrackMuteStateChanged(mediaStream, trackType, true).catch((err) => this.logger('warn', `Couldn't notify track mute state`, err));
5274
+ };
5223
5275
  track.addEventListener('ended', handleTrackEnded);
5224
- if (!track.enabled) {
5225
- track.enabled = true;
5226
- }
5227
- transceiver = this.pc.addTransceiver(track, {
5228
- direction: 'sendonly',
5229
- streams: trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
5230
- ? [mediaStream]
5231
- : undefined,
5232
- sendEncodings: videoEncodings,
5233
- });
5234
- this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
5235
- this.transceiverInitOrder.push(trackType);
5236
- this.transceiverRegistry[trackType] = transceiver;
5237
- this.publishOptionsPerTrackType.set(trackType, opts);
5238
- const { preferredCodec } = opts;
5239
- const codec = isReactNative() && trackType === TrackType.VIDEO && !preferredCodec
5240
- ? getRNOptimalCodec()
5241
- : preferredCodec;
5242
- const codecPreferences = 'setCodecPreferences' in transceiver
5243
- ? this.getCodecPreferences(trackType, codec)
5244
- : undefined;
5245
- if (codecPreferences) {
5246
- this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
5247
- try {
5248
- transceiver.setCodecPreferences(codecPreferences);
5249
- }
5250
- catch (err) {
5251
- this.logger('warn', `Couldn't set codec preferences`, err);
5252
- }
5253
- }
5276
+ this.addTransceiver(trackType, track, opts, mediaStream);
5254
5277
  }
5255
5278
  else {
5256
- const previousTrack = transceiver.sender.track;
5257
- // don't stop the track if we are re-publishing the same track
5258
- if (previousTrack && previousTrack !== track) {
5259
- previousTrack.stop();
5260
- previousTrack.removeEventListener('ended', handleTrackEnded);
5261
- track.addEventListener('ended', handleTrackEnded);
5262
- }
5263
- if (!track.enabled) {
5264
- track.enabled = true;
5265
- }
5266
- await transceiver.sender.replaceTrack(track);
5279
+ await this.updateTransceiver(transceiver, track);
5267
5280
  }
5268
5281
  await this.notifyTrackMuteStateChanged(mediaStream, trackType, false);
5269
5282
  };
5283
+ /**
5284
+ * Adds a new transceiver to the peer connection.
5285
+ * This needs to be called when a new track kind is added to the peer connection.
5286
+ * In other cases, use `updateTransceiver` method.
5287
+ */
5288
+ this.addTransceiver = (trackType, track, opts, mediaStream) => {
5289
+ const { forceCodec, preferredCodec } = opts;
5290
+ const codecInUse = forceCodec || getOptimalVideoCodec(preferredCodec);
5291
+ const videoEncodings = this.computeLayers(trackType, track, opts);
5292
+ const transceiver = this.pc.addTransceiver(track, {
5293
+ direction: 'sendonly',
5294
+ streams: trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
5295
+ ? [mediaStream]
5296
+ : undefined,
5297
+ sendEncodings: isSvcCodec(codecInUse)
5298
+ ? toSvcEncodings(videoEncodings)
5299
+ : videoEncodings,
5300
+ });
5301
+ this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
5302
+ this.transceiverInitOrder.push(trackType);
5303
+ this.transceiverCache.set(trackType, transceiver);
5304
+ this.publishOptsForTrack.set(trackType, opts);
5305
+ // handle codec preferences
5306
+ if (!('setCodecPreferences' in transceiver))
5307
+ return;
5308
+ const codecPreferences = this.getCodecPreferences(trackType, trackType === TrackType.VIDEO ? codecInUse : undefined);
5309
+ if (!codecPreferences)
5310
+ return;
5311
+ try {
5312
+ this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
5313
+ transceiver.setCodecPreferences(codecPreferences);
5314
+ }
5315
+ catch (err) {
5316
+ this.logger('warn', `Couldn't set codec preferences`, err);
5317
+ }
5318
+ };
5319
+ /**
5320
+ * Updates the given transceiver with the new track.
5321
+ * Stops the previous track and replaces it with the new one.
5322
+ */
5323
+ this.updateTransceiver = async (transceiver, track) => {
5324
+ const previousTrack = transceiver.sender.track;
5325
+ // don't stop the track if we are re-publishing the same track
5326
+ if (previousTrack && previousTrack !== track) {
5327
+ previousTrack.stop();
5328
+ }
5329
+ await transceiver.sender.replaceTrack(track);
5330
+ };
5270
5331
  /**
5271
5332
  * Stops publishing the given track type to the SFU, if it is currently being published.
5272
5333
  * Underlying track will be stopped and removed from the publisher.
@@ -5274,9 +5335,7 @@ class Publisher {
5274
5335
  * @param stopTrack specifies whether track should be stopped or just disabled
5275
5336
  */
5276
5337
  this.unpublishStream = async (trackType, stopTrack) => {
5277
- const transceiver = this.pc
5278
- .getTransceivers()
5279
- .find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
5338
+ const transceiver = this.transceiverCache.get(trackType);
5280
5339
  if (transceiver &&
5281
5340
  transceiver.sender.track &&
5282
5341
  (stopTrack
@@ -5297,7 +5356,7 @@ class Publisher {
5297
5356
  * @param trackType the track type to check.
5298
5357
  */
5299
5358
  this.isPublishing = (trackType) => {
5300
- const transceiver = this.transceiverRegistry[trackType];
5359
+ const transceiver = this.transceiverCache.get(trackType);
5301
5360
  if (!transceiver || !transceiver.sender)
5302
5361
  return false;
5303
5362
  const track = transceiver.sender.track;
@@ -5337,9 +5396,9 @@ class Publisher {
5337
5396
  }
5338
5397
  });
5339
5398
  };
5340
- this.updateVideoPublishQuality = async (enabledLayers) => {
5399
+ this.changePublishQuality = async (enabledLayers) => {
5341
5400
  this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
5342
- const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
5401
+ const videoSender = this.transceiverCache.get(TrackType.VIDEO)?.sender;
5343
5402
  if (!videoSender) {
5344
5403
  this.logger('warn', 'Update publish quality, no video sender found.');
5345
5404
  return;
@@ -5349,48 +5408,52 @@ class Publisher {
5349
5408
  this.logger('warn', 'Update publish quality, No suitable video encoding quality found');
5350
5409
  return;
5351
5410
  }
5411
+ const [codecInUse] = params.codecs;
5412
+ const usesSvcCodec = codecInUse && isSvcCodec(codecInUse.mimeType);
5352
5413
  let changed = false;
5353
- let enabledRids = enabledLayers
5354
- .filter((ly) => ly.active)
5355
- .map((ly) => ly.name);
5356
- params.encodings.forEach((enc) => {
5414
+ for (const encoder of params.encodings) {
5415
+ const layer = usesSvcCodec
5416
+ ? // for SVC, we only have one layer (q) and often rid is omitted
5417
+ enabledLayers[0]
5418
+ : // for non-SVC, we need to find the layer by rid (simulcast)
5419
+ enabledLayers.find((l) => l.name === encoder.rid);
5357
5420
  // flip 'active' flag only when necessary
5358
- const shouldEnable = enabledRids.includes(enc.rid);
5359
- if (shouldEnable !== enc.active) {
5360
- enc.active = shouldEnable;
5421
+ const shouldActivate = !!layer?.active;
5422
+ if (shouldActivate !== encoder.active) {
5423
+ encoder.active = shouldActivate;
5361
5424
  changed = true;
5362
5425
  }
5363
- if (shouldEnable) {
5364
- let layer = enabledLayers.find((vls) => vls.name === enc.rid);
5365
- if (layer !== undefined) {
5366
- if (layer.scaleResolutionDownBy >= 1 &&
5367
- layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
5368
- this.logger('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
5369
- enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
5370
- changed = true;
5371
- }
5372
- if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
5373
- this.logger('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
5374
- enc.maxBitrate = layer.maxBitrate;
5375
- changed = true;
5376
- }
5377
- if (layer.maxFramerate > 0 &&
5378
- layer.maxFramerate !== enc.maxFramerate) {
5379
- this.logger('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
5380
- enc.maxFramerate = layer.maxFramerate;
5381
- changed = true;
5382
- }
5383
- }
5426
+ // skip the rest of the settings if the layer is disabled or not found
5427
+ if (!layer)
5428
+ continue;
5429
+ const { maxFramerate, scaleResolutionDownBy, maxBitrate, scalabilityMode, } = layer;
5430
+ if (scaleResolutionDownBy >= 1 &&
5431
+ scaleResolutionDownBy !== encoder.scaleResolutionDownBy) {
5432
+ encoder.scaleResolutionDownBy = scaleResolutionDownBy;
5433
+ changed = true;
5434
+ }
5435
+ if (maxBitrate > 0 && maxBitrate !== encoder.maxBitrate) {
5436
+ encoder.maxBitrate = maxBitrate;
5437
+ changed = true;
5438
+ }
5439
+ if (maxFramerate > 0 && maxFramerate !== encoder.maxFramerate) {
5440
+ encoder.maxFramerate = maxFramerate;
5441
+ changed = true;
5442
+ }
5443
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
5444
+ if (scalabilityMode && scalabilityMode !== encoder.scalabilityMode) {
5445
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
5446
+ encoder.scalabilityMode = scalabilityMode;
5447
+ changed = true;
5384
5448
  }
5385
- });
5386
- const activeLayers = params.encodings.filter((e) => e.active);
5387
- if (changed) {
5388
- await videoSender.setParameters(params);
5389
- this.logger('info', `Update publish quality, enabled rids: `, activeLayers);
5390
5449
  }
5391
- else {
5392
- this.logger('info', `Update publish quality, no change: `, activeLayers);
5450
+ const activeLayers = params.encodings.filter((e) => e.active);
5451
+ if (!changed) {
5452
+ this.logger('info', `Update publish quality, no change:`, activeLayers);
5453
+ return;
5393
5454
  }
5455
+ await videoSender.setParameters(params);
5456
+ this.logger('info', `Update publish quality, enabled rids:`, activeLayers);
5394
5457
  };
5395
5458
  /**
5396
5459
  * Returns the result of the `RTCPeerConnection.getStats()` method
@@ -5458,19 +5521,19 @@ class Publisher {
5458
5521
  */
5459
5522
  this.negotiate = async (options) => {
5460
5523
  const offer = await this.pc.createOffer(options);
5461
- let sdp = this.mungeCodecs(offer.sdp);
5462
- if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
5463
- sdp = this.enableHighQualityAudio(sdp);
5524
+ if (offer.sdp) {
5525
+ offer.sdp = toggleDtx(offer.sdp, this.isDtxEnabled);
5526
+ if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
5527
+ offer.sdp = this.enableHighQualityAudio(offer.sdp);
5528
+ }
5464
5529
  }
5465
- // set the munged SDP back to the offer
5466
- offer.sdp = sdp;
5467
5530
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
5468
5531
  if (trackInfos.length === 0) {
5469
5532
  throw new Error(`Can't negotiate without announcing any tracks`);
5470
5533
  }
5471
- this.isIceRestarting = options?.iceRestart ?? false;
5472
- await this.pc.setLocalDescription(offer);
5473
5534
  try {
5535
+ this.isIceRestarting = options?.iceRestart ?? false;
5536
+ await this.pc.setLocalDescription(offer);
5474
5537
  const { response } = await this.sfuClient.setPublisher({
5475
5538
  sdp: offer.sdp || '',
5476
5539
  tracks: trackInfos,
@@ -5493,44 +5556,13 @@ class Publisher {
5493
5556
  });
5494
5557
  };
5495
5558
  this.enableHighQualityAudio = (sdp) => {
5496
- const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
5559
+ const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
5497
5560
  if (!transceiver)
5498
5561
  return sdp;
5499
- const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
5562
+ const transceiverInitIndex = this.transceiverInitOrder.indexOf(TrackType.SCREEN_SHARE_AUDIO);
5563
+ const mid = extractMid(transceiver, transceiverInitIndex, sdp);
5500
5564
  return enableHighQualityAudio(sdp, mid);
5501
5565
  };
5502
- this.mungeCodecs = (sdp) => {
5503
- if (sdp) {
5504
- sdp = toggleDtx(sdp, this.isDtxEnabled);
5505
- }
5506
- return sdp;
5507
- };
5508
- this.extractMid = (transceiver, sdp, trackType) => {
5509
- if (transceiver.mid)
5510
- return transceiver.mid;
5511
- if (!sdp) {
5512
- this.logger('warn', 'No SDP found. Returning empty mid');
5513
- return '';
5514
- }
5515
- this.logger('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
5516
- const track = transceiver.sender.track;
5517
- const parsedSdp = SDP__namespace.parse(sdp);
5518
- const media = parsedSdp.media.find((m) => {
5519
- return (m.type === track.kind &&
5520
- // if `msid` is not present, we assume that the track is the first one
5521
- (m.msid?.includes(track.id) ?? true));
5522
- });
5523
- if (typeof media?.mid === 'undefined') {
5524
- this.logger('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`);
5525
- const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
5526
- if (heuristicMid !== -1) {
5527
- return String(heuristicMid);
5528
- }
5529
- this.logger('debug', 'No heuristic mid found. Returning empty mid');
5530
- return '';
5531
- }
5532
- return String(media.mid);
5533
- };
5534
5566
  /**
5535
5567
  * Returns a list of tracks that are currently being published.
5536
5568
  *
@@ -5539,37 +5571,32 @@ class Publisher {
5539
5571
  */
5540
5572
  this.getAnnouncedTracks = (sdp) => {
5541
5573
  sdp = sdp || this.pc.localDescription?.sdp;
5542
- const { settings } = this.state;
5543
- const targetResolution = settings?.video
5544
- .target_resolution;
5545
5574
  return this.pc
5546
5575
  .getTransceivers()
5547
5576
  .filter((t) => t.direction === 'sendonly' && t.sender.track)
5548
5577
  .map((transceiver) => {
5549
- const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
5578
+ let trackType;
5579
+ this.transceiverCache.forEach((value, key) => {
5580
+ if (value === transceiver)
5581
+ trackType = key;
5582
+ });
5550
5583
  const track = transceiver.sender.track;
5551
5584
  let optimalLayers;
5552
5585
  const isTrackLive = track.readyState === 'live';
5553
5586
  if (isTrackLive) {
5554
- const publishOpts = this.publishOptionsPerTrackType.get(trackType);
5555
- optimalLayers =
5556
- trackType === TrackType.VIDEO
5557
- ? findOptimalVideoLayers(track, targetResolution, publishOpts)
5558
- : trackType === TrackType.SCREEN_SHARE
5559
- ? findOptimalScreenSharingLayers(track, publishOpts)
5560
- : [];
5561
- this.trackLayersCache[trackType] = optimalLayers;
5587
+ optimalLayers = this.computeLayers(trackType, track) || [];
5588
+ this.trackLayersCache.set(trackType, optimalLayers);
5562
5589
  }
5563
5590
  else {
5564
5591
  // we report the last known optimal layers for ended tracks
5565
- optimalLayers = this.trackLayersCache[trackType] || [];
5592
+ optimalLayers = this.trackLayersCache.get(trackType) || [];
5566
5593
  this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
5567
5594
  }
5568
5595
  const layers = optimalLayers.map((optimalLayer) => ({
5569
5596
  rid: optimalLayer.rid || '',
5570
5597
  bitrate: optimalLayer.maxBitrate || 0,
5571
5598
  fps: optimalLayer.maxFramerate || 0,
5572
- quality: this.ridToVideoQuality(optimalLayer.rid || ''),
5599
+ quality: ridToVideoQuality(optimalLayer.rid || ''),
5573
5600
  videoDimension: {
5574
5601
  width: optimalLayer.width,
5575
5602
  height: optimalLayer.height,
@@ -5581,11 +5608,12 @@ class Publisher {
5581
5608
  ].includes(trackType);
5582
5609
  const trackSettings = track.getSettings();
5583
5610
  const isStereo = isAudioTrack && trackSettings.channelCount === 2;
5611
+ const transceiverInitIndex = this.transceiverInitOrder.indexOf(trackType);
5584
5612
  return {
5585
5613
  trackId: track.id,
5586
5614
  layers: layers,
5587
5615
  trackType,
5588
- mid: this.extractMid(transceiver, sdp, trackType),
5616
+ mid: extractMid(transceiver, transceiverInitIndex, sdp),
5589
5617
  stereo: isStereo,
5590
5618
  dtx: isAudioTrack && this.isDtxEnabled,
5591
5619
  red: isAudioTrack && this.isRedEnabled,
@@ -5593,6 +5621,19 @@ class Publisher {
5593
5621
  };
5594
5622
  });
5595
5623
  };
5624
+ this.computeLayers = (trackType, track, opts) => {
5625
+ const { settings } = this.state;
5626
+ const targetResolution = settings?.video
5627
+ .target_resolution;
5628
+ const screenShareBitrate = settings?.screensharing.target_resolution?.bitrate;
5629
+ const publishOpts = opts || this.publishOptsForTrack.get(trackType);
5630
+ const codecInUse = getOptimalVideoCodec(publishOpts?.preferredCodec);
5631
+ return trackType === TrackType.VIDEO
5632
+ ? findOptimalVideoLayers(track, targetResolution, codecInUse, publishOpts)
5633
+ : trackType === TrackType.SCREEN_SHARE
5634
+ ? findOptimalScreenSharingLayers(track, publishOpts, screenShareBitrate)
5635
+ : undefined;
5636
+ };
5596
5637
  this.onIceCandidateError = (e) => {
5597
5638
  const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
5598
5639
  `${e.errorCode}: ${e.errorText}`;
@@ -5619,13 +5660,6 @@ class Publisher {
5619
5660
  this.onSignalingStateChange = () => {
5620
5661
  this.logger('debug', `Signaling state changed`, this.pc.signalingState);
5621
5662
  };
5622
- this.ridToVideoQuality = (rid) => {
5623
- return rid === 'q'
5624
- ? VideoQuality.LOW_UNSPECIFIED
5625
- : rid === 'h'
5626
- ? VideoQuality.MID
5627
- : VideoQuality.HIGH; // default to HIGH
5628
- };
5629
5663
  this.logger = getLogger(['Publisher', logTag]);
5630
5664
  this.pc = this.createPeerConnection(connectionConfig);
5631
5665
  this.sfuClient = sfuClient;
@@ -5641,6 +5675,17 @@ class Publisher {
5641
5675
  this.onUnrecoverableError?.();
5642
5676
  });
5643
5677
  });
5678
+ this.unsubscribeChangePublishQuality = dispatcher.on('changePublishQuality', ({ videoSenders }) => {
5679
+ withoutConcurrency('publisher.changePublishQuality', async () => {
5680
+ for (const videoSender of videoSenders) {
5681
+ const { layers } = videoSender;
5682
+ const enabledLayers = layers.filter((l) => l.active);
5683
+ await this.changePublishQuality(enabledLayers);
5684
+ }
5685
+ }).catch((err) => {
5686
+ this.logger('warn', 'Failed to change publish quality', err);
5687
+ });
5688
+ });
5644
5689
  }
5645
5690
  }
5646
5691
 
@@ -6368,18 +6413,6 @@ const watchCallGrantsUpdated = (state) => {
6368
6413
  };
6369
6414
  };
6370
6415
 
6371
- /**
6372
- * An event responder which handles the `changePublishQuality` event.
6373
- */
6374
- const watchChangePublishQuality = (dispatcher, call) => {
6375
- return dispatcher.on('changePublishQuality', (e) => {
6376
- const { videoSenders } = e;
6377
- videoSenders.forEach((videoSender) => {
6378
- const { layers } = videoSender;
6379
- call.updatePublishQuality(layers.filter((l) => l.active));
6380
- });
6381
- });
6382
- };
6383
6416
  const watchConnectionQualityChanged = (dispatcher, state) => {
6384
6417
  return dispatcher.on('connectionQualityChanged', (e) => {
6385
6418
  const { connectionQualityUpdates } = e;
@@ -6648,7 +6681,6 @@ const registerEventHandlers = (call, dispatcher) => {
6648
6681
  watchSfuCallEnded(call),
6649
6682
  watchLiveEnded(dispatcher, call),
6650
6683
  watchSfuErrorReports(dispatcher),
6651
- watchChangePublishQuality(dispatcher, call),
6652
6684
  watchConnectionQualityChanged(dispatcher, state),
6653
6685
  watchParticipantCountChanged(dispatcher, state),
6654
6686
  call.on('participantJoined', watchParticipantJoined(state)),
@@ -7083,38 +7115,6 @@ class ViewportTracker {
7083
7115
  }
7084
7116
  }
7085
7117
 
7086
- /**
7087
- * Checks whether the current browser is Safari.
7088
- */
7089
- const isSafari = () => {
7090
- if (typeof navigator === 'undefined')
7091
- return false;
7092
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent || '');
7093
- };
7094
- /**
7095
- * Checks whether the current browser is Firefox.
7096
- */
7097
- const isFirefox = () => {
7098
- if (typeof navigator === 'undefined')
7099
- return false;
7100
- return navigator.userAgent?.includes('Firefox');
7101
- };
7102
- /**
7103
- * Checks whether the current browser is Google Chrome.
7104
- */
7105
- const isChrome = () => {
7106
- if (typeof navigator === 'undefined')
7107
- return false;
7108
- return navigator.userAgent?.includes('Chrome');
7109
- };
7110
-
7111
- var browsers = /*#__PURE__*/Object.freeze({
7112
- __proto__: null,
7113
- isChrome: isChrome,
7114
- isFirefox: isFirefox,
7115
- isSafari: isSafari
7116
- });
7117
-
7118
7118
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
7119
7119
  videoTrack: exports.VisibilityState.UNKNOWN,
7120
7120
  screenShareTrack: exports.VisibilityState.UNKNOWN,
@@ -8536,6 +8536,14 @@ class CameraManagerState extends InputMediaDeviceManagerState {
8536
8536
  }
8537
8537
  }
8538
8538
 
8539
+ /**
8540
+ * Checks if the current platform is a mobile device.
8541
+ *
8542
+ * See:
8543
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
8544
+ */
8545
+ const isMobile = () => /Mobi/i.test(navigator.userAgent);
8546
+
8539
8547
  class CameraManager extends InputMediaDeviceManager {
8540
8548
  /**
8541
8549
  * Constructs a new CameraManager.
@@ -8555,10 +8563,15 @@ class CameraManager extends InputMediaDeviceManager {
8555
8563
  * @param direction the direction of the camera to select.
8556
8564
  */
8557
8565
  async selectDirection(direction) {
8558
- this.state.setDirection(direction);
8559
- // Providing both device id and direction doesn't work, so we deselect the device
8560
- this.state.setDevice(undefined);
8561
- await this.applySettingsToStream();
8566
+ if (isMobile()) {
8567
+ this.state.setDirection(direction);
8568
+ // Providing both device id and direction doesn't work, so we deselect the device
8569
+ this.state.setDevice(undefined);
8570
+ await this.applySettingsToStream();
8571
+ }
8572
+ else {
8573
+ this.logger('warn', 'Camera direction ignored for desktop devices');
8574
+ }
8562
8575
  }
8563
8576
  /**
8564
8577
  * Flips the camera direction: if it's front it will change to back, if it's back, it will change to front.
@@ -8585,10 +8598,11 @@ class CameraManager extends InputMediaDeviceManager {
8585
8598
  this.logger('warn', 'could not apply target resolution', error);
8586
8599
  }
8587
8600
  }
8588
- if (this.enabled) {
8589
- const { width, height } = this.state
8590
- .mediaStream.getVideoTracks()[0]
8591
- ?.getSettings();
8601
+ if (this.enabled && this.state.mediaStream) {
8602
+ const [videoTrack] = this.state.mediaStream.getVideoTracks();
8603
+ if (!videoTrack)
8604
+ return;
8605
+ const { width, height } = videoTrack.getSettings();
8592
8606
  if (width !== this.targetResolution.width ||
8593
8607
  height !== this.targetResolution.height) {
8594
8608
  await this.applySettingsToStream();
@@ -8600,36 +8614,11 @@ class CameraManager extends InputMediaDeviceManager {
8600
8614
  * Sets the preferred codec for encoding the video.
8601
8615
  *
8602
8616
  * @internal internal use only, not part of the public API.
8617
+ * @deprecated use {@link call.updatePublishOptions} instead.
8603
8618
  * @param codec the codec to use for encoding the video.
8604
8619
  */
8605
8620
  setPreferredCodec(codec) {
8606
- this.updatePublishOptions({ preferredCodec: codec });
8607
- }
8608
- /**
8609
- * Updates the preferred publish options for the video stream.
8610
- *
8611
- * @internal
8612
- * @param options the options to use.
8613
- */
8614
- updatePublishOptions(options) {
8615
- this.publishOptions = { ...this.publishOptions, ...options };
8616
- }
8617
- /**
8618
- * Returns the capture resolution of the camera.
8619
- */
8620
- getCaptureResolution() {
8621
- const { mediaStream } = this.state;
8622
- if (!mediaStream)
8623
- return;
8624
- const [videoTrack] = mediaStream.getVideoTracks();
8625
- if (!videoTrack)
8626
- return;
8627
- const settings = videoTrack.getSettings();
8628
- return {
8629
- width: settings.width,
8630
- height: settings.height,
8631
- frameRate: settings.frameRate,
8632
- };
8621
+ this.call.updatePublishOptions({ preferredCodec: codec });
8633
8622
  }
8634
8623
  getDevices() {
8635
8624
  return getVideoDevices();
@@ -8639,14 +8628,14 @@ class CameraManager extends InputMediaDeviceManager {
8639
8628
  constraints.height = this.targetResolution.height;
8640
8629
  // We can't set both device id and facing mode
8641
8630
  // Device id has higher priority
8642
- if (!constraints.deviceId && this.state.direction) {
8631
+ if (!constraints.deviceId && this.state.direction && isMobile()) {
8643
8632
  constraints.facingMode =
8644
8633
  this.state.direction === 'front' ? 'user' : 'environment';
8645
8634
  }
8646
8635
  return getVideoStream(constraints);
8647
8636
  }
8648
8637
  publishStream(stream) {
8649
- return this.call.publishVideoStream(stream, this.publishOptions);
8638
+ return this.call.publishVideoStream(stream);
8650
8639
  }
8651
8640
  stopPublishStream(stopTracks) {
8652
8641
  return this.call.stopPublish(TrackType.VIDEO, stopTracks);
@@ -9136,9 +9125,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
9136
9125
  return getScreenShareStream(constraints);
9137
9126
  }
9138
9127
  publishStream(stream) {
9139
- return this.call.publishScreenShareStream(stream, {
9140
- screenShareSettings: this.state.settings,
9141
- });
9128
+ return this.call.publishScreenShareStream(stream);
9142
9129
  }
9143
9130
  async stopPublishStream(stopTracks) {
9144
9131
  await this.call.stopPublish(TrackType.SCREEN_SHARE, stopTracks);
@@ -10050,16 +10037,13 @@ class Call {
10050
10037
  break;
10051
10038
  case TrackType.VIDEO:
10052
10039
  const videoStream = this.camera.state.mediaStream;
10053
- if (videoStream) {
10054
- await this.publishVideoStream(videoStream, this.camera.publishOptions);
10055
- }
10040
+ if (videoStream)
10041
+ await this.publishVideoStream(videoStream);
10056
10042
  break;
10057
10043
  case TrackType.SCREEN_SHARE:
10058
10044
  const screenShareStream = this.screenShare.state.mediaStream;
10059
10045
  if (screenShareStream) {
10060
- await this.publishScreenShareStream(screenShareStream, {
10061
- screenShareSettings: this.screenShare.getSettings(),
10062
- });
10046
+ await this.publishScreenShareStream(screenShareStream);
10063
10047
  }
10064
10048
  break;
10065
10049
  // screen share audio can't exist without a screen share, so we handle it there
@@ -10090,9 +10074,8 @@ class Call {
10090
10074
  * The previous video stream will be stopped.
10091
10075
  *
10092
10076
  * @param videoStream the video stream to publish.
10093
- * @param opts the options to use when publishing the stream.
10094
10077
  */
10095
- this.publishVideoStream = async (videoStream, opts = {}) => {
10078
+ this.publishVideoStream = async (videoStream) => {
10096
10079
  if (!this.sfuClient)
10097
10080
  throw new Error(`Call not joined yet.`);
10098
10081
  // joining is in progress, and we should wait until the client is ready
@@ -10108,7 +10091,7 @@ class Call {
10108
10091
  if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
10109
10092
  this.trackPublishOrder.push(TrackType.VIDEO);
10110
10093
  }
10111
- await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
10094
+ await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, this.publishOptions);
10112
10095
  };
10113
10096
  /**
10114
10097
  * Starts publishing the given audio stream to the call.
@@ -10144,9 +10127,8 @@ class Call {
10144
10127
  * The previous screen-share stream will be stopped.
10145
10128
  *
10146
10129
  * @param screenShareStream the screen-share stream to publish.
10147
- * @param opts the options to use when publishing the stream.
10148
10130
  */
10149
- this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
10131
+ this.publishScreenShareStream = async (screenShareStream) => {
10150
10132
  if (!this.sfuClient)
10151
10133
  throw new Error(`Call not joined yet.`);
10152
10134
  // joining is in progress, and we should wait until the client is ready
@@ -10163,6 +10145,9 @@ class Call {
10163
10145
  if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
10164
10146
  this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
10165
10147
  }
10148
+ const opts = {
10149
+ screenShareSettings: this.screenShare.getSettings(),
10150
+ };
10166
10151
  await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
10167
10152
  const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
10168
10153
  if (screenShareAudioTrack) {
@@ -10239,15 +10224,6 @@ class Call {
10239
10224
  this.setSortParticipantsBy = (criteria) => {
10240
10225
  return this.state.setSortParticipantsBy(criteria);
10241
10226
  };
10242
- /**
10243
- * Updates the list of video layers to publish.
10244
- *
10245
- * @internal
10246
- * @param enabledLayers the list of layers to enable.
10247
- */
10248
- this.updatePublishQuality = async (enabledLayers) => {
10249
- return this.publisher?.updateVideoPublishQuality(enabledLayers);
10250
- };
10251
10227
  /**
10252
10228
  * Sends a reaction to the other call participants.
10253
10229
  *
@@ -10662,7 +10638,7 @@ class Call {
10662
10638
  if (this.camera.enabled &&
10663
10639
  this.camera.state.mediaStream &&
10664
10640
  !this.publisher?.isPublishing(TrackType.VIDEO)) {
10665
- await this.publishVideoStream(this.camera.state.mediaStream, this.camera.publishOptions);
10641
+ await this.publishVideoStream(this.camera.state.mediaStream);
10666
10642
  }
10667
10643
  // Start camera if backend config specifies, and there is no local setting
10668
10644
  if (this.camera.state.status === undefined &&
@@ -10951,6 +10927,15 @@ class Call {
10951
10927
  get isCreatedByMe() {
10952
10928
  return this.state.createdBy?.id === this.currentUserId;
10953
10929
  }
10930
+ /**
10931
+ * Updates the preferred publishing options
10932
+ *
10933
+ * @internal
10934
+ * @param options the options to use.
10935
+ */
10936
+ updatePublishOptions(options) {
10937
+ this.publishOptions = { ...this.publishOptions, ...options };
10938
+ }
10954
10939
  }
10955
10940
 
10956
10941
  class InsightMetrics {
@@ -12488,7 +12473,7 @@ class StreamClient {
12488
12473
  });
12489
12474
  };
12490
12475
  this.getUserAgent = () => {
12491
- const version = "1.8.3";
12476
+ const version = "1.9.0";
12492
12477
  return (this.userAgent ||
12493
12478
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12494
12479
  };