@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.es.js
CHANGED
|
@@ -6360,7 +6360,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6360
6360
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6361
6361
|
};
|
|
6362
6362
|
|
|
6363
|
-
const version = "1.
|
|
6363
|
+
const version = "1.51.0";
|
|
6364
6364
|
const [major, minor, patch] = version.split('.');
|
|
6365
6365
|
let sdkInfo = {
|
|
6366
6366
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7479,7 +7479,7 @@ class BasePeerConnection {
|
|
|
7479
7479
|
this.on = (event, fn) => {
|
|
7480
7480
|
const getTag = () => this.tag;
|
|
7481
7481
|
this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
|
|
7482
|
-
const lockKey =
|
|
7482
|
+
const lockKey = this.eventLockKey(event);
|
|
7483
7483
|
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
7484
7484
|
if (this.isDisposed)
|
|
7485
7485
|
return;
|
|
@@ -7487,6 +7487,13 @@ class BasePeerConnection {
|
|
|
7487
7487
|
});
|
|
7488
7488
|
}));
|
|
7489
7489
|
};
|
|
7490
|
+
/**
|
|
7491
|
+
* Returns the per-event `withoutConcurrency` tag used to serialize the
|
|
7492
|
+
* dispatcher handler for `event` on this peer connection.
|
|
7493
|
+
*/
|
|
7494
|
+
this.eventLockKey = (event) => {
|
|
7495
|
+
return `pc.${this.lock}.${event}`;
|
|
7496
|
+
};
|
|
7490
7497
|
/**
|
|
7491
7498
|
* Appends the trickled ICE candidates to the `RTCPeerConnection`.
|
|
7492
7499
|
*/
|
|
@@ -7740,7 +7747,7 @@ class BasePeerConnection {
|
|
|
7740
7747
|
/**
|
|
7741
7748
|
* Disposes the `RTCPeerConnection` instance.
|
|
7742
7749
|
*/
|
|
7743
|
-
dispose() {
|
|
7750
|
+
async dispose() {
|
|
7744
7751
|
clearTimeout(this.iceRestartTimeout);
|
|
7745
7752
|
this.iceRestartTimeout = undefined;
|
|
7746
7753
|
clearTimeout(this.preConnectStuckTimeout);
|
|
@@ -7762,6 +7769,7 @@ class BasePeerConnection {
|
|
|
7762
7769
|
pc.removeEventListener('signalingstatechange', this.onSignalingChange);
|
|
7763
7770
|
pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
7764
7771
|
pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
|
|
7772
|
+
pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
|
|
7765
7773
|
this.unsubscribeIceTrickle?.();
|
|
7766
7774
|
this.subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
7767
7775
|
this.subscriptions = [];
|
|
@@ -7789,8 +7797,14 @@ class TransceiverCache {
|
|
|
7789
7797
|
* Gets the transceiver for the given publish option.
|
|
7790
7798
|
*/
|
|
7791
7799
|
this.get = (publishOption) => {
|
|
7792
|
-
return this.
|
|
7793
|
-
|
|
7800
|
+
return this.getBy(publishOption.id, publishOption.trackType);
|
|
7801
|
+
};
|
|
7802
|
+
/**
|
|
7803
|
+
* Gets the transceiver for the given publish option id and track type.
|
|
7804
|
+
*/
|
|
7805
|
+
this.getBy = (publishOptionId, trackType) => {
|
|
7806
|
+
return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
|
|
7807
|
+
bundle.publishOption.trackType === trackType);
|
|
7794
7808
|
};
|
|
7795
7809
|
/**
|
|
7796
7810
|
* Updates the cached bundle with the given patch.
|
|
@@ -8075,6 +8089,21 @@ const toRTCDegradationPreference = (preference) => {
|
|
|
8075
8089
|
ensureExhausted(preference, 'Unknown degradation preference');
|
|
8076
8090
|
}
|
|
8077
8091
|
};
|
|
8092
|
+
const fromRTCDegradationPreference = (preference) => {
|
|
8093
|
+
switch (preference) {
|
|
8094
|
+
case 'balanced':
|
|
8095
|
+
return DegradationPreference.BALANCED;
|
|
8096
|
+
case 'maintain-framerate':
|
|
8097
|
+
return DegradationPreference.MAINTAIN_FRAMERATE;
|
|
8098
|
+
case 'maintain-resolution':
|
|
8099
|
+
return DegradationPreference.MAINTAIN_RESOLUTION;
|
|
8100
|
+
// @ts-expect-error not in the typedefs yet
|
|
8101
|
+
case 'maintain-framerate-and-resolution':
|
|
8102
|
+
return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
|
|
8103
|
+
default:
|
|
8104
|
+
return DegradationPreference.UNSPECIFIED;
|
|
8105
|
+
}
|
|
8106
|
+
};
|
|
8078
8107
|
|
|
8079
8108
|
/**
|
|
8080
8109
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
@@ -8109,13 +8138,13 @@ class Publisher extends BasePeerConnection {
|
|
|
8109
8138
|
// create a clone of the track as otherwise the same trackId will
|
|
8110
8139
|
// appear in the SDP in multiple transceivers
|
|
8111
8140
|
const trackToPublish = this.cloneTrack(track);
|
|
8112
|
-
const
|
|
8113
|
-
if (!
|
|
8141
|
+
const bundle = this.transceiverCache.get(publishOption);
|
|
8142
|
+
if (!bundle) {
|
|
8114
8143
|
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8115
8144
|
}
|
|
8116
8145
|
else {
|
|
8117
|
-
const previousTrack = transceiver.sender.track;
|
|
8118
|
-
await this.updateTransceiver(
|
|
8146
|
+
const previousTrack = bundle.transceiver.sender.track;
|
|
8147
|
+
await this.updateTransceiver(bundle, trackToPublish, options);
|
|
8119
8148
|
if (!isReactNative()) {
|
|
8120
8149
|
this.stopTrack(previousTrack);
|
|
8121
8150
|
}
|
|
@@ -8150,13 +8179,20 @@ class Publisher extends BasePeerConnection {
|
|
|
8150
8179
|
/**
|
|
8151
8180
|
* Updates the transceiver with the given track and track type.
|
|
8152
8181
|
*/
|
|
8153
|
-
this.updateTransceiver = async (
|
|
8182
|
+
this.updateTransceiver = async (bundle, track, options = {}) => {
|
|
8183
|
+
const { transceiver, publishOption } = bundle;
|
|
8184
|
+
const trackType = publishOption.trackType;
|
|
8154
8185
|
const sender = transceiver.sender;
|
|
8155
8186
|
if (sender.track)
|
|
8156
8187
|
this.trackIdToTrackType.delete(sender.track.id);
|
|
8157
8188
|
await sender.replaceTrack(track);
|
|
8158
|
-
if (track)
|
|
8189
|
+
if (track) {
|
|
8159
8190
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
8191
|
+
if (isFirefox() && bundle.videoSender) {
|
|
8192
|
+
// restore the encoding config from the cache, if any
|
|
8193
|
+
await this.changePublishQuality(bundle.videoSender, bundle);
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8160
8196
|
if (isAudioTrackType(trackType)) {
|
|
8161
8197
|
await this.updateAudioPublishOptions(trackType, options);
|
|
8162
8198
|
}
|
|
@@ -8216,7 +8252,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8216
8252
|
continue;
|
|
8217
8253
|
// it is safe to stop the track here, it is a clone
|
|
8218
8254
|
this.stopTrack(transceiver.sender.track);
|
|
8219
|
-
await this.updateTransceiver(
|
|
8255
|
+
await this.updateTransceiver(item, null);
|
|
8220
8256
|
}
|
|
8221
8257
|
};
|
|
8222
8258
|
/**
|
|
@@ -8273,33 +8309,38 @@ class Publisher extends BasePeerConnection {
|
|
|
8273
8309
|
/**
|
|
8274
8310
|
* Stops the cloned track that is being published to the SFU.
|
|
8275
8311
|
*/
|
|
8276
|
-
this.stopTracks = (...trackTypes) => {
|
|
8277
|
-
|
|
8278
|
-
const
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8312
|
+
this.stopTracks = async (...trackTypes) => {
|
|
8313
|
+
return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
|
|
8314
|
+
for (const item of this.transceiverCache.items()) {
|
|
8315
|
+
const { publishOption, transceiver } = item;
|
|
8316
|
+
if (!trackTypes.includes(publishOption.trackType))
|
|
8317
|
+
continue;
|
|
8318
|
+
const track = transceiver.sender.track;
|
|
8319
|
+
await this.silenceSenderOnFirefox(item);
|
|
8320
|
+
this.stopTrack(track);
|
|
8321
|
+
}
|
|
8322
|
+
});
|
|
8283
8323
|
};
|
|
8284
8324
|
/**
|
|
8285
8325
|
* Stops all the cloned tracks that are being published to the SFU.
|
|
8286
8326
|
*/
|
|
8287
|
-
this.stopAllTracks = () => {
|
|
8288
|
-
|
|
8289
|
-
this.
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8327
|
+
this.stopAllTracks = async () => {
|
|
8328
|
+
return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
|
|
8329
|
+
for (const item of this.transceiverCache.items()) {
|
|
8330
|
+
const track = item.transceiver.sender.track;
|
|
8331
|
+
await this.silenceSenderOnFirefox(item);
|
|
8332
|
+
this.stopTrack(track);
|
|
8333
|
+
}
|
|
8334
|
+
for (const track of this.clonedTracks) {
|
|
8335
|
+
this.stopTrack(track);
|
|
8336
|
+
}
|
|
8337
|
+
});
|
|
8294
8338
|
};
|
|
8295
|
-
this.changePublishQuality = async (videoSender) => {
|
|
8296
|
-
const
|
|
8297
|
-
const enabledLayers = layers.filter((l) => l.active);
|
|
8339
|
+
this.changePublishQuality = async (videoSender, bundle) => {
|
|
8340
|
+
const enabledLayers = videoSender.layers.filter((l) => l.active);
|
|
8298
8341
|
const tag = 'Update publish quality:';
|
|
8299
8342
|
this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
|
|
8300
|
-
const
|
|
8301
|
-
t.publishOption.trackType === trackType);
|
|
8302
|
-
const sender = transceiverId?.transceiver.sender;
|
|
8343
|
+
const sender = bundle?.transceiver.sender;
|
|
8303
8344
|
if (!sender) {
|
|
8304
8345
|
return this.logger.warn(`${tag} no video sender found.`);
|
|
8305
8346
|
}
|
|
@@ -8307,7 +8348,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8307
8348
|
if (params.encodings.length === 0) {
|
|
8308
8349
|
return this.logger.warn(`${tag} there are no encodings set.`);
|
|
8309
8350
|
}
|
|
8310
|
-
const codecInUse =
|
|
8351
|
+
const codecInUse = bundle?.publishOption.codec?.name;
|
|
8311
8352
|
const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
|
|
8312
8353
|
let changed = false;
|
|
8313
8354
|
for (const encoder of params.encodings) {
|
|
@@ -8507,6 +8548,72 @@ class Publisher extends BasePeerConnection {
|
|
|
8507
8548
|
track.stop();
|
|
8508
8549
|
this.clonedTracks.delete(track);
|
|
8509
8550
|
};
|
|
8551
|
+
/**
|
|
8552
|
+
* Silences a Firefox sender on the wire during unpublish.
|
|
8553
|
+
*
|
|
8554
|
+
* Firefox keeps emitting RTP after track.stop(), but the right lever
|
|
8555
|
+
* differs by track type:
|
|
8556
|
+
* - audio: `replaceTrack(null)` is the only reliable silencer;
|
|
8557
|
+
* `setParameters({encodings:[...active:false]})` does NOT stop
|
|
8558
|
+
* the Opus encoder.
|
|
8559
|
+
* - video: `setParameters({encodings:[...active:false]})` pauses
|
|
8560
|
+
* the encoder; `replaceTrack(null)` does NOT reliably stop the
|
|
8561
|
+
* video encoder. The prior active=true configuration is captured
|
|
8562
|
+
* onto `bundle.videoSender` so `updateTransceiver` can restore
|
|
8563
|
+
* it on the next publish.
|
|
8564
|
+
*
|
|
8565
|
+
* No-op on non-Firefox browsers and during teardown.
|
|
8566
|
+
*/
|
|
8567
|
+
this.silenceSenderOnFirefox = async (bundle) => {
|
|
8568
|
+
if (this.isDisposed || !isFirefox())
|
|
8569
|
+
return;
|
|
8570
|
+
const { transceiver, publishOption } = bundle;
|
|
8571
|
+
if (isAudioTrackType(publishOption.trackType)) {
|
|
8572
|
+
await transceiver.sender.replaceTrack(null).catch((err) => {
|
|
8573
|
+
this.logger.warn('Failed to clear audio sender track', err);
|
|
8574
|
+
});
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
8577
|
+
await this.disableAllEncodings(bundle);
|
|
8578
|
+
};
|
|
8579
|
+
this.disableAllEncodings = async (bundle) => {
|
|
8580
|
+
const { transceiver, publishOption } = bundle;
|
|
8581
|
+
const sender = transceiver.sender;
|
|
8582
|
+
const params = sender.getParameters();
|
|
8583
|
+
if (!params.encodings || params.encodings.length === 0)
|
|
8584
|
+
return;
|
|
8585
|
+
if (!bundle.videoSender) {
|
|
8586
|
+
this.transceiverCache.update(publishOption, {
|
|
8587
|
+
videoSender: {
|
|
8588
|
+
trackType: publishOption.trackType,
|
|
8589
|
+
publishOptionId: publishOption.id,
|
|
8590
|
+
codec: publishOption.codec,
|
|
8591
|
+
degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
|
|
8592
|
+
layers: params.encodings.map((e) => ({
|
|
8593
|
+
name: e.rid ?? 'q',
|
|
8594
|
+
active: e.active ?? true,
|
|
8595
|
+
maxBitrate: e.maxBitrate ?? 0,
|
|
8596
|
+
scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
|
|
8597
|
+
maxFramerate: e.maxFramerate ?? 0,
|
|
8598
|
+
// @ts-expect-error scalabilityMode is not in the typedefs yet
|
|
8599
|
+
scalabilityMode: e.scalabilityMode ?? '',
|
|
8600
|
+
})),
|
|
8601
|
+
},
|
|
8602
|
+
});
|
|
8603
|
+
}
|
|
8604
|
+
let changed = false;
|
|
8605
|
+
for (const encoding of params.encodings) {
|
|
8606
|
+
if (encoding.active !== false) {
|
|
8607
|
+
encoding.active = false;
|
|
8608
|
+
changed = true;
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8611
|
+
if (!changed)
|
|
8612
|
+
return;
|
|
8613
|
+
await sender.setParameters(params).catch((err) => {
|
|
8614
|
+
this.logger.error('Failed to disable video sender encodings:', err);
|
|
8615
|
+
});
|
|
8616
|
+
};
|
|
8510
8617
|
this.publishOptions = publishOptions;
|
|
8511
8618
|
this.on('iceRestart', (iceRestart) => {
|
|
8512
8619
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
@@ -8515,7 +8622,16 @@ class Publisher extends BasePeerConnection {
|
|
|
8515
8622
|
});
|
|
8516
8623
|
this.on('changePublishQuality', async (event) => {
|
|
8517
8624
|
for (const videoSender of event.videoSenders) {
|
|
8518
|
-
|
|
8625
|
+
// if not publishing, update the encodingConfigCache and don't modify the state.
|
|
8626
|
+
// we'll apply this config on the next publish/unmute.
|
|
8627
|
+
const { trackType, publishOptionId } = videoSender;
|
|
8628
|
+
const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
|
|
8629
|
+
if (bundle) {
|
|
8630
|
+
this.transceiverCache.update(bundle.publishOption, { videoSender });
|
|
8631
|
+
}
|
|
8632
|
+
if (isFirefox() && !this.isPublishing(trackType))
|
|
8633
|
+
continue;
|
|
8634
|
+
await this.changePublishQuality(videoSender, bundle);
|
|
8519
8635
|
}
|
|
8520
8636
|
});
|
|
8521
8637
|
this.on('changePublishOptions', (event) => {
|
|
@@ -8526,9 +8642,14 @@ class Publisher extends BasePeerConnection {
|
|
|
8526
8642
|
/**
|
|
8527
8643
|
* Disposes this Publisher instance.
|
|
8528
8644
|
*/
|
|
8529
|
-
dispose() {
|
|
8530
|
-
super.dispose();
|
|
8531
|
-
|
|
8645
|
+
async dispose() {
|
|
8646
|
+
await super.dispose();
|
|
8647
|
+
try {
|
|
8648
|
+
await this.stopAllTracks();
|
|
8649
|
+
}
|
|
8650
|
+
catch (err) {
|
|
8651
|
+
this.logger.warn('Failed to stop tracks during dispose', err);
|
|
8652
|
+
}
|
|
8532
8653
|
this.clonedTracks.clear();
|
|
8533
8654
|
}
|
|
8534
8655
|
}
|
|
@@ -11312,8 +11433,8 @@ const normalize = (options) => {
|
|
|
11312
11433
|
: false,
|
|
11313
11434
|
};
|
|
11314
11435
|
};
|
|
11315
|
-
const createSyntheticDevice = (deviceId, kind) => {
|
|
11316
|
-
return { deviceId, kind, label
|
|
11436
|
+
const createSyntheticDevice = (deviceId, kind, label = '') => {
|
|
11437
|
+
return { deviceId, kind, label, groupId: '' };
|
|
11317
11438
|
};
|
|
11318
11439
|
const readPreferences = (storageKey) => {
|
|
11319
11440
|
try {
|
|
@@ -11367,6 +11488,8 @@ class DeviceManager {
|
|
|
11367
11488
|
this.areSubscriptionsSetUp = false;
|
|
11368
11489
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
11369
11490
|
this.filters = [];
|
|
11491
|
+
this.virtualDevicesSubject = new BehaviorSubject([]);
|
|
11492
|
+
this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
|
|
11370
11493
|
this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
|
|
11371
11494
|
this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
|
|
11372
11495
|
/**
|
|
@@ -11379,6 +11502,7 @@ class DeviceManager {
|
|
|
11379
11502
|
this.subscriptions.forEach((s) => s());
|
|
11380
11503
|
this.subscriptions = [];
|
|
11381
11504
|
this.areSubscriptionsSetUp = false;
|
|
11505
|
+
this.virtualDevicesSubject.next([]);
|
|
11382
11506
|
};
|
|
11383
11507
|
this.runCurrentStreamCleanups = () => {
|
|
11384
11508
|
this.currentStreamCleanups.forEach((c) => c());
|
|
@@ -11437,7 +11561,93 @@ class DeviceManager {
|
|
|
11437
11561
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
11438
11562
|
*/
|
|
11439
11563
|
listDevices() {
|
|
11440
|
-
return this.getDevices()
|
|
11564
|
+
return combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(map(([real, virtual]) => [
|
|
11565
|
+
...real,
|
|
11566
|
+
...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
|
|
11567
|
+
]));
|
|
11568
|
+
}
|
|
11569
|
+
/**
|
|
11570
|
+
* Registers a virtual camera or microphone backed by a caller-supplied
|
|
11571
|
+
* stream factory. The device appears in `listDevices()` and can be selected
|
|
11572
|
+
* via `select()` like any real device.
|
|
11573
|
+
*
|
|
11574
|
+
* Web only. React Native is not supported.
|
|
11575
|
+
*
|
|
11576
|
+
* Only supported for camera and microphone managers; calling on any other
|
|
11577
|
+
* manager throws.
|
|
11578
|
+
*/
|
|
11579
|
+
registerVirtualDevice(virtualDevice) {
|
|
11580
|
+
if (isReactNative()) {
|
|
11581
|
+
throw new Error('Virtual devices are not supported on React Native.');
|
|
11582
|
+
}
|
|
11583
|
+
if (this.trackType !== TrackType.AUDIO &&
|
|
11584
|
+
this.trackType !== TrackType.VIDEO) {
|
|
11585
|
+
throw new Error('Virtual devices are only supported for camera and microphone.');
|
|
11586
|
+
}
|
|
11587
|
+
const deviceId = `stream-virtual:${generateUUIDv4()}`;
|
|
11588
|
+
const entry = {
|
|
11589
|
+
deviceId,
|
|
11590
|
+
kind: this.mediaDeviceKind,
|
|
11591
|
+
...virtualDevice,
|
|
11592
|
+
};
|
|
11593
|
+
setCurrentValue(this.virtualDevicesSubject, (current) => [
|
|
11594
|
+
...current,
|
|
11595
|
+
entry,
|
|
11596
|
+
]);
|
|
11597
|
+
return {
|
|
11598
|
+
deviceId: entry.deviceId,
|
|
11599
|
+
unregister: async () => {
|
|
11600
|
+
await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
|
|
11601
|
+
setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
|
|
11602
|
+
if (this.activeVirtualSession?.deviceId === deviceId) {
|
|
11603
|
+
await this.stopActiveVirtualSession();
|
|
11604
|
+
}
|
|
11605
|
+
});
|
|
11606
|
+
if (this.state.selectedDevice === deviceId) {
|
|
11607
|
+
await this.statusChangeSettled();
|
|
11608
|
+
await this.disable({ forceStop: true });
|
|
11609
|
+
await this.select(undefined);
|
|
11610
|
+
}
|
|
11611
|
+
},
|
|
11612
|
+
};
|
|
11613
|
+
}
|
|
11614
|
+
sanitizeVirtualStream(stream) {
|
|
11615
|
+
stream.getTracks().forEach((track) => {
|
|
11616
|
+
const originalGetSettings = track.getSettings.bind(track);
|
|
11617
|
+
track.getSettings = () => {
|
|
11618
|
+
const settings = originalGetSettings();
|
|
11619
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11620
|
+
const { deviceId, ...rest } = settings;
|
|
11621
|
+
return rest;
|
|
11622
|
+
};
|
|
11623
|
+
});
|
|
11624
|
+
return stream;
|
|
11625
|
+
}
|
|
11626
|
+
findVirtualDevice(deviceId) {
|
|
11627
|
+
if (!deviceId)
|
|
11628
|
+
return undefined;
|
|
11629
|
+
return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
|
|
11630
|
+
}
|
|
11631
|
+
async stopActiveVirtualSession() {
|
|
11632
|
+
const session = this.activeVirtualSession;
|
|
11633
|
+
this.activeVirtualSession = undefined;
|
|
11634
|
+
await session?.stop?.();
|
|
11635
|
+
}
|
|
11636
|
+
async getSelectedStream(constraints) {
|
|
11637
|
+
const deviceId = this.state.selectedDevice;
|
|
11638
|
+
if (!deviceId?.startsWith('stream-virtual')) {
|
|
11639
|
+
return this.getStream(constraints);
|
|
11640
|
+
}
|
|
11641
|
+
return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
|
|
11642
|
+
const virtualDevice = this.findVirtualDevice(deviceId);
|
|
11643
|
+
if (!virtualDevice) {
|
|
11644
|
+
throw new Error(`Virtual device is not registered: ${deviceId}`);
|
|
11645
|
+
}
|
|
11646
|
+
await this.stopActiveVirtualSession();
|
|
11647
|
+
const { stream, stop } = await virtualDevice.getUserMedia(constraints);
|
|
11648
|
+
this.activeVirtualSession = { deviceId, stop };
|
|
11649
|
+
return this.sanitizeVirtualStream(stream);
|
|
11650
|
+
});
|
|
11441
11651
|
}
|
|
11442
11652
|
/**
|
|
11443
11653
|
* Returns `true` when this device is in enabled state.
|
|
@@ -11597,6 +11807,9 @@ class DeviceManager {
|
|
|
11597
11807
|
}
|
|
11598
11808
|
});
|
|
11599
11809
|
}
|
|
11810
|
+
getResolvedConstraints(constraints) {
|
|
11811
|
+
return constraints;
|
|
11812
|
+
}
|
|
11600
11813
|
publishStream(stream, options) {
|
|
11601
11814
|
return this.call.publish(stream, this.trackType, options);
|
|
11602
11815
|
}
|
|
@@ -11617,6 +11830,7 @@ class DeviceManager {
|
|
|
11617
11830
|
this.muteLocalStream(stopTracks);
|
|
11618
11831
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
11619
11832
|
if (allEnded) {
|
|
11833
|
+
await this.stopActiveVirtualSession();
|
|
11620
11834
|
// @ts-expect-error release() is present in react-native-webrtc
|
|
11621
11835
|
if (typeof mediaStream.release === 'function') {
|
|
11622
11836
|
// @ts-expect-error called to dispose the stream in RN
|
|
@@ -11672,12 +11886,12 @@ class DeviceManager {
|
|
|
11672
11886
|
// before chainWith below registers new ones for the new chain.
|
|
11673
11887
|
this.runCurrentStreamCleanups();
|
|
11674
11888
|
const defaultConstraints = this.state.defaultConstraints;
|
|
11675
|
-
const constraints = {
|
|
11889
|
+
const constraints = this.getResolvedConstraints({
|
|
11676
11890
|
...defaultConstraints,
|
|
11677
11891
|
deviceId: this.state.selectedDevice
|
|
11678
11892
|
? { exact: this.state.selectedDevice }
|
|
11679
11893
|
: undefined,
|
|
11680
|
-
};
|
|
11894
|
+
});
|
|
11681
11895
|
/**
|
|
11682
11896
|
* Chains two media streams together.
|
|
11683
11897
|
*
|
|
@@ -11734,7 +11948,7 @@ class DeviceManager {
|
|
|
11734
11948
|
};
|
|
11735
11949
|
// the rootStream represents the stream coming from the actual device
|
|
11736
11950
|
// e.g. camera or microphone stream
|
|
11737
|
-
rootStreamPromise = this.
|
|
11951
|
+
rootStreamPromise = this.getSelectedStream(constraints);
|
|
11738
11952
|
// we publish the last MediaStream of the chain
|
|
11739
11953
|
stream = await this.filters.reduce((parent, entry) => parent
|
|
11740
11954
|
.then((inputStream) => {
|
|
@@ -12051,7 +12265,10 @@ class DeviceManagerState {
|
|
|
12051
12265
|
setCurrentValue(this.mediaStreamSubject, stream);
|
|
12052
12266
|
setCurrentValue(this.rootMediaStreamSubject, rootStream);
|
|
12053
12267
|
if (rootStream) {
|
|
12054
|
-
this.
|
|
12268
|
+
const derived = this.getDeviceIdFromStream(rootStream);
|
|
12269
|
+
if (derived) {
|
|
12270
|
+
this.setDevice(derived);
|
|
12271
|
+
}
|
|
12055
12272
|
}
|
|
12056
12273
|
}
|
|
12057
12274
|
/**
|
|
@@ -12264,7 +12481,7 @@ class CameraManager extends DeviceManager {
|
|
|
12264
12481
|
getDevices() {
|
|
12265
12482
|
return getVideoDevices(this.call.tracer);
|
|
12266
12483
|
}
|
|
12267
|
-
|
|
12484
|
+
getResolvedConstraints(constraints) {
|
|
12268
12485
|
constraints.width = this.targetResolution.width;
|
|
12269
12486
|
constraints.height = this.targetResolution.height;
|
|
12270
12487
|
// We can't set both device id and facing mode
|
|
@@ -12275,6 +12492,9 @@ class CameraManager extends DeviceManager {
|
|
|
12275
12492
|
constraints.facingMode =
|
|
12276
12493
|
this.state.direction === 'front' ? 'user' : 'environment';
|
|
12277
12494
|
}
|
|
12495
|
+
return constraints;
|
|
12496
|
+
}
|
|
12497
|
+
getStream(constraints) {
|
|
12278
12498
|
return getVideoStream(constraints, this.call.tracer);
|
|
12279
12499
|
}
|
|
12280
12500
|
}
|
|
@@ -13602,9 +13822,10 @@ class Call {
|
|
|
13602
13822
|
this.sfuStatsReporter?.flush();
|
|
13603
13823
|
this.sfuStatsReporter?.stop();
|
|
13604
13824
|
this.sfuStatsReporter = undefined;
|
|
13605
|
-
this.
|
|
13825
|
+
this.lastStatsOptions = undefined;
|
|
13826
|
+
await this.subscriber?.dispose();
|
|
13606
13827
|
this.subscriber = undefined;
|
|
13607
|
-
this.publisher?.dispose();
|
|
13828
|
+
await this.publisher?.dispose();
|
|
13608
13829
|
this.publisher = undefined;
|
|
13609
13830
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
13610
13831
|
this.sfuClient = undefined;
|
|
@@ -13880,15 +14101,17 @@ class Call {
|
|
|
13880
14101
|
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
13881
14102
|
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
13882
14103
|
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
13883
|
-
let statsOptions = this.
|
|
14104
|
+
let statsOptions = this.lastStatsOptions;
|
|
13884
14105
|
if (!this.credentials ||
|
|
13885
14106
|
!statsOptions ||
|
|
13886
14107
|
performingRejoin ||
|
|
13887
|
-
performingMigration
|
|
14108
|
+
performingMigration ||
|
|
14109
|
+
data?.migrating_from) {
|
|
13888
14110
|
try {
|
|
13889
14111
|
const joinResponse = await this.doJoinRequest(data);
|
|
13890
14112
|
this.credentials = joinResponse.credentials;
|
|
13891
14113
|
statsOptions = joinResponse.stats_options;
|
|
14114
|
+
this.lastStatsOptions = statsOptions;
|
|
13892
14115
|
}
|
|
13893
14116
|
catch (error) {
|
|
13894
14117
|
// prevent triggering reconnect flow if the state is OFFLINE
|
|
@@ -13995,7 +14218,7 @@ class Call {
|
|
|
13995
14218
|
}
|
|
13996
14219
|
else {
|
|
13997
14220
|
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
13998
|
-
this.initPublisherAndSubscriber({
|
|
14221
|
+
await this.initPublisherAndSubscriber({
|
|
13999
14222
|
sfuClient,
|
|
14000
14223
|
connectionConfig,
|
|
14001
14224
|
clientDetails,
|
|
@@ -14140,11 +14363,11 @@ class Call {
|
|
|
14140
14363
|
* Initializes the Publisher and Subscriber Peer Connections.
|
|
14141
14364
|
* @internal
|
|
14142
14365
|
*/
|
|
14143
|
-
this.initPublisherAndSubscriber = (opts) => {
|
|
14366
|
+
this.initPublisherAndSubscriber = async (opts) => {
|
|
14144
14367
|
const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
|
|
14145
14368
|
const { enable_rtc_stats: enableTracing } = statsOptions;
|
|
14146
14369
|
if (closePreviousInstances && this.subscriber) {
|
|
14147
|
-
this.subscriber.dispose();
|
|
14370
|
+
await this.subscriber.dispose();
|
|
14148
14371
|
}
|
|
14149
14372
|
const basePeerConnectionOptions = {
|
|
14150
14373
|
sfuClient,
|
|
@@ -14173,7 +14396,7 @@ class Call {
|
|
|
14173
14396
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
14174
14397
|
if (!isAnonymous) {
|
|
14175
14398
|
if (closePreviousInstances && this.publisher) {
|
|
14176
|
-
this.publisher.dispose();
|
|
14399
|
+
await this.publisher.dispose();
|
|
14177
14400
|
}
|
|
14178
14401
|
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14179
14402
|
}
|
|
@@ -14276,10 +14499,17 @@ class Call {
|
|
|
14276
14499
|
* `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
|
|
14277
14500
|
*/
|
|
14278
14501
|
this.reconnect = async (strategy, reason) => {
|
|
14279
|
-
if (this.state.callingState === CallingState.
|
|
14502
|
+
if (this.state.callingState === CallingState.JOINING ||
|
|
14503
|
+
this.state.callingState === CallingState.RECONNECTING ||
|
|
14280
14504
|
this.state.callingState === CallingState.MIGRATING ||
|
|
14281
14505
|
this.state.callingState === CallingState.RECONNECTING_FAILED)
|
|
14282
14506
|
return;
|
|
14507
|
+
// Drop redundant reconnect calls. If a reconnect is already queued or
|
|
14508
|
+
// running for this Call, that entry will resolve whatever broke;
|
|
14509
|
+
// queueing more entries just replays the full REJOIN cycle (one extra
|
|
14510
|
+
// `POST /join` per entry) once the call is already healthy again.
|
|
14511
|
+
if (hasPending(this.reconnectConcurrencyTag))
|
|
14512
|
+
return;
|
|
14283
14513
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
14284
14514
|
const reconnectStartTime = Date.now();
|
|
14285
14515
|
this.reconnectStrategy = strategy;
|
|
@@ -14484,8 +14714,8 @@ class Call {
|
|
|
14484
14714
|
this.state.setCallingState(CallingState.JOINED);
|
|
14485
14715
|
}
|
|
14486
14716
|
finally {
|
|
14487
|
-
currentSubscriber?.dispose();
|
|
14488
|
-
currentPublisher?.dispose();
|
|
14717
|
+
await currentSubscriber?.dispose();
|
|
14718
|
+
await currentPublisher?.dispose();
|
|
14489
14719
|
// and close the previous SFU client, without specifying close code
|
|
14490
14720
|
currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
|
|
14491
14721
|
}
|
|
@@ -14674,7 +14904,7 @@ class Call {
|
|
|
14674
14904
|
this.stopPublish = async (...trackTypes) => {
|
|
14675
14905
|
if (!this.sfuClient || !this.publisher)
|
|
14676
14906
|
return;
|
|
14677
|
-
this.publisher.stopTracks(...trackTypes);
|
|
14907
|
+
await this.publisher.stopTracks(...trackTypes);
|
|
14678
14908
|
await this.updateLocalStreamState(undefined, ...trackTypes);
|
|
14679
14909
|
};
|
|
14680
14910
|
/**
|
|
@@ -16787,7 +17017,7 @@ class StreamClient {
|
|
|
16787
17017
|
this.getUserAgent = () => {
|
|
16788
17018
|
if (!this.cachedUserAgent) {
|
|
16789
17019
|
const { clientAppIdentifier = {} } = this.options;
|
|
16790
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17020
|
+
const { sdkName = 'js', sdkVersion = "1.51.0", ...extras } = clientAppIdentifier;
|
|
16791
17021
|
this.cachedUserAgent = [
|
|
16792
17022
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16793
17023
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|