@stream-io/video-client 1.53.2 → 1.54.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.browser.es.js +111 -12
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +111 -12
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +111 -12
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +13 -1
  9. package/dist/src/coordinator/connection/types.d.ts +6 -0
  10. package/dist/src/gen/google/protobuf/struct.d.ts +3 -1
  11. package/dist/src/gen/google/protobuf/timestamp.d.ts +3 -1
  12. package/dist/src/gen/video/sfu/event/events.d.ts +22 -1
  13. package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
  14. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +23 -2
  15. package/dist/src/reporting/ClientEventReporter.d.ts +2 -0
  16. package/dist/src/rtc/Publisher.d.ts +4 -1
  17. package/dist/src/rtc/Subscriber.d.ts +7 -0
  18. package/package.json +1 -1
  19. package/src/Call.ts +86 -6
  20. package/src/StreamVideoClient.ts +1 -0
  21. package/src/coordinator/connection/types.ts +7 -0
  22. package/src/gen/google/protobuf/struct.ts +7 -12
  23. package/src/gen/google/protobuf/timestamp.ts +6 -7
  24. package/src/gen/video/sfu/event/events.ts +23 -25
  25. package/src/gen/video/sfu/models/models.ts +11 -1
  26. package/src/gen/video/sfu/signal_rpc/signal.client.ts +25 -29
  27. package/src/gen/video/sfu/signal_rpc/signal.ts +1 -0
  28. package/src/helpers/client-details.ts +1 -1
  29. package/src/reporting/ClientEventReporter.ts +4 -0
  30. package/src/reporting/__tests__/ClientEventReporter.test.ts +33 -0
  31. package/src/rtc/Publisher.ts +4 -0
  32. package/src/rtc/Subscriber.ts +28 -1
  33. package/src/rtc/__tests__/Call.reconnect.test.ts +149 -2
package/dist/index.cjs.js CHANGED
@@ -527,6 +527,7 @@ class ErrorFromResponse extends Error {
527
527
  }
528
528
  }
529
529
 
530
+ /* eslint-disable */
530
531
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
531
532
  // @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
532
533
  // tslint:disable
@@ -792,6 +793,7 @@ class ListValue$Type extends runtime.MessageType {
792
793
  */
793
794
  const ListValue = new ListValue$Type();
794
795
 
796
+ /* eslint-disable */
795
797
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
796
798
  // @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
797
799
  // tslint:disable
@@ -1858,6 +1860,12 @@ class TrackInfo$Type extends runtime.MessageType {
1858
1860
  kind: 'scalar',
1859
1861
  T: 5 /*ScalarType.INT32*/,
1860
1862
  },
1863
+ {
1864
+ no: 13,
1865
+ name: 'self_sub_audio_video',
1866
+ kind: 'scalar',
1867
+ T: 8 /*ScalarType.BOOL*/,
1868
+ },
1861
1869
  ]);
1862
1870
  }
1863
1871
  }
@@ -6660,7 +6668,7 @@ const getSdkVersion = (sdk) => {
6660
6668
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6661
6669
  };
6662
6670
 
6663
- const version = "1.53.2";
6671
+ const version = "1.54.1-beta.0";
6664
6672
  const [major, minor, patch] = version.split('.');
6665
6673
  let sdkInfo = {
6666
6674
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6804,7 +6812,7 @@ const getClientDetails = async () => {
6804
6812
  .join(' '),
6805
6813
  version: '',
6806
6814
  },
6807
- webrtcVersion: browserVersion,
6815
+ webrtcVersion: webRtcInfo?.version || '',
6808
6816
  };
6809
6817
  };
6810
6818
 
