@stream-io/video-client 1.50.0 → 1.51.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 +11 -0
- package/dist/index.browser.es.js +288 -58
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +288 -58
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +288 -58
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +1 -0
- package/dist/src/devices/CameraManager.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +20 -0
- package/dist/src/devices/VirtualDevice.d.ts +59 -0
- package/dist/src/devices/devicePersistence.d.ts +1 -1
- package/dist/src/devices/index.d.ts +1 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +7 -2
- package/dist/src/rtc/Publisher.d.ts +21 -3
- package/dist/src/rtc/TransceiverCache.d.ts +5 -1
- package/dist/src/rtc/helpers/degradationPreference.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/Call.ts +22 -11
- package/src/devices/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +148 -8
- package/src/devices/DeviceManagerState.ts +4 -1
- package/src/devices/VirtualDevice.ts +69 -0
- package/src/devices/__tests__/CameraManager.test.ts +19 -0
- package/src/devices/__tests__/DeviceManager.test.ts +121 -1
- package/src/devices/devicePersistence.ts +2 -1
- package/src/devices/index.ts +1 -0
- package/src/rtc/BasePeerConnection.ts +15 -3
- package/src/rtc/Publisher.ts +140 -41
- package/src/rtc/TransceiverCache.ts +10 -3
- package/src/rtc/__tests__/Call.reconnect.test.ts +121 -2
- package/src/rtc/__tests__/Publisher.test.ts +659 -112
- package/src/rtc/__tests__/Subscriber.test.ts +2 -2
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +33 -1
- package/src/rtc/helpers/degradationPreference.ts +18 -0
- package/src/rtc/types.ts +2 -0
package/dist/index.cjs.js
CHANGED
|
@@ -6379,7 +6379,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6379
6379
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6380
6380
|
};
|
|
6381
6381
|
|
|
6382
|
-
const version = "1.
|
|
6382
|
+
const version = "1.51.0";
|
|
6383
6383
|
const [major, minor, patch] = version.split('.');
|
|
6384
6384
|
let sdkInfo = {
|
|
6385
6385
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7498,7 +7498,7 @@ class BasePeerConnection {
|
|
|
7498
7498
|
this.on = (event, fn) => {
|
|
7499
7499
|
const getTag = () => this.tag;
|
|
7500
7500
|
this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
|
|
7501
|
-
const lockKey =
|
|
7501
|
+
const lockKey = this.eventLockKey(event);
|
|
7502
7502
|
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
7503
7503
|
if (this.isDisposed)
|
|
7504
7504
|
return;
|
|
@@ -7506,6 +7506,13 @@ class BasePeerConnection {
|
|
|
7506
7506
|
});
|
|
7507
7507
|
}));
|
|
7508
7508
|
};
|
|
7509
|
+
/**
|
|
7510
|
+
* Returns the per-event `withoutConcurrency` tag used to serialize the
|
|
7511
|
+
* dispatcher handler for `event` on this peer connection.
|
|
7512
|
+
*/
|
|
7513
|
+
this.eventLockKey = (event) => {
|
|
7514
|
+
return `pc.${this.lock}.${event}`;
|
|
7515
|
+
};
|
|
7509
7516
|
/**
|
|
7510
7517
|
* Appends the trickled ICE candidates to the `RTCPeerConnection`.
|
|
7511
7518
|
*/
|
|
@@ -7759,7 +7766,7 @@ class BasePeerConnection {
|
|
|
7759
7766
|
/**
|
|
7760
7767
|
* Disposes the `RTCPeerConnection` instance.
|
|
7761
7768
|
*/
|
|
7762
|
-
dispose() {
|
|
7769
|
+
async dispose() {
|
|
7763
7770
|
clearTimeout(this.iceRestartTimeout);
|
|
7764
7771
|
this.iceRestartTimeout = undefined;
|
|
7765
7772
|
clearTimeout(this.preConnectStuckTimeout);
|
|
@@ -7781,6 +7788,7 @@ class BasePeerConnection {
|
|
|
7781
7788
|
pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
7782
7789
|
pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
7783
7790
|
pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
7791
|
+
pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
7784
7792
|
this.unsubscribeIceTrickle?.();
|
|
7785
7793
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
7786
7794
|
this.subscriptions = [];
|
|
@@ -7808,8 +7816,14 @@ class TransceiverCache {
|
|
|
7808
7816
|
* Gets the transceiver for the given publish option.
|
|
7809
7817
|
*/
|
|
7810
7818
|
this.get = (publishOption) => {
|
|
7811
|
-
return this.
|
|
7812
|
-
|
|
7819
|
+
return this.getBy(publishOption.id, publishOption.trackType);
|
|
7820
|
+
};
|
|
7821
|
+
/**
|
|
7822
|
+
* Gets the transceiver for the given publish option id and track type.
|
|
7823
|
+
*/
|
|
7824
|
+
this.getBy = (publishOptionId, trackType) => {
|
|
7825
|
+
return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
|
|
7826
|
+
bundle.publishOption.trackType === trackType);
|
|
7813
7827
|
};
|
|
7814
7828
|
/**
|
|
7815
7829
|
* Updates the cached bundle with the given patch.
|
|
@@ -8094,6 +8108,21 @@ const toRTCDegradationPreference = (preference) => {
|
|
|
8094
8108
|
ensureExhausted(preference, 'Unknown degradation preference');
|
|
8095
8109
|
}
|
|
8096
8110
|
};
|
|
8111
|
+
const fromRTCDegradationPreference = (preference) => {
|
|
8112
|
+
switch (preference) {
|
|
8113
|
+
case 'balanced':
|
|
8114
|
+
return DegradationPreference.BALANCED;
|
|
8115
|
+
case 'maintain-framerate':
|
|
8116
|
+
return DegradationPreference.MAINTAIN_FRAMERATE;
|
|
8117
|
+
case 'maintain-resolution':
|
|
8118
|
+
return DegradationPreference.MAINTAIN_RESOLUTION;
|
|
8119
|
+
// @ts-expect-error not in the typedefs yet
|
|
8120
|
+
case 'maintain-framerate-and-resolution':
|
|
8121
|
+
return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
|
|
8122
|
+
default:
|
|
8123
|
+
return DegradationPreference.UNSPECIFIED;
|
|
8124
|
+
}
|
|
8125
|
+
};
|
|
8097
8126
|
|
|
8098
8127
|
/**
|
|
8099
8128
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
@@ -8128,13 +8157,13 @@ class Publisher extends BasePeerConnection {
|
|
|
8128
8157
|
// create a clone of the track as otherwise the same trackId will
|
|
8129
8158
|
// appear in the SDP in multiple transceivers
|
|
8130
8159
|
const trackToPublish = this.cloneTrack(track);
|
|
8131
|
-
const
|
|
8132
|
-
if (!
|
|
8160
|
+
const bundle = this.transceiverCache.get(publishOption);
|
|
8161
|
+
if (!bundle) {
|
|
8133
8162
|
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8134
8163
|
}
|
|
8135
8164
|
else {
|
|
8136
|
-
const previousTrack = transceiver.sender.track;
|
|
8137
|
-
await this.updateTransceiver(
|
|
8165
|
+
const previousTrack = bundle.transceiver.sender.track;
|
|
8166
|
+
await this.updateTransceiver(bundle, trackToPublish, options);
|
|
8138
8167
|
if (!isReactNative()) {
|
|
8139
8168
|
this.stopTrack(previousTrack);
|
|
8140
8169
|
}
|
|
@@ -8169,13 +8198,20 @@ class Publisher extends BasePeerConnection {
|
|
|
8169
8198
|
/**
|
|
8170
8199
|
* Updates the transceiver with the given track and track type.
|
|
8171
8200
|
*/
|
|
8172
|
-
this.updateTransceiver = async (
|
|
8201
|
+
this.updateTransceiver = async (bundle, track, options = {}) => {
|
|
8202
|
+
const { transceiver, publishOption } = bundle;
|
|
8203
|
+
const trackType = publishOption.trackType;
|
|
8173
8204
|
const sender = transceiver.sender;
|
|
8174
8205
|
if (sender.track)
|
|
8175
8206
|
this.trackIdToTrackType.delete(sender.track.id);
|
|
8176
8207
|
await sender.replaceTrack(track);
|
|
8177
|
-
if (track)
|
|
8208
|
+
if (track) {
|
|
8178
8209
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
8210
|
+
if (isFirefox() && bundle.videoSender) {
|
|
8211
|
+
// restore the encoding config from the cache, if any
|
|
8212
|
+
await this.changePublishQuality(bundle.videoSender, bundle);
|
|
8213
|
+
}
|
|
8214
|
+
}
|
|
8179
8215
|
if (isAudioTrackType(trackType)) {
|
|
8180
8216
|
await this.updateAudioPublishOptions(trackType, options);
|
|
8181
8217
|
}
|
|
@@ -8235,7 +8271,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8235
8271
|
continue;
|
|
8236
8272
|
// it is safe to stop the track here, it is a clone
|
|
8237
8273
|
this.stopTrack(transceiver.sender.track);
|
|
8238
|
-
await this.updateTransceiver(
|
|
8274
|
+
await this.updateTransceiver(item, null);
|
|
8239
8275
|
}
|
|
8240
8276
|
};
|
|
8241
8277
|
/**
|
|
@@ -8292,33 +8328,38 @@ class Publisher extends BasePeerConnection {
|
|
|
8292
8328
|
/**
|
|
8293
8329
|
* Stops the cloned track that is being published to the SFU.
|
|
8294
8330
|
*/
|
|
8295
|
-
this.stopTracks = (...trackTypes) => {
|
|
8296
|
-
|
|
8297
|
-
const
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8331
|
+
this.stopTracks = async (...trackTypes) => {
|
|
8332
|
+
return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
|
|
8333
|
+
for (const item of this.transceiverCache.items()) {
|
|
8334
|
+
const { publishOption, transceiver } = item;
|
|
8335
|
+
if (!trackTypes.includes(publishOption.trackType))
|
|
8336
|
+
continue;
|
|
8337
|
+
const track = transceiver.sender.track;
|
|
8338
|
+
await this.silenceSenderOnFirefox(item);
|
|
8339
|
+
this.stopTrack(track);
|
|
8340
|
+
}
|
|
8341
|
+
});
|
|
8302
8342
|
};
|
|
8303
8343
|
/**
|
|
8304
8344
|
* Stops all the cloned tracks that are being published to the SFU.
|
|
8305
8345
|
*/
|
|
8306
|
-
this.stopAllTracks = () => {
|
|
8307
|
-
|
|
8308
|
-
this.
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8346
|
+
this.stopAllTracks = async () => {
|
|
8347
|
+
return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
|
|
8348
|
+
for (const item of this.transceiverCache.items()) {
|
|
8349
|
+
const track = item.transceiver.sender.track;
|
|
8350
|
+
await this.silenceSenderOnFirefox(item);
|
|
8351
|
+
this.stopTrack(track);
|
|
8352
|
+
}
|
|
8353
|
+
for (const track of this.clonedTracks) {
|
|
8354
|
+
this.stopTrack(track);
|
|
8355
|
+
}
|
|
8356
|
+
});
|
|
8313
8357
|
};
|
|
8314
|
-
this.changePublishQuality = async (videoSender) => {
|
|
8315
|
-
const
|
|
8316
|
-
const enabledLayers = layers.filter((l) => l.active);
|
|
8358
|
+
this.changePublishQuality = async (videoSender, bundle) => {
|
|
8359
|
+
const enabledLayers = videoSender.layers.filter((l) => l.active);
|
|
8317
8360
|
const tag = 'Update publish quality:';
|
|
8318
8361
|
this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
|
|
8319
|
-
const
|
|
8320
|
-
t.publishOption.trackType === trackType);
|
|
8321
|
-
const sender = transceiverId?.transceiver.sender;
|
|
8362
|
+
const sender = bundle?.transceiver.sender;
|
|
8322
8363
|
if (!sender) {
|
|
8323
8364
|
return this.logger.warn(`${tag} no video sender found.`);
|
|
8324
8365
|
}
|
|
@@ -8326,7 +8367,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8326
8367
|
if (params.encodings.length === 0) {
|
|
8327
8368
|
return this.logger.warn(`${tag} there are no encodings set.`);
|
|
8328
8369
|
}
|
|
8329
|
-
const codecInUse =
|
|
8370
|
+
const codecInUse = bundle?.publishOption.codec?.name;
|
|
8330
8371
|
const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
|
|
8331
8372
|
let changed = false;
|
|
8332
8373
|
for (const encoder of params.encodings) {
|
|
@@ -8526,6 +8567,72 @@ class Publisher extends BasePeerConnection {
|
|
|
8526
8567
|
track.stop();
|
|
8527
8568
|
this.clonedTracks.delete(track);
|
|
8528
8569
|
};
|
|
8570
|
+
/**
|
|
8571
|
+
* Silences a Firefox sender on the wire during unpublish.
|
|
8572
|
+
*
|
|
8573
|
+
* Firefox keeps emitting RTP after track.stop(), but the right lever
|
|
8574
|
+
* differs by track type:
|
|
8575
|
+
* - audio: `replaceTrack(null)` is the only reliable silencer;
|
|
8576
|
+
* `setParameters({encodings:[...active:false]})` does NOT stop
|
|
8577
|
+
* the Opus encoder.
|
|
8578
|
+
* - video: `setParameters({encodings:[...active:false]})` pauses
|
|
8579
|
+
* the encoder; `replaceTrack(null)` does NOT reliably stop the
|
|
8580
|
+
* video encoder. The prior active=true configuration is captured
|
|
8581
|
+
* onto `bundle.videoSender` so `updateTransceiver` can restore
|
|
8582
|
+
* it on the next publish.
|
|
8583
|
+
*
|
|
8584
|
+
* No-op on non-Firefox browsers and during teardown.
|
|
8585
|
+
*/
|
|
8586
|
+
this.silenceSenderOnFirefox = async (bundle) => {
|
|
8587
|
+
if (this.isDisposed || !isFirefox())
|
|
8588
|
+
return;
|
|
8589
|
+
const { transceiver, publishOption } = bundle;
|
|
8590
|
+
if (isAudioTrackType(publishOption.trackType)) {
|
|
8591
|
+
await transceiver.sender.replaceTrack(null).catch((err) => {
|
|
8592
|
+
this.logger.warn('Failed to clear audio sender track', err);
|
|
8593
|
+
});
|
|
8594
|
+
return;
|
|
8595
|
+
}
|
|
8596
|
+
await this.disableAllEncodings(bundle);
|
|
8597
|
+
};
|
|
8598
|
+
this.disableAllEncodings = async (bundle) => {
|
|
8599
|
+
const { transceiver, publishOption } = bundle;
|
|
8600
|
+
const sender = transceiver.sender;
|
|
8601
|
+
const params = sender.getParameters();
|
|
8602
|
+
if (!params.encodings || params.encodings.length === 0)
|
|
8603
|
+
return;
|
|
8604
|
+
if (!bundle.videoSender) {
|
|
8605
|
+
this.transceiverCache.update(publishOption, {
|
|
8606
|
+
videoSender: {
|
|
8607
|
+
trackType: publishOption.trackType,
|
|
8608
|
+
publishOptionId: publishOption.id,
|
|
8609
|
+
codec: publishOption.codec,
|
|
8610
|
+
degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
|
|
8611
|
+
layers: params.encodings.map((e) => ({
|
|
8612
|
+
name: e.rid ?? 'q',
|
|
8613
|
+
active: e.active ?? true,
|
|
8614
|
+
maxBitrate: e.maxBitrate ?? 0,
|
|
8615
|
+
scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
|
|
8616
|
+
maxFramerate: e.maxFramerate ?? 0,
|
|
8617
|
+
// @ts-expect-error scalabilityMode is not in the typedefs yet
|
|
8618
|
+
scalabilityMode: e.scalabilityMode ?? '',
|
|
8619
|
+
})),
|
|
8620
|
+
},
|
|
8621
|
+
});
|
|
8622
|
+
}
|
|
8623
|
+
let changed = false;
|
|
8624
|
+
for (const encoding of params.encodings) {
|
|
8625
|
+
if (encoding.active !== false) {
|
|
8626
|
+
encoding.active = false;
|
|
8627
|
+
changed = true;
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
if (!changed)
|
|
8631
|
+
return;
|
|
8632
|
+
await sender.setParameters(params).catch((err) => {
|
|
8633
|
+
this.logger.error('Failed to disable video sender encodings:', err);
|
|
8634
|
+
});
|
|
8635
|
+
};
|
|
8529
8636
|
this.publishOptions = publishOptions;
|
|
8530
8637
|
this.on('iceRestart', (iceRestart) => {
|
|
8531
8638
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
@@ -8534,7 +8641,16 @@ class Publisher extends BasePeerConnection {
|
|
|
8534
8641
|
});
|
|
8535
8642
|
this.on('changePublishQuality', async (event) => {
|
|
8536
8643
|
for (const videoSender of event.videoSenders) {
|
|
8537
|
-
|
|
8644
|
+
// if not publishing, update the encodingConfigCache and don't modify the state.
|
|
8645
|
+
// we'll apply this config on the next publish/unmute.
|
|
8646
|
+
const { trackType, publishOptionId } = videoSender;
|
|
8647
|
+
const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
|
|
8648
|
+
if (bundle) {
|
|
8649
|
+
this.transceiverCache.update(bundle.publishOption, { videoSender });
|
|
8650
|
+
}
|
|
8651
|
+
if (isFirefox() && !this.isPublishing(trackType))
|
|
8652
|
+
continue;
|
|
8653
|
+
await this.changePublishQuality(videoSender, bundle);
|
|
8538
8654
|
}
|
|
8539
8655
|
});
|
|
8540
8656
|
this.on('changePublishOptions', (event) => {
|
|
@@ -8545,9 +8661,14 @@ class Publisher extends BasePeerConnection {
|
|
|
8545
8661
|
/**
|
|
8546
8662
|
* Disposes this Publisher instance.
|
|
8547
8663
|
*/
|
|
8548
|
-
dispose() {
|
|
8549
|
-
super.dispose();
|
|
8550
|
-
|
|
8664
|
+
async dispose() {
|
|
8665
|
+
await super.dispose();
|
|
8666
|
+
try {
|
|
8667
|
+
await this.stopAllTracks();
|
|
8668
|
+
}
|
|
8669
|
+
catch (err) {
|
|
8670
|
+
this.logger.warn('Failed to stop tracks during dispose', err);
|
|
8671
|
+
}
|
|
8551
8672
|
this.clonedTracks.clear();
|
|
8552
8673
|
}
|
|
8553
8674
|
}
|
|
@@ -11331,8 +11452,8 @@ const normalize = (options) => {
|
|
|
11331
11452
|
: false,
|
|
11332
11453
|
};
|
|
11333
11454
|
};
|
|
11334
|
-
const createSyntheticDevice = (deviceId, kind) => {
|
|
11335
|
-
return { deviceId, kind, label
|
|
11455
|
+
const createSyntheticDevice = (deviceId, kind, label = '') => {
|
|
11456
|
+
return { deviceId, kind, label, groupId: '' };
|
|
11336
11457
|
};
|
|
11337
11458
|
const readPreferences = (storageKey) => {
|
|
11338
11459
|
try {
|
|
@@ -11386,6 +11507,8 @@ class DeviceManager {
|
|
|
11386
11507
|
this.areSubscriptionsSetUp = false;
|
|
11387
11508
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
11388
11509
|
this.filters = [];
|
|
11510
|
+
this.virtualDevicesSubject = new rxjs.BehaviorSubject([]);
|
|
11511
|
+
this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
|
|
11389
11512
|
this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
|
|
11390
11513
|
this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
|
|
11391
11514
|
/**
|
|
@@ -11398,6 +11521,7 @@ class DeviceManager {
|
|
|
11398
11521
|
this.subscriptions.forEach((s) => s());
|
|
11399
11522
|
this.subscriptions = [];
|
|
11400
11523
|
this.areSubscriptionsSetUp = false;
|
|
11524
|
+
this.virtualDevicesSubject.next([]);
|
|
11401
11525
|
};
|
|
11402
11526
|
this.runCurrentStreamCleanups = () => {
|
|
11403
11527
|
this.currentStreamCleanups.forEach((c) => c());
|
|
@@ -11456,7 +11580,93 @@ class DeviceManager {
|
|
|
11456
11580
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
11457
11581
|
*/
|
|
11458
11582
|
listDevices() {
|
|
11459
|
-
return this.getDevices()
|
|
11583
|
+
return rxjs.combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(rxjs.map(([real, virtual]) => [
|
|
11584
|
+
...real,
|
|
11585
|
+
...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
|
|
11586
|
+
]));
|
|
11587
|
+
}
|
|
11588
|
+
/**
|
|
11589
|
+
* Registers a virtual camera or microphone backed by a caller-supplied
|
|
11590
|
+
* stream factory. The device appears in `listDevices()` and can be selected
|
|
11591
|
+
* via `select()` like any real device.
|
|
11592
|
+
*
|
|
11593
|
+
* Web only. React Native is not supported.
|
|
11594
|
+
*
|
|
11595
|
+
* Only supported for camera and microphone managers; calling on any other
|
|
11596
|
+
* manager throws.
|
|
11597
|
+
*/
|
|
11598
|
+
registerVirtualDevice(virtualDevice) {
|
|
11599
|
+
if (isReactNative()) {
|
|
11600
|
+
throw new Error('Virtual devices are not supported on React Native.');
|
|
11601
|
+
}
|
|
11602
|
+
if (this.trackType !== TrackType.AUDIO &&
|
|
11603
|
+
this.trackType !== TrackType.VIDEO) {
|
|
11604
|
+
throw new Error('Virtual devices are only supported for camera and microphone.');
|
|
11605
|
+
}
|
|
11606
|
+
const deviceId = `stream-virtual:${generateUUIDv4()}`;
|
|
11607
|
+
const entry = {
|
|
11608
|
+
deviceId,
|
|
11609
|
+
kind: this.mediaDeviceKind,
|
|
11610
|
+
...virtualDevice,
|
|
11611
|
+
};
|
|
11612
|
+
setCurrentValue(this.virtualDevicesSubject, (current) => [
|
|
11613
|
+
...current,
|
|
11614
|
+
entry,
|
|
11615
|
+
]);
|
|
11616
|
+
return {
|
|
11617
|
+
deviceId: entry.deviceId,
|
|
11618
|
+
unregister: async () => {
|
|
11619
|
+
await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
|
|
11620
|
+
setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
|
|
11621
|
+
if (this.activeVirtualSession?.deviceId === deviceId) {
|
|
11622
|
+
await this.stopActiveVirtualSession();
|
|
11623
|
+
}
|
|
11624
|
+
});
|
|
11625
|
+
if (this.state.selectedDevice === deviceId) {
|
|
11626
|
+
await this.statusChangeSettled();
|
|
11627
|
+
await this.disable({ forceStop: true });
|
|
11628
|
+
await this.select(undefined);
|
|
11629
|
+
}
|
|
11630
|
+
},
|
|
11631
|
+
};
|
|
11632
|
+
}
|
|
11633
|
+
sanitizeVirtualStream(stream) {
|
|
11634
|
+
stream.getTracks().forEach((track) => {
|
|
11635
|
+
const originalGetSettings = track.getSettings.bind(track);
|
|
11636
|
+
track.getSettings = () => {
|
|
11637
|
+
const settings = originalGetSettings();
|
|
11638
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11639
|
+
const { deviceId, ...rest } = settings;
|
|
11640
|
+
return rest;
|
|
11641
|
+
};
|
|
11642
|
+
});
|
|
11643
|
+
return stream;
|
|
11644
|
+
}
|
|
11645
|
+
findVirtualDevice(deviceId) {
|
|
11646
|
+
if (!deviceId)
|
|
11647
|
+
return undefined;
|
|
11648
|
+
return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
|
|
11649
|
+
}
|
|
11650
|
+
async stopActiveVirtualSession() {
|
|
11651
|
+
const session = this.activeVirtualSession;
|
|
11652
|
+
this.activeVirtualSession = undefined;
|
|
11653
|
+
await session?.stop?.();
|
|
11654
|
+
}
|
|
11655
|
+
async getSelectedStream(constraints) {
|
|
11656
|
+
const deviceId = this.state.selectedDevice;
|
|
11657
|
+
if (!deviceId?.startsWith('stream-virtual')) {
|
|
11658
|
+
return this.getStream(constraints);
|
|
11659
|
+
}
|
|
11660
|
+
return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
|
|
11661
|
+
const virtualDevice = this.findVirtualDevice(deviceId);
|
|
11662
|
+
if (!virtualDevice) {
|
|
11663
|
+
throw new Error(`Virtual device is not registered: ${deviceId}`);
|
|
11664
|
+
}
|
|
11665
|
+
await this.stopActiveVirtualSession();
|
|
11666
|
+
const { stream, stop } = await virtualDevice.getUserMedia(constraints);
|
|
11667
|
+
this.activeVirtualSession = { deviceId, stop };
|
|
11668
|
+
return this.sanitizeVirtualStream(stream);
|
|
11669
|
+
});
|
|
11460
11670
|
}
|
|
11461
11671
|
/**
|
|
11462
11672
|
* Returns `true` when this device is in enabled state.
|
|
@@ -11616,6 +11826,9 @@ class DeviceManager {
|
|
|
11616
11826
|
}
|
|
11617
11827
|
});
|
|
11618
11828
|
}
|
|
11829
|
+
getResolvedConstraints(constraints) {
|
|
11830
|
+
return constraints;
|
|
11831
|
+
}
|
|
11619
11832
|
publishStream(stream, options) {
|
|
11620
11833
|
return this.call.publish(stream, this.trackType, options);
|
|
11621
11834
|
}
|
|
@@ -11636,6 +11849,7 @@ class DeviceManager {
|
|
|
11636
11849
|
this.muteLocalStream(stopTracks);
|
|
11637
11850
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
11638
11851
|
if (allEnded) {
|
|
11852
|
+
await this.stopActiveVirtualSession();
|
|
11639
11853
|
// @ts-expect-error release() is present in react-native-webrtc
|
|
11640
11854
|
if (typeof mediaStream.release === 'function') {
|
|
11641
11855
|
// @ts-expect-error called to dispose the stream in RN
|
|
@@ -11691,12 +11905,12 @@ class DeviceManager {
|
|
|
11691
11905
|
// before chainWith below registers new ones for the new chain.
|
|
11692
11906
|
this.runCurrentStreamCleanups();
|
|
11693
11907
|
const defaultConstraints = this.state.defaultConstraints;
|
|
11694
|
-
const constraints = {
|
|
11908
|
+
const constraints = this.getResolvedConstraints({
|
|
11695
11909
|
...defaultConstraints,
|
|
11696
11910
|
deviceId: this.state.selectedDevice
|
|
11697
11911
|
? { exact: this.state.selectedDevice }
|
|
11698
11912
|
: undefined,
|
|
11699
|
-
};
|
|
11913
|
+
});
|
|
11700
11914
|
/**
|
|
11701
11915
|
* Chains two media streams together.
|
|
11702
11916
|
*
|
|
@@ -11753,7 +11967,7 @@ class DeviceManager {
|
|
|
11753
11967
|
};
|
|
11754
11968
|
// the rootStream represents the stream coming from the actual device
|
|
11755
11969
|
// e.g. camera or microphone stream
|
|
11756
|
-
rootStreamPromise = this.
|
|
11970
|
+
rootStreamPromise = this.getSelectedStream(constraints);
|
|
11757
11971
|
// we publish the last MediaStream of the chain
|
|
11758
11972
|
stream = await this.filters.reduce((parent, entry) => parent
|
|
11759
11973
|
.then((inputStream) => {
|
|
@@ -12070,7 +12284,10 @@ class DeviceManagerState {
|
|
|
12070
12284
|
setCurrentValue(this.mediaStreamSubject, stream);
|
|
12071
12285
|
setCurrentValue(this.rootMediaStreamSubject, rootStream);
|
|
12072
12286
|
if (rootStream) {
|
|
12073
|
-
this.
|
|
12287
|
+
const derived = this.getDeviceIdFromStream(rootStream);
|
|
12288
|
+
if (derived) {
|
|
12289
|
+
this.setDevice(derived);
|
|
12290
|
+
}
|
|
12074
12291
|
}
|
|
12075
12292
|
}
|
|
12076
12293
|
/**
|
|
@@ -12283,7 +12500,7 @@ class CameraManager extends DeviceManager {
|
|
|
12283
12500
|
getDevices() {
|
|
12284
12501
|
return getVideoDevices(this.call.tracer);
|
|
12285
12502
|
}
|
|
12286
|
-
|
|
12503
|
+
getResolvedConstraints(constraints) {
|
|
12287
12504
|
constraints.width = this.targetResolution.width;
|
|
12288
12505
|
constraints.height = this.targetResolution.height;
|
|
12289
12506
|
// We can't set both device id and facing mode
|
|
@@ -12294,6 +12511,9 @@ class CameraManager extends DeviceManager {
|
|
|
12294
12511
|
constraints.facingMode =
|
|
12295
12512
|
this.state.direction === 'front' ? 'user' : 'environment';
|
|
12296
12513
|
}
|
|
12514
|
+
return constraints;
|
|
12515
|
+
}
|
|
12516
|
+
getStream(constraints) {
|
|
12297
12517
|
return getVideoStream(constraints, this.call.tracer);
|
|
12298
12518
|
}
|
|
12299
12519
|
}
|
|
@@ -13621,9 +13841,10 @@ class Call {
|
|
|
13621
13841
|
this.sfuStatsReporter?.flush();
|
|
13622
13842
|
this.sfuStatsReporter?.stop();
|
|
13623
13843
|
this.sfuStatsReporter = undefined;
|
|
13624
|
-
this.
|
|
13844
|
+
this.lastStatsOptions = undefined;
|
|
13845
|
+
await this.subscriber?.dispose();
|
|
13625
13846
|
this.subscriber = undefined;
|
|
13626
|
-
this.publisher?.dispose();
|
|
13847
|
+
await this.publisher?.dispose();
|
|
13627
13848
|
this.publisher = undefined;
|
|
13628
13849
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
13629
13850
|
this.sfuClient = undefined;
|
|
@@ -13899,15 +14120,17 @@ class Call {
|
|
|
13899
14120
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
13900
14121
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
13901
14122
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
13902
|
-
let statsOptions = this.
|
|
14123
|
+
let statsOptions = this.lastStatsOptions;
|
|
13903
14124
|
if (!this.credentials ||
|
|
13904
14125
|
!statsOptions ||
|
|
13905
14126
|
performingRejoin ||
|
|
13906
|
-
performingMigration
|
|
14127
|
+
performingMigration ||
|
|
14128
|
+
data?.migrating_from) {
|
|
13907
14129
|
try {
|
|
13908
14130
|
const joinResponse = await this.doJoinRequest(data);
|
|
13909
14131
|
this.credentials = joinResponse.credentials;
|
|
13910
14132
|
statsOptions = joinResponse.stats_options;
|
|
14133
|
+
this.lastStatsOptions = statsOptions;
|
|
13911
14134
|
}
|
|
13912
14135
|
catch (error) {
|
|
13913
14136
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
@@ -14014,7 +14237,7 @@ class Call {
|
|
|
14014
14237
|
}
|
|
14015
14238
|
else {
|
|
14016
14239
|
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
14017
|
-
this.initPublisherAndSubscriber({
|
|
14240
|
+
await this.initPublisherAndSubscriber({
|
|
14018
14241
|
sfuClient,
|
|
14019
14242
|
connectionConfig,
|
|
14020
14243
|
clientDetails,
|
|
@@ -14159,11 +14382,11 @@ class Call {
|
|
|
14159
14382
|
* Initializes the Publisher and Subscriber Peer Connections.
|
|
14160
14383
|
* @internal
|
|
14161
14384
|
*/
|
|
14162
|
-
this.initPublisherAndSubscriber = (opts) => {
|
|
14385
|
+
this.initPublisherAndSubscriber = async (opts) => {
|
|
14163
14386
|
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
|
|
14164
14387
|
const { enable_rtc_stats: enableTracing } = statsOptions;
|
|
14165
14388
|
if (closePreviousInstances && this.subscriber) {
|
|
14166
|
-
this.subscriber.dispose();
|
|
14389
|
+
await this.subscriber.dispose();
|
|
14167
14390
|
}
|
|
14168
14391
|
const basePeerConnectionOptions = {
|
|
14169
14392
|
sfuClient,
|
|
@@ -14192,7 +14415,7 @@ class Call {
|
|
|
14192
14415
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
14193
14416
|
if (!isAnonymous) {
|
|
14194
14417
|
if (closePreviousInstances && this.publisher) {
|
|
14195
|
-
this.publisher.dispose();
|
|
14418
|
+
await this.publisher.dispose();
|
|
14196
14419
|
}
|
|
14197
14420
|
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14198
14421
|
}
|
|
@@ -14295,10 +14518,17 @@ class Call {
|
|
|
14295
14518
|
* `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
|
|
14296
14519
|
*/
|
|
14297
14520
|
this.reconnect = async (strategy, reason) => {
|
|
14298
|
-
if (this.state.callingState === exports.CallingState.
|
|
14521
|
+
if (this.state.callingState === exports.CallingState.JOINING ||
|
|
14522
|
+
this.state.callingState === exports.CallingState.RECONNECTING ||
|
|
14299
14523
|
this.state.callingState === exports.CallingState.MIGRATING ||
|
|
14300
14524
|
this.state.callingState === exports.CallingState.RECONNECTING_FAILED)
|
|
14301
14525
|
return;
|
|
14526
|
+
// Drop redundant reconnect calls. If a reconnect is already queued or
|
|
14527
|
+
// running for this Call, that entry will resolve whatever broke;
|
|
14528
|
+
// queueing more entries just replays the full REJOIN cycle (one extra
|
|
14529
|
+
// `POST /join` per entry) once the call is already healthy again.
|
|
14530
|
+
if (hasPending(this.reconnectConcurrencyTag))
|
|
14531
|
+
return;
|
|
14302
14532
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
14303
14533
|
const reconnectStartTime = Date.now();
|
|
14304
14534
|
this.reconnectStrategy = strategy;
|
|
@@ -14503,8 +14733,8 @@ class Call {
|
|
|
14503
14733
|
this.state.setCallingState(exports.CallingState.JOINED);
|
|
14504
14734
|
}
|
|
14505
14735
|
finally {
|
|
14506
|
-
currentSubscriber?.dispose();
|
|
14507
|
-
currentPublisher?.dispose();
|
|
14736
|
+
await currentSubscriber?.dispose();
|
|
14737
|
+
await currentPublisher?.dispose();
|
|
14508
14738
|
// and close the previous SFU client, without specifying close code
|
|
14509
14739
|
currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
|
|
14510
14740
|
}
|
|
@@ -14693,7 +14923,7 @@ class Call {
|
|
|
14693
14923
|
this.stopPublish = async (...trackTypes) => {
|
|
14694
14924
|
if (!this.sfuClient || !this.publisher)
|
|
14695
14925
|
return;
|
|
14696
|
-
this.publisher.stopTracks(...trackTypes);
|
|
14926
|
+
await this.publisher.stopTracks(...trackTypes);
|
|
14697
14927
|
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
14698
14928
|
};
|
|
14699
14929
|
/**
|
|
@@ -16806,7 +17036,7 @@ class StreamClient {
|
|
|
16806
17036
|
this.getUserAgent = () => {
|
|
16807
17037
|
if (!this.cachedUserAgent) {
|
|
16808
17038
|
const { clientAppIdentifier = {} } = this.options;
|
|
16809
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17039
|
+
const { sdkName = 'js', sdkVersion = "1.51.0", ...extras } = clientAppIdentifier;
|
|
16810
17040
|
this.cachedUserAgent = [
|
|
16811
17041
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16812
17042
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|