@stream-io/video-client 1.11.3 → 1.11.5

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.11.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.4...@stream-io/video-client-1.11.5) (2024-11-22)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * unhandled promise rejections during reconnect ([#1585](https://github.com/GetStream/stream-video-js/issues/1585)) ([920c4ea](https://github.com/GetStream/stream-video-js/commit/920c4ea3b3f622430b35ac1bade74a6206ee17e5)), closes [/github.com/GetStream/stream-video-js/pull/1585/files#diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81](https://github.com/GetStream//github.com/GetStream/stream-video-js/pull/1585/files/issues/diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81)
11
+
12
+ ## [1.11.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.3...@stream-io/video-client-1.11.4) (2024-11-21)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * experimental option to force single codec preference in the SDP ([#1581](https://github.com/GetStream/stream-video-js/issues/1581)) ([894a86e](https://github.com/GetStream/stream-video-js/commit/894a86e407dc0dd36b7463bb964c86da0c3055d1))
18
+
5
19
  ## [1.11.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.2...@stream-io/video-client-1.11.3) (2024-11-20)
6
20
 
7
21
 
@@ -3190,13 +3190,6 @@ function convertErrorToJson(err) {
3190
3190
  }
3191
3191
  return jsonObj;
3192
3192
  }
3193
- /**
3194
- * Informs if a promise is yet to be resolved or rejected
3195
- */
3196
- async function isPromisePending(promise) {
3197
- const emptyObj = {};
3198
- return Promise.race([promise, emptyObj]).then((value) => (value === emptyObj ? true : false), () => false);
3199
- }
3200
3193
  /**
3201
3194
  * isOnline safely return the navigator.online value for browser env
3202
3195
  * if navigator is not in global object, it always return true
@@ -3335,7 +3328,7 @@ const retryable = async (rpc, signal) => {
3335
3328
  return result;
3336
3329
  };
3337
3330
 
3338
- const version = "1.11.3";
3331
+ const version = "1.11.5";
3339
3332
  const [major, minor, patch] = version.split('.');
3340
3333
  let sdkInfo = {
3341
3334
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -3470,7 +3463,7 @@ const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
3470
3463
  continue;
3471
3464
  }
3472
3465
  const sdpFmtpLine = codec.sdpFmtpLine;
3473
- if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42e01f')) {
3466
+ if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
3474
3467
  // this is not the baseline h264 codec, prioritize it lower
3475
3468
  partiallyPreferred.push(codec);
3476
3469
  continue;
@@ -5497,6 +5490,50 @@ const toggleDtx = (sdp, enable) => {
5497
5490
  : `${opusFmtp.original};${requiredDtxConfig}`;
5498
5491
  return sdp.replace(opusFmtp.original, newFmtp);
5499
5492
  };
5493
+ /**
5494
+ * Returns and SDP with all the codecs except the given codec removed.
5495
+ */
5496
+ const preserveCodec = (sdp, mid, codec) => {
5497
+ const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
5498
+ const toSet = (fmtpLine) => new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
5499
+ const equal = (a, b) => {
5500
+ if (a.size !== b.size)
5501
+ return false;
5502
+ for (const item of a)
5503
+ if (!b.has(item))
5504
+ return false;
5505
+ return true;
5506
+ };
5507
+ const codecFmtp = toSet(codec.sdpFmtpLine || '');
5508
+ const parsedSdp = SDP.parse(sdp);
5509
+ for (const media of parsedSdp.media) {
5510
+ if (media.type !== kind || String(media.mid) !== mid)
5511
+ continue;
5512
+ // find the payload id of the desired codec
5513
+ const payloads = new Set();
5514
+ for (const rtp of media.rtp) {
5515
+ if (rtp.codec.toLowerCase() === codecName &&
5516
+ media.fmtp.some((f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp))) {
5517
+ payloads.add(rtp.payload);
5518
+ }
5519
+ }
5520
+ // find the corresponding rtx codec by matching apt=<preserved-codec-payload>
5521
+ for (const fmtp of media.fmtp) {
5522
+ const match = fmtp.config.match(/(apt)=(\d+)/);
5523
+ if (!match)
5524
+ continue;
5525
+ const [, , preservedCodecPayload] = match;
5526
+ if (payloads.has(Number(preservedCodecPayload))) {
5527
+ payloads.add(fmtp.payload);
5528
+ }
5529
+ }
5530
+ media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
5531
+ media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
5532
+ media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
5533
+ media.payloads = Array.from(payloads).join(' ');
5534
+ }
5535
+ return SDP.write(parsedSdp);
5536
+ };
5500
5537
  /**
5501
5538
  * Enables high-quality audio through SDP munging for the given trackMid.
5502
5539
  *
@@ -5900,6 +5937,12 @@ class Publisher {
5900
5937
  if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
5901
5938
  offer.sdp = this.enableHighQualityAudio(offer.sdp);
5902
5939
  }
5940
+ if (this.isPublishing(TrackType.VIDEO)) {
5941
+ // Hotfix for platforms that don't respect the ordered codec list
5942
+ // (Firefox, Android, Linux, etc...).
5943
+ // We remove all the codecs from the SDP except the one we want to use.
5944
+ offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
5945
+ }
5903
5946
  }
5904
5947
  const trackInfos = this.getAnnouncedTracks(offer.sdp);
5905
5948
  if (trackInfos.length === 0) {
@@ -6061,6 +6104,22 @@ class Publisher {
6061
6104
  });
6062
6105
  });
6063
6106
  }
6107
+ removeUnpreferredCodecs(sdp, trackType) {
6108
+ const opts = this.publishOptsForTrack.get(trackType);
6109
+ if (!opts || !opts.forceSingleCodec)
6110
+ return sdp;
6111
+ const codec = opts.forceCodec || opts.preferredCodec;
6112
+ const orderedCodecs = this.getCodecPreferences(trackType, codec);
6113
+ if (!orderedCodecs || orderedCodecs.length === 0)
6114
+ return sdp;
6115
+ const transceiver = this.transceiverCache.get(trackType);
6116
+ if (!transceiver)
6117
+ return sdp;
6118
+ const index = this.transceiverInitOrder.indexOf(trackType);
6119
+ const mid = extractMid(transceiver, index, sdp);
6120
+ const [codecToPreserve] = orderedCodecs;
6121
+ return preserveCodec(sdp, mid, codecToPreserve);
6122
+ }
6064
6123
  }
6065
6124
 
6066
6125
  /**
@@ -11503,6 +11562,33 @@ function buildWsSuccessAfterFailureInsight(connection) {
11503
11562
  return buildWsBaseInsight(connection);
11504
11563
  }
11505
11564
 
11565
+ /**
11566
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
11567
+ * since rejecting the promise causes an unhandled rejection error (even if the
11568
+ * rejection is handled everywhere promise result is expected).
11569
+ *
11570
+ * To avoid that, we add both resolution and rejection handlers to the promise.
11571
+ * That way, the saved promise never rejects. A callback is provided as return
11572
+ * value to build a *new* promise, that resolves and rejects along with
11573
+ * the original promise.
11574
+ * @param promise Promise to wrap, which possibly rejects
11575
+ * @returns Callback to build a new promise, which resolves and rejects along
11576
+ * with the original promise
11577
+ */
11578
+ function makeSafePromise(promise) {
11579
+ let isPending = true;
11580
+ const safePromise = promise
11581
+ .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
11582
+ .finally(() => (isPending = false));
11583
+ const unwrapPromise = () => safePromise.then((fulfillment) => {
11584
+ if (fulfillment.status === 'rejected')
11585
+ throw fulfillment.error;
11586
+ return fulfillment.result;
11587
+ });
11588
+ unwrapPromise.checkPending = () => isPending;
11589
+ return unwrapPromise;
11590
+ }
11591
+
11506
11592
  // Type guards to check WebSocket error type
11507
11593
  const isCloseEvent = (res) => res.code !== undefined;
11508
11594
  const isErrorEvent = (res) => res.error !== undefined;
@@ -11751,10 +11837,10 @@ class StableWSConnection {
11751
11837
  this._setupConnectionPromise = () => {
11752
11838
  this.isResolved = false;
11753
11839
  /** a promise that is resolved once ws.open is called */
11754
- this.connectionOpen = new Promise((resolve, reject) => {
11840
+ this.connectionOpenSafe = makeSafePromise(new Promise((resolve, reject) => {
11755
11841
  this.resolvePromise = resolve;
11756
11842
  this.rejectPromise = reject;
11757
- });
11843
+ }));
11758
11844
  };
11759
11845
  /**
11760
11846
  * Schedules a next health check ping for websocket.
@@ -11975,13 +12061,7 @@ class StableWSConnection {
11975
12061
  this._log(`_connect() - tokenProvider failed before, so going to retry`);
11976
12062
  await this.client.tokenManager.loadToken();
11977
12063
  }
11978
- let mustSetupConnectionIdPromise = true;
11979
- if (this.client.connectionIdPromise) {
11980
- if (await isPromisePending(this.client.connectionIdPromise)) {
11981
- mustSetupConnectionIdPromise = false;
11982
- }
11983
- }
11984
- if (mustSetupConnectionIdPromise) {
12064
+ if (!this.client.isConnectionIsPromisePending) {
11985
12065
  this.client._setupConnectionIdPromise();
11986
12066
  }
11987
12067
  this._setupConnectionPromise();
@@ -12101,6 +12181,9 @@ class StableWSConnection {
12101
12181
  // we don't care
12102
12182
  }
12103
12183
  }
12184
+ get connectionOpen() {
12185
+ return this.connectionOpenSafe?.();
12186
+ }
12104
12187
  }
12105
12188
 
12106
12189
  function isString(arrayOrString) {
@@ -12664,19 +12747,21 @@ class StreamClient {
12664
12747
  if (!this.userID) {
12665
12748
  throw Error('UserWithId is not set on client, use client.connectUser or client.connectAnonymousUser instead');
12666
12749
  }
12667
- if (this.wsConnection?.isConnecting && this.wsPromise) {
12750
+ const wsPromise = this.wsPromiseSafe?.();
12751
+ if (this.wsConnection?.isConnecting && wsPromise) {
12668
12752
  this.logger('info', 'client:openConnection() - connection already in progress');
12669
- return this.wsPromise;
12753
+ return await wsPromise;
12670
12754
  }
12671
12755
  if ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) &&
12672
12756
  this._hasConnectionID()) {
12673
12757
  this.logger('info', 'client:openConnection() - openConnection called twice, healthy connection already exists');
12674
- return Promise.resolve();
12758
+ return;
12675
12759
  }
12676
12760
  this._setupConnectionIdPromise();
12677
12761
  this.clientID = `${this.userID}--${randomId()}`;
12678
- this.wsPromise = this.connect();
12679
- return this.wsPromise;
12762
+ const newWsPromise = this.connect();
12763
+ this.wsPromiseSafe = makeSafePromise(newWsPromise);
12764
+ return await newWsPromise;
12680
12765
  };
12681
12766
  /**
12682
12767
  * Disconnects the websocket and removes the user from client.
@@ -12694,7 +12779,7 @@ class StreamClient {
12694
12779
  await this.closeConnection(timeout);
12695
12780
  removeConnectionEventListeners(this.updateNetworkConnectionStatus);
12696
12781
  this.tokenManager.reset();
12697
- this.connectionIdPromise = undefined;
12782
+ this.connectionIdPromiseSafe = undefined;
12698
12783
  this.rejectConnectionId = undefined;
12699
12784
  this.resolveConnectionId = undefined;
12700
12785
  };
@@ -12755,12 +12840,12 @@ class StreamClient {
12755
12840
  /**
12756
12841
  * sets up the this.connectionIdPromise
12757
12842
  */
12758
- this._setupConnectionIdPromise = async () => {
12843
+ this._setupConnectionIdPromise = () => {
12759
12844
  /** a promise that is resolved once connection id is set */
12760
- this.connectionIdPromise = new Promise((resolve, reject) => {
12845
+ this.connectionIdPromiseSafe = makeSafePromise(new Promise((resolve, reject) => {
12761
12846
  this.resolveConnectionId = resolve;
12762
12847
  this.rejectConnectionId = reject;
12763
- });
12848
+ }));
12764
12849
  };
12765
12850
  this._logApiRequest = (type, url, data, config) => {
12766
12851
  this.logger('trace', `client: ${type} - Request - ${url}`, {
@@ -12978,7 +13063,7 @@ class StreamClient {
12978
13063
  });
12979
13064
  };
12980
13065
  this.getUserAgent = () => {
12981
- const version = "1.11.3";
13066
+ const version = "1.11.5";
12982
13067
  return (this.userAgent ||
12983
13068
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12984
13069
  };
@@ -13092,7 +13177,7 @@ class StreamClient {
13092
13177
  });
13093
13178
  // WS connection is initialized when setUser is called
13094
13179
  this.wsConnection = null;
13095
- this.wsPromise = null;
13180
+ this.wsPromiseSafe = null;
13096
13181
  this.setUserPromise = null;
13097
13182
  // mapping between channel groups and configs
13098
13183
  this.anonymous = false;
@@ -13109,6 +13194,15 @@ class StreamClient {
13109
13194
  ? inputOptions.logger
13110
13195
  : () => null;
13111
13196
  }
13197
+ get connectionIdPromise() {
13198
+ return this.connectionIdPromiseSafe?.();
13199
+ }
13200
+ get isConnectionIsPromisePending() {
13201
+ return this.connectionIdPromiseSafe?.checkPending() ?? false;
13202
+ }
13203
+ get wsPromise() {
13204
+ return this.wsPromiseSafe?.();
13205
+ }
13112
13206
  }
13113
13207
 
13114
13208
  /**
@@ -13118,6 +13212,7 @@ class StreamVideoClient {
13118
13212
  constructor(apiKeyOrArgs, opts) {
13119
13213
  this.logLevel = 'warn';
13120
13214
  this.eventHandlersToUnregister = [];
13215
+ this.connectionConcurrencyTag = Symbol('connectionConcurrencyTag');
13121
13216
  /**
13122
13217
  * Connects the given user to the client.
13123
13218
  * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
@@ -13139,11 +13234,7 @@ class StreamVideoClient {
13139
13234
  return this.streamClient.connectGuestUser(user);
13140
13235
  };
13141
13236
  }
13142
- this.connectionPromise = this.disconnectionPromise
13143
- ? this.disconnectionPromise.then(() => connectUser())
13144
- : connectUser();
13145
- this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
13146
- const connectUserResponse = await this.connectionPromise;
13237
+ const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, () => connectUser());
13147
13238
  // connectUserResponse will be void if connectUser called twice for the same user
13148
13239
  if (connectUserResponse?.me) {
13149
13240
  this.writeableStateStore.setConnectedUser(connectUserResponse.me);
@@ -13220,17 +13311,13 @@ class StreamVideoClient {
13220
13311
  * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
13221
13312
  */
13222
13313
  this.disconnectUser = async (timeout) => {
13223
- if (!this.streamClient.user && !this.connectionPromise) {
13314
+ if (!this.streamClient.user) {
13224
13315
  return;
13225
13316
  }
13226
13317
  const userId = this.streamClient.user?.id;
13227
13318
  const apiKey = this.streamClient.key;
13228
13319
  const disconnectUser = () => this.streamClient.disconnectUser(timeout);
13229
- this.disconnectionPromise = this.connectionPromise
13230
- ? this.connectionPromise.then(() => disconnectUser())
13231
- : disconnectUser();
13232
- this.disconnectionPromise.finally(() => (this.disconnectionPromise = undefined));
13233
- await this.disconnectionPromise;
13320
+ await withoutConcurrency(this.connectionConcurrencyTag, () => disconnectUser());
13234
13321
  if (userId) {
13235
13322
  StreamVideoClient._instanceMap.delete(apiKey + userId);
13236
13323
  }
@@ -13405,11 +13492,7 @@ class StreamVideoClient {
13405
13492
  */
13406
13493
  this.connectAnonymousUser = async (user, tokenOrProvider) => {
13407
13494
  const connectAnonymousUser = () => this.streamClient.connectAnonymousUser(user, tokenOrProvider);
13408
- this.connectionPromise = this.disconnectionPromise
13409
- ? this.disconnectionPromise.then(() => connectAnonymousUser())
13410
- : connectAnonymousUser();
13411
- this.connectionPromise.finally(() => (this.connectionPromise = undefined));
13412
- return this.connectionPromise;
13495
+ return await withoutConcurrency(this.connectionConcurrencyTag, () => connectAnonymousUser());
13413
13496
  };
13414
13497
  let logger = logToConsole;
13415
13498
  let logLevel = 'warn';