@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +434 -449
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +434 -449
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +434 -449
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +10 -12
- package/dist/src/compatibility.d.ts +7 -0
- package/dist/src/devices/CameraManager.d.ts +2 -22
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
- package/dist/src/helpers/sdp-munging.d.ts +8 -0
- package/dist/src/rtc/Publisher.d.ts +18 -23
- package/dist/src/rtc/bitrateLookup.d.ts +2 -0
- package/dist/src/rtc/codecs.d.ts +9 -2
- package/dist/src/rtc/videoLayers.d.ts +31 -4
- package/dist/src/types.d.ts +30 -2
- package/package.json +1 -1
- package/src/Call.ts +21 -38
- package/src/compatibility.ts +7 -0
- package/src/devices/CameraManager.ts +18 -47
- package/src/devices/ScreenShareManager.ts +1 -3
- package/src/devices/__tests__/CameraManager.test.ts +7 -15
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
- package/src/events/callEventHandlers.ts +0 -2
- package/src/events/internal.ts +0 -16
- package/src/gen/video/sfu/event/events.ts +8 -120
- package/src/helpers/sdp-munging.ts +38 -15
- package/src/rtc/Publisher.ts +211 -317
- package/src/rtc/__tests__/Publisher.test.ts +196 -7
- package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
- package/src/rtc/__tests__/videoLayers.test.ts +51 -36
- package/src/rtc/bitrateLookup.ts +61 -0
- package/src/rtc/codecs.ts +56 -9
- package/src/rtc/videoLayers.ts +74 -23
- 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.
|
|
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
|
|
3135
|
+
* Returns the optimal video codec for the device.
|
|
3186
3136
|
*/
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
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 (
|
|
3148
|
+
if (isFirefox())
|
|
3195
3149
|
return 'vp8';
|
|
3196
|
-
return
|
|
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
|
|
3327
|
-
const {
|
|
3328
|
-
const maxBitrate = getComputedMaxBitrate(targetResolution,
|
|
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
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
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
|
|
3339
|
-
height
|
|
3340
|
-
maxBitrate
|
|
3341
|
-
scaleResolutionDownBy: downscaleFactor,
|
|
3399
|
+
width,
|
|
3400
|
+
height,
|
|
3401
|
+
maxBitrate,
|
|
3342
3402
|
maxFramerate: 30,
|
|
3343
|
-
}
|
|
3344
|
-
|
|
3345
|
-
|
|
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
|
|
3438
|
+
* @param codecInUse the codec in use.
|
|
3439
|
+
* @param publishOptions the publish options.
|
|
3362
3440
|
*/
|
|
3363
|
-
const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight,
|
|
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
|
|
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
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
}
|
|
5044
|
-
|
|
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.
|
|
5108
|
-
|
|
5109
|
-
|
|
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
|
-
|
|
5155
|
-
|
|
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
|
-
|
|
5196
|
-
|
|
5197
|
-
.
|
|
5198
|
-
|
|
5199
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
5399
|
+
this.changePublishQuality = async (enabledLayers) => {
|
|
5341
5400
|
this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
|
|
5342
|
-
const videoSender = this.
|
|
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
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
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
|
|
5359
|
-
if (
|
|
5360
|
-
|
|
5421
|
+
const shouldActivate = !!layer?.active;
|
|
5422
|
+
if (shouldActivate !== encoder.active) {
|
|
5423
|
+
encoder.active = shouldActivate;
|
|
5361
5424
|
changed = true;
|
|
5362
5425
|
}
|
|
5363
|
-
if
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
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
|
-
|
|
5392
|
-
|
|
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
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
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.
|
|
5559
|
+
const transceiver = this.transceiverCache.get(TrackType.SCREEN_SHARE_AUDIO);
|
|
5497
5560
|
if (!transceiver)
|
|
5498
5561
|
return sdp;
|
|
5499
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
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
|
|
8590
|
-
|
|
8591
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
12476
|
+
const version = "1.9.0";
|
|
12492
12477
|
return (this.userAgent ||
|
|
12493
12478
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
12494
12479
|
};
|