@stream-io/video-client 1.7.4 → 1.8.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/dist/index.cjs.js CHANGED
@@ -3041,7 +3041,7 @@ const retryable = async (rpc, signal) => {
3041
3041
  return result;
3042
3042
  };
3043
3043
 
3044
- const version = "1.7.4";
3044
+ const version = "1.8.0";
3045
3045
  const [major, minor, patch] = version.split('.');
3046
3046
  let sdkInfo = {
3047
3047
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -4312,6 +4312,34 @@ class CallState {
4312
4312
  return p;
4313
4313
  }));
4314
4314
  };
4315
+ /**
4316
+ * Update track subscription configuration for one or more participants.
4317
+ * You have to create a subscription for each participant for all the different kinds of tracks you want to receive.
4318
+ * You can only subscribe for tracks after the participant started publishing the given kind of track.
4319
+ *
4320
+ * @param trackType the kind of subscription to update.
4321
+ * @param changes the list of subscription changes to do.
4322
+ * @param type the debounce type to use for the update.
4323
+ */
4324
+ this.updateParticipantTracks = (trackType, changes) => {
4325
+ return this.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
4326
+ if (change.dimension) {
4327
+ change.dimension.height = Math.ceil(change.dimension.height);
4328
+ change.dimension.width = Math.ceil(change.dimension.width);
4329
+ }
4330
+ const prop = trackType === 'videoTrack'
4331
+ ? 'videoDimension'
4332
+ : trackType === 'screenShareTrack'
4333
+ ? 'screenShareDimension'
4334
+ : undefined;
4335
+ if (prop) {
4336
+ acc[sessionId] = {
4337
+ [prop]: change.dimension,
4338
+ };
4339
+ }
4340
+ return acc;
4341
+ }, {}));
4342
+ };
4315
4343
  /**
4316
4344
  * Updates the call state with the data received from the server.
4317
4345
  *
@@ -7092,6 +7120,7 @@ const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
7092
7120
  videoTrack: exports.VisibilityState.UNKNOWN,
7093
7121
  screenShareTrack: exports.VisibilityState.UNKNOWN,
7094
7122
  };
7123
+ const globalOverrideKey = Symbol('globalOverrideKey');
7095
7124
  /**
7096
7125
  * A manager class that handles dynascale related tasks like:
7097
7126
  *
@@ -7108,12 +7137,64 @@ class DynascaleManager {
7108
7137
  *
7109
7138
  * @param call the call to manage.
7110
7139
  */