@@ -8448,7 +8456,7 @@ class Publisher extends BasePeerConnection {
8448
8456
  /**
8449
8457
  * Constructs a new `Publisher` instance.
8450
8458
  */
8451
- constructor(baseOptions, publishOptions) {
8459
+ constructor(baseOptions, publishOptions, opts = {}) {
8452
8460
  super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
8453
8461
  this.transceiverCache = new TransceiverCache();
8454
8462
  this.clonedTracks = new Set();
@@ -8869,6 +8877,7 @@ class Publisher extends BasePeerConnection {
8869
8877
  muted: !isTrackLive,
8870
8878
  codec: publishOption.codec,
8871
8879
  publishOptionId: publishOption.id,
8880
+ selfSubAudioVideo: this.selfSubEnabled,
8872
8881
  };
8873
8882
  };
8874
8883
  this.cloneTrack = (track) => {
@@ -8949,6 +8958,7 @@ class Publisher extends BasePeerConnection {
8949
8958
  });
8950
8959
  };
8951
8960
  this.publishOptions = publishOptions;
8961
+ this.selfSubEnabled = opts.selfSubEnabled ?? false;
8952
8962
  this.on('iceRestart', (iceRestart) => {
8953
8963
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
8954
8964
  return;
@@ -9030,6 +9040,13 @@ class Subscriber extends BasePeerConnection {
9030
9040
  */
9031
9041
  constructor(opts) {
9032
9042
  super(PeerType.SUBSCRIBER, opts);
9043
+ /**
9044
+ * Remote streams received from the SFU. For a self-sub case
9045
+ * we need to be able to distinguish between the local capture stream.
9046
+ * The map will never contain local streams so we can safely use it to
9047
+ * check if the stream is remote and dispose it when needed.
9048
+ */
9049
+ this.trackedStreams = new WeakSet();
9033
9050
  /**
9034
9051
  * Restarts the ICE connection and renegotiates with the SFU.
9035
9052
  */
@@ -9064,6 +9081,7 @@ class Subscriber extends BasePeerConnection {
9064
9081
  // example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
9065
9082
  const [trackId, rawTrackType] = primaryStream.id.split(':');
9066
9083
  const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9084
+ const isSelfSub = !!participantToUpdate?.isLocalParticipant;
9067
9085
  this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
9068
9086
  const trackType = toTrackType(rawTrackType);
9069
9087
  if (!trackType) {
@@ -9088,6 +9106,9 @@ class Subscriber extends BasePeerConnection {
9088
9106
  this.setRemoteTrackInterrupted(trackId, trackType, true);
9089
9107
  }
9090
9108
  this.trackIdToTrackType.set(track.id, trackType);
9109
+ if (isSelfSub) {
9110
+ this.trackedStreams.add(primaryStream);
9111
+ }
9091
9112
  if (!participantToUpdate) {
9092
9113
  this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
9093
9114
  this.state.registerOrphanedTrack({
@@ -9103,6 +9124,12 @@ class Subscriber extends BasePeerConnection {
9103
9124
  this.logger.error(`Unknown track type: ${rawTrackType}`);
9104
9125
  return;
9105
9126
  }
9127
+ // Self-sub loopback audio routes to the speaker by default, which
9128
+ // would echo the local user's voice. Default-mute here; consumers
9129
+ // (the loopback recording hook) re-enable explicitly when needed.
9130
+ if (isSelfSub && e.track.kind === 'audio') {
9131
+ e.track.enabled = false;
9132
+ }
9106
9133
  // get the previous stream to dispose it later
9107
9134
  // usually this happens during migration, when the stream is replaced
9108
9135
  // with a new one but the old one is still in the state
@@ -9111,8 +9138,12 @@ class Subscriber extends BasePeerConnection {
9111
9138
  this.state.updateParticipant(participantToUpdate.sessionId, {
9112
9139
  [streamKindProp]: primaryStream,
9113
9140
  });
9114
- // now, dispose the previous stream if it exists
9115
9141
  if (previousStream) {
9142
+ if (isSelfSub && !this.trackedStreams.has(previousStream)) {
9143
+ // this is the local capture stream, we don't want to dispose it
9144
+ this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
9145
+ return;
9146
+ }
9116
9147
  this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
9117
9148
  previousStream.getTracks().forEach((t) => {
9118
9149
  t.stop();
@@ -13935,6 +13966,7 @@ class Call {
13935
13966
  // maintain the order of publishing tracks to restore them after a reconnection
13936
13967
  // it shouldn't contain duplicates
13937
13968
  this.trackPublishOrder = [];
13969
+ this.selfSubEnabled = false;
13938
13970
  this.hasJoinedOnce = false;
13939
13971
  this.deviceSettingsAppliedOnce = false;
13940
13972
  this.initialized = false;
@@ -14284,6 +14316,30 @@ class Call {
14284
14316
  await Promise.all(stopOnLeavePromises);
14285
14317
  });
14286
14318
  };
14319
+ /**
14320
+ * The largest video publish dimension across the current publish options.
14321
+ *
14322
+ * @internal
14323
+ */
14324
+ this.getMaxVideoPublishDimension = () => {
14325
+ if (!this.currentPublishOptions)
14326
+ return undefined;
14327
+ let maxDimension;
14328
+ let maxArea = 0;
14329
+ for (const opt of this.currentPublishOptions) {
14330
+ if (opt.trackType !== TrackType.VIDEO)
14331
+ continue;
14332
+ const dim = opt.videoDimension;
14333
+ if (!dim || !dim.width || !dim.height)
14334
+ continue;
14335
+ const area = dim.width * dim.height;
14336
+ if (area > maxArea) {
14337
+ maxDimension = dim;
14338
+ maxArea = area;
14339
+ }
14340
+ }
14341
+ return maxDimension;
14342
+ };
14287
14343
  /**
14288
14344
  * Update from the call response from the "call.ring" event
14289
14345
  * @internal
@@ -14430,7 +14486,7 @@ class Call {
14430
14486
  *
14431
14487
  * @returns a promise which resolves once the call join-flow has finished.
14432
14488
  */
14433
- this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
14489
+ this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled = false, ...data } = {}) => {
14434
14490
  const callingState = this.state.callingState;
14435
14491
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
14436
14492
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -14438,6 +14494,9 @@ class Call {
14438
14494
  if (data?.ring) {
14439
14495
  this.ringingSubject.next(true);
14440
14496
  }
14497
+ // we need this to be set before the callingx.joinCall() is
14498
+ // called to avoid registering the test call in the CallKit/Telecom
14499
+ this.selfSubEnabled = selfSubEnabled;
14441
14500
  const callingX = globalThis.streamRNVideoSDK?.callingX;
14442
14501
  if (callingX) {
14443
14502
  // for Android/iOS, we need to start the call in the callingx library as soon as possible
@@ -14574,7 +14633,7 @@ class Call {
14574
14633
  ]);
14575
14634
  const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
14576
14635
  const reconnectDetails = isReconnecting
14577
- ? this.getReconnectDetails(data?.migrating_from, previousSessionId)
14636
+ ? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
14578
14637
  : undefined;
14579
14638
  const preferredPublishOptions = !isReconnecting
14580
14639
  ? this.getPreferredPublishOptions()
@@ -14827,7 +14886,9 @@ class Call {
14827
14886
  if (closePreviousInstances && this.publisher) {
14828
14887
  await this.publisher.dispose();
14829
14888
  }
14830
- this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14889
+ this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
14890
+ selfSubEnabled: this.selfSubEnabled,
14891
+ });
14831
14892
  }
14832
14893
  this.statsReporter?.stop();
14833
14894
  if (this.statsReportingIntervalInMs > 0) {
@@ -14943,6 +15004,7 @@ class Call {
14943
15004
  const reconnectStartTime = Date.now();
14944
15005
  this.reconnectStrategy = strategy;
14945
15006
  this.reconnectReason = reason;
15007
+ const sfuRejoinFailures = new Map();
14946
15008
  const markAsReconnectingFailed = async () => {
14947
15009
  try {
14948
15010
  // attempt to fetch the call data from the server, as the call
@@ -15001,7 +15063,8 @@ class Call {
15001
15063
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
15002
15064
  this.reconnectAttempts++;
15003
15065
  }
15004
- const currentStrategy = WebsocketReconnectStrategy[this.reconnectStrategy];
15066
+ const attemptedStrategy = this.reconnectStrategy;
15067
+ const currentStrategy = WebsocketReconnectStrategy[attemptedStrategy];
15005
15068
  try {
15006
15069
  // wait until the network is available
15007
15070
  await this.networkAvailableTask?.promise;
@@ -15014,9 +15077,24 @@ class Call {
15014
15077
  case WebsocketReconnectStrategy.FAST:
15015
15078
  await this.reconnectFast();
15016
15079
  break;
15017
- case WebsocketReconnectStrategy.REJOIN:
15018
- await this.reconnectRejoin();
15080
+ case WebsocketReconnectStrategy.REJOIN: {
15081
+ const confirmedBadSfus = Array.from(sfuRejoinFailures)
15082
+ .filter(([, failures]) => failures >= 2)
15083
+ .map(([sfu]) => sfu);
15084
+ if (this.joinCallData && confirmedBadSfus.length) {
15085
+ this.joinCallData.migrating_from =
15086
+ confirmedBadSfus[confirmedBadSfus.length - 1];
15087
+ this.joinCallData.migrating_from_list = confirmedBadSfus;
15088
+ }
15089
+ try {
15090
+ await this.reconnectRejoin();
15091
+ }
15092
+ finally {
15093
+ delete this.joinCallData?.migrating_from;
15094
+ delete this.joinCallData?.migrating_from_list;
15095
+ }
15019
15096
  break;
15097
+ }
15020
15098
  case WebsocketReconnectStrategy.MIGRATE:
15021
15099
  await this.reconnectMigrate();
15022
15100
  break;
@@ -15029,6 +15107,15 @@ class Call {
15029
15107
  break; // do-while loop, reconnection worked, exit the loop
15030
15108
  }
15031
15109
  catch (error) {
15110
+ if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
15111
+ const failedSfu = this.credentials?.server.edge_name;
15112
+ if (failedSfu) {
15113
+ const switchSfu = error instanceof SfuJoinError &&
15114
+ SfuJoinError.isJoinErrorCode(error.errorEvent);
15115
+ const failures = (sfuRejoinFailures.get(failedSfu) ?? 0) + 1;
15116
+ sfuRejoinFailures.set(failedSfu, switchSfu ? Math.max(failures, 2) : failures);
15117
+ }
15118
+ }
15032
15119
  if (this.state.callingState === exports.CallingState.OFFLINE) {
15033
15120
  this.logger.debug(`[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
15034
15121
  break;
@@ -15224,6 +15311,8 @@ class Call {
15224
15311
  this.state.setCallingState(exports.CallingState.OFFLINE);
15225
15312
  }
15226
15313
  else {
15314
+ if (!this.networkAvailableTask)
15315
+ return;
15227
15316
  this.logger.debug('[Reconnect] Going online');
15228
15317
  this.sfuClient?.close(StreamSfuClient.DISPOSE_OLD_SOCKET, 'Closing WS to reconnect after going online');
15229
15318
  // we went online, release the previous waiters and reset the state
@@ -16295,6 +16384,12 @@ class Call {
16295
16384
  get currentUserId() {
16296
16385
  return this.clientStore.connectedUser?.id;
16297
16386
  }
16387
+ /**
16388
+ * A flag indicating whether self-subscription is enabled for the call.
16389
+ */
16390
+ get isSelfSubEnabled() {
16391
+ return this.selfSubEnabled;
16392
+ }
16298
16393
  /**
16299
16394
  * A flag indicating whether the call was created by the current user.
16300
16395
  */
@@ -17482,11 +17577,11 @@ class StreamClient {
17482
17577
  return await this.wsConnection.connect(this.defaultWSTimeout);
17483
17578
  };
17484
17579
  this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
17485
- "1.53.2";
17580
+ "1.54.1-beta.0";
17486
17581
  this.getUserAgent = () => {
17487
17582
  if (!this.cachedUserAgent) {
17488
17583
  const { clientAppIdentifier = {} } = this.options;
17489
- const { sdkName = 'js', sdkVersion = "1.53.2", ...extras } = clientAppIdentifier;
17584
+ const { sdkName = 'js', sdkVersion = "1.54.1-beta.0", ...extras } = clientAppIdentifier;
17490
17585
  this.cachedUserAgent = [
17491
17586
  `stream-video-${sdkName}-v${sdkVersion}`,
17492
17587
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18164,6 +18259,8 @@ class ClientEventReporter {
18164
18259
  };
18165
18260
  };
18166
18261
  this.send = (body) => {
18262
+ if (!this.enabled)
18263
+ return;
18167
18264
  void this.sendWithRetry(body);
18168
18265
  };
18169
18266
  this.sendWithRetry = async (body) => {
@@ -18189,6 +18286,7 @@ class ClientEventReporter {
18189
18286
  return false;
18190
18287
  };
18191
18288
  this.streamClient = options.streamClient;
18289
+ this.enabled = options.enabled ?? true;
18192
18290
  }
18193
18291
  }
18194
18292
  const readPermissionStatus = (permission) => {
@@ -18669,6 +18767,7 @@ class StreamVideoClient {
18669
18767
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
18670
18768
  this.clientEventReporter = new ClientEventReporter({
18671
18769
  streamClient: this.streamClient,
18770
+ enabled: clientOptions?.clientEventsReportingEnabled ?? true,
18672
18771
  });
18673
18772
  this.writeableStateStore = new StreamVideoWriteableStateStore();
18674
18773
  this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);