7111
- constructor(call) {
7140
+ constructor(callState, speaker) {
7112
7141
  /**
7113
7142
  * The viewport tracker instance.
7114
7143
  */
7115
7144
  this.viewportTracker = new ViewportTracker();
7116
7145
  this.logger = getLogger(['DynascaleManager']);
7146
+ this.pendingSubscriptionsUpdate = null;
7147
+ this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
7148
+ this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
7149
+ this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(rxjs.map((overrides) => {
7150
+ const { [globalOverrideKey]: globalSettings, ...participants } = overrides;
7151
+ return {
7152
+ enabled: globalSettings?.enabled !== false,
7153
+ preferredResolution: globalSettings?.enabled
7154
+ ? globalSettings.dimension
7155
+ : undefined,
7156
+ participants: Object.fromEntries(Object.entries(participants).map(([sessionId, participantOverride]) => [
7157
+ sessionId,
7158
+ {
7159
+ enabled: participantOverride?.enabled !== false,
7160
+ preferredResolution: participantOverride?.enabled
7161
+ ? participantOverride.dimension
7162
+ : undefined,
7163
+ },
7164
+ ])),
7165
+ isParticipantVideoEnabled: (sessionId) => overrides[sessionId]?.enabled ??
7166
+ overrides[globalOverrideKey]?.enabled ??
7167
+ true,
7168
+ };
7169
+ }), rxjs.shareReplay(1));
7170
+ this.setVideoTrackSubscriptionOverrides = (override, sessionIds) => {
7171
+ if (!sessionIds) {
7172
+ return setCurrentValue(this.videoTrackSubscriptionOverridesSubject, override ? { [globalOverrideKey]: override } : {});
7173
+ }
7174
+ return setCurrentValue(this.videoTrackSubscriptionOverridesSubject, (overrides) => ({
7175
+ ...overrides,
7176
+ ...Object.fromEntries(sessionIds.map((id) => [id, override])),
7177
+ }));
7178
+ };
7179
+ this.applyTrackSubscriptions = (debounceType = exports.DebounceType.SLOW) => {
7180
+ if (this.pendingSubscriptionsUpdate) {
7181
+ clearTimeout(this.pendingSubscriptionsUpdate);
7182
+ }
7183
+ const updateSubscriptions = () => {
7184
+ this.pendingSubscriptionsUpdate = null;
7185
+ this.sfuClient
7186
+ ?.updateSubscriptions(this.trackSubscriptions)
7187
+ .catch((err) => {
7188
+ this.logger('debug', `Failed to update track subscriptions`, err);
7189
+ });
7190
+ };
7191
+ if (debounceType) {
7192
+ this.pendingSubscriptionsUpdate = setTimeout(updateSubscriptions, debounceType);
7193
+ }
7194
+ else {
7195
+ updateSubscriptions();
7196
+ }
7197
+ };
7117
7198
  /**
7118
7199
  * Will begin tracking the given element for visibility changes within the
7119
7200
  * configured viewport element (`call.setViewport`).
@@ -7125,7 +7206,7 @@ class DynascaleManager {
7125
7206
  */
7126
7207
  this.trackElementVisibility = (element, sessionId, trackType) => {
7127
7208
  const cleanup = this.viewportTracker.observe(element, (entry) => {
7128
- this.call.state.updateParticipant(sessionId, (participant) => {
7209
+ this.callState.updateParticipant(sessionId, (participant) => {
7129
7210
  const previousVisibilityState = participant.viewportVisibilityState ??
7130
7211
  DEFAULT_VIEWPORT_VISIBILITY_STATE;
7131
7212
  // observer triggers when the element is "moved" to be a fullscreen element
@@ -7147,7 +7228,7 @@ class DynascaleManager {
7147
7228
  // reset visibility state to UNKNOWN upon cleanup
7148
7229
  // so that the layouts that are not actively observed
7149
7230
  // can still function normally (runtime layout switching)
7150
- this.call.state.updateParticipant(sessionId, (participant) => {
7231
+ this.callState.updateParticipant(sessionId, (participant) => {
7151
7232
  const previousVisibilityState = participant.viewportVisibilityState ??
7152
7233
  DEFAULT_VIEWPORT_VISIBILITY_STATE;
7153
7234
  return {
@@ -7184,7 +7265,7 @@ class DynascaleManager {
7184
7265
  * @param trackType the kind of video.
7185
7266
  */
7186
7267
  this.bindVideoElement = (videoElement, sessionId, trackType) => {
7187
- const boundParticipant = this.call.state.findParticipantBySessionId(sessionId);
7268
+ const boundParticipant = this.callState.findParticipantBySessionId(sessionId);
7188
7269
  if (!boundParticipant)
7189
7270
  return;
7190
7271
  const requestTrackWithDimensions = (debounceType, dimension) => {
@@ -7196,9 +7277,12 @@ class DynascaleManager {
7196
7277
  this.logger('debug', `Ignoring 0x0 dimension`, boundParticipant);
7197
7278
  dimension = undefined;
7198
7279
  }
7199
- this.call.updateSubscriptionsPartial(trackType, { [sessionId]: { dimension } }, debounceType);
7280
+ this.callState.updateParticipantTracks(trackType, {
7281
+ [sessionId]: { dimension },
7282
+ });
7283
+ this.applyTrackSubscriptions(debounceType);
7200
7284
  };
7201
- const participant$ = this.call.state.participants$.pipe(rxjs.map((participants) => participants.find((participant) => participant.sessionId === sessionId)), rxjs.takeWhile((participant) => !!participant), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
7285
+ const participant$ = this.callState.participants$.pipe(rxjs.map((participants) => participants.find((participant) => participant.sessionId === sessionId)), rxjs.takeWhile((participant) => !!participant), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
7202
7286
  /**
7203
7287
  * Since the video elements are now being removed from the DOM (React SDK) upon
7204
7288
  * visibility change, this subscription is not in use an stays here only for the
@@ -7313,10 +7397,10 @@ class DynascaleManager {
7313
7397
  * @returns a cleanup function that will unbind the audio element.
7314
7398
  */
7315
7399
  this.bindAudioElement = (audioElement, sessionId, trackType) => {
7316
- const participant = this.call.state.findParticipantBySessionId(sessionId);
7400
+ const participant = this.callState.findParticipantBySessionId(sessionId);
7317
7401
  if (!participant || participant.isLocalParticipant)
7318
7402
  return;
7319
- const participant$ = this.call.state.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.sessionId === sessionId)), rxjs.takeWhile((p) => !!p), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
7403
+ const participant$ = this.callState.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.sessionId === sessionId)), rxjs.takeWhile((p) => !!p), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
7320
7404
  const updateMediaStreamSubscription = participant$
7321
7405
  .pipe(rxjs.distinctUntilKeyChanged(trackType === 'screenShareAudioTrack'
7322
7406
  ? 'screenShareAudioStream'
@@ -7336,7 +7420,7 @@ class DynascaleManager {
7336
7420
  // audio output device shall be set after the audio element is played
7337
7421
  // otherwise, the browser will not pick it up, and will always
7338
7422
  // play audio through the system's default device
7339
- const { selectedDevice } = this.call.speaker.state;
7423
+ const { selectedDevice } = this.speaker.state;
7340
7424
  if (selectedDevice && 'setSinkId' in audioElement) {
7341
7425
  audioElement.setSinkId(selectedDevice);
7342
7426
  }
@@ -7345,13 +7429,13 @@ class DynascaleManager {
7345
7429
  });
7346
7430
  const sinkIdSubscription = !('setSinkId' in audioElement)
7347
7431
  ? null
7348
- : this.call.speaker.state.selectedDevice$.subscribe((deviceId) => {
7432
+ : this.speaker.state.selectedDevice$.subscribe((deviceId) => {
7349
7433
  if (deviceId) {
7350
7434
  audioElement.setSinkId(deviceId);
7351
7435
  }
7352
7436
  });
7353
7437
  const volumeSubscription = rxjs.combineLatest([
7354
- this.call.speaker.state.volume$,
7438
+ this.speaker.state.volume$,
7355
7439
  participant$.pipe(rxjs.distinctUntilKeyChanged('audioVolume')),
7356
7440
  ]).subscribe(([volume, p]) => {
7357
7441
  audioElement.volume = p.audioVolume ?? volume;
@@ -7363,7 +7447,50 @@ class DynascaleManager {
7363
7447
  updateMediaStreamSubscription.unsubscribe();
7364
7448
  };
7365
7449
  };
7366
- this.call = call;
7450
+ this.callState = callState;
7451
+ this.speaker = speaker;
7452
+ }
7453
+ setSfuClient(sfuClient) {
7454
+ this.sfuClient = sfuClient;
7455
+ }
7456
+ get trackSubscriptions() {
7457
+ const subscriptions = [];
7458
+ for (const p of this.callState.remoteParticipants) {
7459
+ // NOTE: audio tracks don't have to be requested explicitly
7460
+ // as the SFU will implicitly subscribe us to all of them,
7461
+ // once they become available.
7462
+ if (p.videoDimension && hasVideo(p)) {
7463
+ const override = this.videoTrackSubscriptionOverrides[p.sessionId] ??
7464
+ this.videoTrackSubscriptionOverrides[globalOverrideKey];
7465
+ if (override?.enabled !== false) {
7466
+ subscriptions.push({
7467
+ userId: p.userId,
7468
+ sessionId: p.sessionId,
7469
+ trackType: TrackType.VIDEO,
7470
+ dimension: override?.dimension ?? p.videoDimension,
7471
+ });
7472
+ }
7473
+ }
7474
+ if (p.screenShareDimension && hasScreenShare(p)) {
7475
+ subscriptions.push({
7476
+ userId: p.userId,
7477
+ sessionId: p.sessionId,
7478
+ trackType: TrackType.SCREEN_SHARE,
7479
+ dimension: p.screenShareDimension,
7480
+ });
7481
+ }
7482
+ if (hasScreenShareAudio(p)) {
7483
+ subscriptions.push({
7484
+ userId: p.userId,
7485
+ sessionId: p.sessionId,
7486
+ trackType: TrackType.SCREEN_SHARE_AUDIO,
7487
+ });
7488
+ }
7489
+ }
7490
+ return subscriptions;
7491
+ }
7492
+ get videoTrackSubscriptionOverrides() {
7493
+ return getCurrentValue(this.videoTrackSubscriptionOverrides$);
7367
7494
  }
7368
7495
  }
7369
7496
 
@@ -9182,10 +9309,6 @@ class Call {
9182
9309
  * The state of this call.
9183
9310
  */
9184
9311
  this.state = new CallState();
9185
- /**
9186
- * The DynascaleManager instance.
9187
- */
9188
- this.dynascaleManager = new DynascaleManager(this);
9189
9312
  /**
9190
9313
  * The permissions context of this call.
9191
9314
  */
@@ -9195,7 +9318,6 @@ class Call {
9195
9318
  * @private
9196
9319
  */
9197
9320
  this.dispatcher = new Dispatcher();
9198
- this.trackSubscriptionsSubject = new rxjs.BehaviorSubject({ type: exports.DebounceType.MEDIUM, data: [] });
9199
9321
  this.sfuClientTag = 0;
9200
9322
  this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
9201
9323
  this.reconnectAttempts = 0;
@@ -9336,6 +9458,7 @@ class Call {
9336
9458
  this.publisher = undefined;
9337
9459
  await this.sfuClient?.leaveAndClose(reason);
9338
9460
  this.sfuClient = undefined;
9461
+ this.dynascaleManager.setSfuClient(undefined);
9339
9462
  this.state.setCallingState(exports.CallingState.LEFT);
9340
9463
  // Call all leave call hooks, e.g. to clean up global event handlers
9341
9464
  this.leaveCallHooks.forEach((hook) => hook());
@@ -9498,6 +9621,7 @@ class Call {
9498
9621
  })
9499
9622
  : previousSfuClient;
9500
9623
  this.sfuClient = sfuClient;
9624
+ this.dynascaleManager.setSfuClient(sfuClient);
9501
9625
  const clientDetails = getClientDetails();
9502
9626
  // we don't need to send JoinRequest if we are re-using an existing healthy SFU client
9503
9627
  if (previousSfuClient !== sfuClient) {
@@ -9570,11 +9694,10 @@ class Call {
9570
9694
  const strategy = this.reconnectStrategy;
9571
9695
  const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
9572
9696
  const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
9573
- const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
9574
9697
  return {
9575
9698
  strategy,
9576
9699
  announcedTracks,
9577
- subscriptions: subscribedTracks.data || [],
9700
+ subscriptions: this.dynascaleManager.trackSubscriptions,
9578
9701
  reconnectAttempt: this.reconnectAttempts,
9579
9702
  fromSfuId: migratingFromSfuId || '',
9580
9703
  previousSessionId: performingRejoin ? previousSessionId || '' : '',
@@ -9944,7 +10067,7 @@ class Call {
9944
10067
  const { remoteParticipants } = this.state;
9945
10068
  if (remoteParticipants.length <= 0)
9946
10069
  return;
9947
- this.updateSubscriptions(remoteParticipants, exports.DebounceType.FAST);
10070
+ this.dynascaleManager.applyTrackSubscriptions(undefined);
9948
10071
  };
9949
10072
  /**
9950
10073
  * Starts publishing the given video stream to the call.
@@ -10067,71 +10190,6 @@ class Call {
10067
10190
  this.logger('warn', 'Failed to notify stop of noise cancellation', err);
10068
10191
  });
10069
10192
  };
10070
- /**
10071
- * Update track subscription configuration for one or more participants.
10072
- * You have to create a subscription for each participant for all the different kinds of tracks you want to receive.
10073
- * You can only subscribe for tracks after the participant started publishing the given kind of track.
10074
- *
10075
- * @param trackType the kind of subscription to update.
10076
- * @param changes the list of subscription changes to do.
10077
- * @param type the debounce type to use for the update.
10078
- */
10079
- this.updateSubscriptionsPartial = (trackType, changes, type = exports.DebounceType.SLOW) => {
10080
- const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
10081
- if (change.dimension) {
10082
- change.dimension.height = Math.ceil(change.dimension.height);
10083
- change.dimension.width = Math.ceil(change.dimension.width);
10084
- }
10085
- const prop = trackType === 'videoTrack'
10086
- ? 'videoDimension'
10087
- : trackType === 'screenShareTrack'
10088
- ? 'screenShareDimension'
10089
- : undefined;
10090
- if (prop) {
10091
- acc[sessionId] = {
10092
- [prop]: change.dimension,
10093
- };
10094
- }
10095
- return acc;
10096
- }, {}));
10097
- this.updateSubscriptions(participants, type);
10098
- };
10099
- this.updateSubscriptions = (participants, type = exports.DebounceType.SLOW) => {
10100
- const subscriptions = [];
10101
- for (const p of participants) {
10102
- // we don't want to subscribe to our own tracks
10103
- if (p.isLocalParticipant)
10104
- continue;
10105
- // NOTE: audio tracks don't have to be requested explicitly
10106
- // as the SFU will implicitly subscribe us to all of them,
10107
- // once they become available.
10108
- if (p.videoDimension && hasVideo(p)) {
10109
- subscriptions.push({
10110
- userId: p.userId,
10111
- sessionId: p.sessionId,
10112
- trackType: TrackType.VIDEO,
10113
- dimension: p.videoDimension,
10114
- });
10115
- }
10116
- if (p.screenShareDimension && hasScreenShare(p)) {
10117
- subscriptions.push({
10118
- userId: p.userId,
10119
- sessionId: p.sessionId,
10120
- trackType: TrackType.SCREEN_SHARE,
10121
- dimension: p.screenShareDimension,
10122
- });
10123
- }
10124
- if (hasScreenShareAudio(p)) {
10125
- subscriptions.push({
10126
- userId: p.userId,
10127
- sessionId: p.sessionId,
10128
- trackType: TrackType.SCREEN_SHARE_AUDIO,
10129
- });
10130
- }
10131
- }
10132
- // schedule update
10133
- this.trackSubscriptionsSubject.next({ type, data: subscriptions });
10134
- };
10135
10193
  /**
10136
10194
  * Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
10137
10195
  * This is usually helpful when detailed stats for a specific participant are needed.
@@ -10711,6 +10769,33 @@ class Call {
10711
10769
  imageElement.removeEventListener('error', handleError);
10712
10770
  };
10713
10771
  };
10772
+ /**
10773
+ * Specify preference for incoming video resolution. The preference will
10774
+ * be matched as close as possible, but actual resolution will depend
10775
+ * on the video source quality and client network conditions. Will enable
10776
+ * incoming video, if previously disabled.
10777
+ *
10778
+ * @param resolution preferred resolution, or `undefined` to clear preference
10779
+ * @param sessionIds optionally specify session ids of the participants this
10780
+ * preference has effect on. Affects all participants by default.
10781
+ */
10782
+ this.setPreferredIncomingVideoResolution = (resolution, sessionIds) => {
10783
+ this.dynascaleManager.setVideoTrackSubscriptionOverrides(resolution
10784
+ ? {
10785
+ enabled: true,
10786
+ dimension: resolution,
10787
+ }
10788
+ : undefined, sessionIds);
10789
+ this.dynascaleManager.applyTrackSubscriptions();
10790
+ };
10791
+ /**
10792
+ * Enables or disables incoming video from all remote call participants,
10793
+ * and removes any preference for preferred resolution.
10794
+ */
10795
+ this.setIncomingVideoEnabled = (enabled) => {
10796
+ this.dynascaleManager.setVideoTrackSubscriptionOverrides(enabled ? undefined : { enabled: false });
10797
+ this.dynascaleManager.applyTrackSubscriptions();
10798
+ };
10714
10799
  this.type = type;
10715
10800
  this.id = id;
10716
10801
  this.cid = `${type}:${id}`;
@@ -10732,6 +10817,7 @@ class Call {
10732
10817
  this.microphone = new MicrophoneManager(this);
10733
10818
  this.speaker = new SpeakerManager(this);
10734
10819
  this.screenShare = new ScreenShareManager(this);
10820
+ this.dynascaleManager = new DynascaleManager(this.state, this.speaker);
10735
10821
  }
10736
10822
  async setup() {
10737
10823
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
@@ -10744,9 +10830,6 @@ class Call {
10744
10830
  this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
10745
10831
  this.registerEffects();
10746
10832
  this.registerReconnectHandlers();
10747
- this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(rxjs.debounce((v) => rxjs.timer(v.type)), rxjs.map((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
10748
- this.logger('debug', `Failed to update track subscriptions`, err);
10749
- })));
10750
10833
  if (this.state.callingState === exports.CallingState.LEFT) {
10751
10834
  this.state.setCallingState(exports.CallingState.IDLE);
10752
10835
  }
@@ -12392,7 +12475,7 @@ class StreamClient {
12392
12475
  });
12393
12476
  };
12394
12477
  this.getUserAgent = () => {
12395
- const version = "1.7.4";
12478
+ const version = "1.8.0";
12396
12479
  return (this.userAgent ||
12397
12480
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12398
12481
  };