@stream-io/video-client 1.20.0 → 1.20.2

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.20.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.20.1...@stream-io/video-client-1.20.2) (2025-05-01)
6
+
7
+ ### Bug Fixes
8
+
9
+ - add options for 4K RTMP and Recording ([#1775](https://github.com/GetStream/stream-video-js/issues/1775)) ([c09213d](https://github.com/GetStream/stream-video-js/commit/c09213df5fc8a46f5a8c5c1ef18f07fd05e1d547))
10
+ - use timeout reason when auto-dropping calls (instead of decline) ([#1776](https://github.com/GetStream/stream-video-js/issues/1776)) ([a043148](https://github.com/GetStream/stream-video-js/commit/a04314814e728c3d05d53c8940e9c223fec18fcc))
11
+
12
+ ## [1.20.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.20.0...@stream-io/video-client-1.20.1) (2025-04-29)
13
+
14
+ ### Bug Fixes
15
+
16
+ - dispose media stream if it cannot be published ([#1771](https://github.com/GetStream/stream-video-js/issues/1771)) ([83fbfd7](https://github.com/GetStream/stream-video-js/commit/83fbfd7bb77bd9a06d6955e6b48bb8238e573f57))
17
+ - use more granular permission state for stats reporter ([#1774](https://github.com/GetStream/stream-video-js/issues/1774)) ([55afdfc](https://github.com/GetStream/stream-video-js/commit/55afdfcdac55fad25ba32978caf55a2f25f7580b))
18
+
5
19
  ## [1.20.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.19.3...@stream-io/video-client-1.20.0) (2025-04-24)
6
20
 
7
21
  - bump test timeout ([7d922ed](https://github.com/GetStream/stream-video-js/commit/7d922ed34c46851a257fb36ee644f1ff5e4cb917))
@@ -117,6 +117,7 @@ const FrameRecordingSettingsRequestQualityEnum = {
117
117
  _720P: '720p',
118
118
  _1080P: '1080p',
119
119
  _1440P: '1440p',
120
+ _2160P: '2160p',
120
121
  };
121
122
  /**
122
123
  * @export
@@ -189,11 +190,13 @@ const RTMPBroadcastRequestQualityEnum = {
189
190
  _720P: '720p',
190
191
  _1080P: '1080p',
191
192
  _1440P: '1440p',
193
+ _2160P: '2160p',
192
194
  PORTRAIT_360X640: 'portrait-360x640',
193
195
  PORTRAIT_480X854: 'portrait-480x854',
194
196
  PORTRAIT_720X1280: 'portrait-720x1280',
195
197
  PORTRAIT_1080X1920: 'portrait-1080x1920',
196
198
  PORTRAIT_1440X2560: 'portrait-1440x2560',
199
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
197
200
  };
198
201
  /**
199
202
  * @export
@@ -204,11 +207,13 @@ const RTMPSettingsRequestQualityEnum = {
204
207
  _720P: '720p',
205
208
  _1080P: '1080p',
206
209
  _1440P: '1440p',
210
+ _2160P: '2160p',
207
211
  PORTRAIT_360X640: 'portrait-360x640',
208
212
  PORTRAIT_480X854: 'portrait-480x854',
209
213
  PORTRAIT_720X1280: 'portrait-720x1280',
210
214
  PORTRAIT_1080X1920: 'portrait-1080x1920',
211
215
  PORTRAIT_1440X2560: 'portrait-1440x2560',
216
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
212
217
  };
213
218
  /**
214
219
  * @export
@@ -227,11 +232,13 @@ const RecordSettingsRequestQualityEnum = {
227
232
  _720P: '720p',
228
233
  _1080P: '1080p',
229
234
  _1440P: '1440p',
235
+ _2160P: '2160p',
230
236
  PORTRAIT_360X640: 'portrait-360x640',
231
237
  PORTRAIT_480X854: 'portrait-480x854',
232
238
  PORTRAIT_720X1280: 'portrait-720x1280',
233
239
  PORTRAIT_1080X1920: 'portrait-1080x1920',
234
240
  PORTRAIT_1440X2560: 'portrait-1440x2560',
241
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
235
242
  };
236
243
  /**
237
244
  * @export
@@ -4058,7 +4065,7 @@ class StreamVideoWriteableStateStore {
4058
4065
  continue;
4059
4066
  logger('info', `User disconnected, leaving call: ${call.cid}`);
4060
4067
  await call
4061
- .leave({ reason: 'client.disconnectUser() called' })
4068
+ .leave({ message: 'client.disconnectUser() called' })
4062
4069
  .catch((err) => {
4063
4070
  logger('error', `Error leaving call: ${call.cid}`, err);
4064
4071
  });
@@ -5643,7 +5650,7 @@ const aggregate = (stats) => {
5643
5650
  return report;
5644
5651
  };
5645
5652
 
5646
- const version = "1.20.0";
5653
+ const version = "1.20.2";
5647
5654
  const [major, minor, patch] = version.split('.');
5648
5655
  let sdkInfo = {
5649
5656
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5784,9 +5791,9 @@ class SfuStatsReporter {
5784
5791
  this.logger = getLogger(['SfuStatsReporter']);
5785
5792
  this.inputDevices = new Map();
5786
5793
  this.observeDevice = (device, kind) => {
5787
- const { hasBrowserPermission$ } = device.state;
5794
+ const { browserPermissionState$ } = device.state;
5788
5795
  this.unsubscribeDevicePermissionsSubscription?.();
5789
- this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([hasBrowserPermission$, this.state.ownCapabilities$]), ([hasPermission, ownCapabilities]) => {
5796
+ this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([browserPermissionState$, this.state.ownCapabilities$]), ([browserPermissionState, ownCapabilities]) => {
5790
5797
  // cleanup the previous listDevices() subscription in case
5791
5798
  // permissions or capabilities have changed.
5792
5799
  // we will subscribe again if everything is in order.
@@ -5794,7 +5801,7 @@ class SfuStatsReporter {
5794
5801
  const hasCapability = kind === 'mic'
5795
5802
  ? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
5796
5803
  : ownCapabilities.includes(OwnCapability.SEND_VIDEO);
5797
- if (!hasPermission || !hasCapability) {
5804
+ if (browserPermissionState !== 'granted' || !hasCapability) {
5798
5805
  this.inputDevices.set(kind, {
5799
5806
  currentDevice: '',
5800
5807
  availableDevices: [],
@@ -7601,13 +7608,17 @@ const watchCallRejected = (call) => {
7601
7608
  .every((m) => rejectedBy[m.user_id]);
7602
7609
  if (everyoneElseRejected) {
7603
7610
  call.logger('info', 'everyone rejected, leaving the call');
7604
- await call.leave({ reason: 'ring: everyone rejected' });
7611
+ await call.leave({
7612
+ reject: true,
7613
+ reason: 'cancel',
7614
+ message: 'ring: everyone rejected',
7615
+ });
7605
7616
  }
7606
7617
  }
7607
7618
  else {
7608
7619
  if (rejectedBy[eventCall.created_by.id]) {
7609
7620
  call.logger('info', 'call creator rejected, leaving call');
7610
- await call.leave({ reason: 'ring: creator rejected' });
7621
+ await call.leave({ message: 'ring: creator rejected' });
7611
7622
  }
7612
7623
  }
7613
7624
  };
@@ -7621,7 +7632,7 @@ const watchCallEnded = (call) => {
7621
7632
  if (callingState !== CallingState.IDLE &&
7622
7633
  callingState !== CallingState.LEFT) {
7623
7634
  call
7624
- .leave({ reason: 'call.ended event received', reject: false })
7635
+ .leave({ message: 'call.ended event received', reject: false })
7625
7636
  .catch((err) => {
7626
7637
  call.logger('error', 'Failed to leave call after call.ended ', err);
7627
7638
  });
@@ -7641,7 +7652,7 @@ const watchSfuCallEnded = (call) => {
7641
7652
  // update the call state to reflect the call has ended.
7642
7653
  call.state.setEndedAt(new Date());
7643
7654
  const reason = CallEndedReason[e.reason];
7644
- await call.leave({ reason: `callEnded received: ${reason}` });
7655
+ await call.leave({ message: `callEnded received: ${reason}` });
7645
7656
  }
7646
7657
  catch (err) {
7647
7658
  call.logger('error', 'Failed to leave call after being ended by the SFU', err);
@@ -7708,7 +7719,7 @@ const watchLiveEnded = (dispatcher, call) => {
7708
7719
  return;
7709
7720
  call.state.setBackstage(true);
7710
7721
  if (!call.permissionsContext.hasPermission(OwnCapability.JOIN_BACKSTAGE)) {
7711
- call.leave({ reason: 'live ended' }).catch((err) => {
7722
+ call.leave({ message: 'live ended' }).catch((err) => {
7712
7723
  call.logger('error', 'Failed to leave call after live ended', err);
7713
7724
  });
7714
7725
  }
@@ -8728,6 +8739,9 @@ class BrowserPermission {
8728
8739
  // Instead of checking if a permission is granted, we check if it isn't denied
8729
8740
  map((state) => state !== 'denied'));
8730
8741
  }
8742
+ asStateObservable() {
8743
+ return this.getStateObservable();
8744
+ }
8731
8745
  getIsPromptingObservable() {
8732
8746
  return this.getStateObservable().pipe(map((state) => state === 'prompting'));
8733
8747
  }
@@ -9254,122 +9268,130 @@ class InputMediaDeviceManager {
9254
9268
  this.logger('debug', 'Starting stream');
9255
9269
  let stream;
9256
9270
  let rootStream;
9257
- if (this.state.mediaStream &&
9258
- this.getTracks().every((t) => t.readyState === 'live')) {
9259
- stream = this.state.mediaStream;
9260
- this.enableTracks();
9261
- }
9262
- else {
9263
- const defaultConstraints = this.state.defaultConstraints;
9264
- const constraints = {
9265
- ...defaultConstraints,
9266
- deviceId: this.state.selectedDevice
9267
- ? { exact: this.state.selectedDevice }
9268
- : undefined,
9269
- };
9270
- /**
9271
- * Chains two media streams together.
9272
- *
9273
- * In our case, filters MediaStreams are derived from their parent MediaStream.
9274
- * However, once a child filter's track is stopped,
9275
- * the tracks of the parent MediaStream aren't automatically stopped.
9276
- * This leads to a situation where the camera indicator light is still on
9277
- * even though the user stopped publishing video.
9278
- *
9279
- * This function works around this issue by stopping the parent MediaStream's tracks
9280
- * as well once the child filter's tracks are stopped.
9281
- *
9282
- * It works by patching the stop() method of the child filter's tracks to also stop
9283
- * the parent MediaStream's tracks of the same type. Here we assume that
9284
- * the parent MediaStream has only one track of each type.
9285
- *
9286
- * @param parentStream the parent MediaStream. Omit for the root stream.
9287
- */
9288
- const chainWith = (parentStream) => async (filterStream) => {
9289
- if (!parentStream)
9290
- return filterStream;
9291
- // TODO OL: take care of track.enabled property as well
9292
- const parent = await parentStream;
9293
- filterStream.getTracks().forEach((track) => {
9294
- const originalStop = track.stop;
9295
- track.stop = function stop() {
9296
- originalStop.call(track);
9297
- parent.getTracks().forEach((parentTrack) => {
9298
- if (parentTrack.kind === track.kind) {
9299
- parentTrack.stop();
9300
- }
9301
- });
9302
- };
9303
- });
9304
- parent.getTracks().forEach((parentTrack) => {
9305
- // When the parent stream abruptly ends, we propagate the event
9306
- // to the filter stream.
9307
- // This usually happens when the camera/microphone permissions
9308
- // are revoked or when the device is disconnected.
9309
- const handleParentTrackEnded = () => {
9310
- filterStream.getTracks().forEach((track) => {
9311
- if (parentTrack.kind !== track.kind)
9312
- return;
9313
- track.stop();
9314
- track.dispatchEvent(new Event('ended')); // propagate the event
9271
+ try {
9272
+ if (this.state.mediaStream &&
9273
+ this.getTracks().every((t) => t.readyState === 'live')) {
9274
+ stream = this.state.mediaStream;
9275
+ this.enableTracks();
9276
+ }
9277
+ else {
9278
+ const defaultConstraints = this.state.defaultConstraints;
9279
+ const constraints = {
9280
+ ...defaultConstraints,
9281
+ deviceId: this.state.selectedDevice
9282
+ ? { exact: this.state.selectedDevice }
9283
+ : undefined,
9284
+ };
9285
+ /**
9286
+ * Chains two media streams together.
9287
+ *
9288
+ * In our case, filters MediaStreams are derived from their parent MediaStream.
9289
+ * However, once a child filter's track is stopped,
9290
+ * the tracks of the parent MediaStream aren't automatically stopped.
9291
+ * This leads to a situation where the camera indicator light is still on
9292
+ * even though the user stopped publishing video.
9293
+ *
9294
+ * This function works around this issue by stopping the parent MediaStream's tracks
9295
+ * as well once the child filter's tracks are stopped.
9296
+ *
9297
+ * It works by patching the stop() method of the child filter's tracks to also stop
9298
+ * the parent MediaStream's tracks of the same type. Here we assume that
9299
+ * the parent MediaStream has only one track of each type.
9300
+ *
9301
+ * @param parentStream the parent MediaStream. Omit for the root stream.
9302
+ */
9303
+ const chainWith = (parentStream) => async (filterStream) => {
9304
+ if (!parentStream)
9305
+ return filterStream;
9306
+ // TODO OL: take care of track.enabled property as well
9307
+ const parent = await parentStream;
9308
+ filterStream.getTracks().forEach((track) => {
9309
+ const originalStop = track.stop;
9310
+ track.stop = function stop() {
9311
+ originalStop.call(track);
9312
+ parent.getTracks().forEach((parentTrack) => {
9313
+ if (parentTrack.kind === track.kind) {
9314
+ parentTrack.stop();
9315
+ }
9316
+ });
9317
+ };
9318
+ });
9319
+ parent.getTracks().forEach((parentTrack) => {
9320
+ // When the parent stream abruptly ends, we propagate the event
9321
+ // to the filter stream.
9322
+ // This usually happens when the camera/microphone permissions
9323
+ // are revoked or when the device is disconnected.
9324
+ const handleParentTrackEnded = () => {
9325
+ filterStream.getTracks().forEach((track) => {
9326
+ if (parentTrack.kind !== track.kind)
9327
+ return;
9328
+ track.stop();
9329
+ track.dispatchEvent(new Event('ended')); // propagate the event
9330
+ });
9331
+ };
9332
+ parentTrack.addEventListener('ended', handleParentTrackEnded);
9333
+ this.subscriptions.push(() => {
9334
+ parentTrack.removeEventListener('ended', handleParentTrackEnded);
9315
9335
  });
9316
- };
9317
- parentTrack.addEventListener('ended', handleParentTrackEnded);
9336
+ });
9337
+ return filterStream;
9338
+ };
9339
+ // the rootStream represents the stream coming from the actual device
9340
+ // e.g. camera or microphone stream
9341
+ rootStream = this.getStream(constraints);
9342
+ // we publish the last MediaStream of the chain
9343
+ stream = await this.filters.reduce((parent, entry) => parent
9344
+ .then((inputStream) => {
9345
+ const { stop, output } = entry.start(inputStream);
9346
+ entry.stop = stop;
9347
+ return output;
9348
+ })
9349
+ .then(chainWith(parent), (error) => {
9350
+ this.logger('warn', 'Filter failed to start and will be ignored', error);
9351
+ return parent;
9352
+ }), rootStream);
9353
+ }
9354
+ if (this.call.state.callingState === CallingState.JOINED) {
9355
+ await this.publishStream(stream);
9356
+ }
9357
+ if (this.state.mediaStream !== stream) {
9358
+ this.state.setMediaStream(stream, await rootStream);
9359
+ const handleTrackEnded = async () => {
9360
+ await this.statusChangeSettled();
9361
+ if (this.enabled) {
9362
+ this.isTrackStoppedDueToTrackEnd = true;
9363
+ setTimeout(() => {
9364
+ this.isTrackStoppedDueToTrackEnd = false;
9365
+ }, 2000);
9366
+ await this.disable();
9367
+ }
9368
+ };
9369
+ const createTrackMuteHandler = (muted) => () => {
9370
+ if (!isMobile() || this.trackType !== TrackType.VIDEO)
9371
+ return;
9372
+ this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
9373
+ this.logger('warn', 'Error while notifying track mute state', err);
9374
+ });
9375
+ };
9376
+ stream.getTracks().forEach((track) => {
9377
+ const muteHandler = createTrackMuteHandler(true);
9378
+ const unmuteHandler = createTrackMuteHandler(false);
9379
+ track.addEventListener('mute', muteHandler);
9380
+ track.addEventListener('unmute', unmuteHandler);
9381
+ track.addEventListener('ended', handleTrackEnded);
9318
9382
  this.subscriptions.push(() => {
9319
- parentTrack.removeEventListener('ended', handleParentTrackEnded);
9383
+ track.removeEventListener('mute', muteHandler);
9384
+ track.removeEventListener('unmute', unmuteHandler);
9385
+ track.removeEventListener('ended', handleTrackEnded);
9320
9386
  });
9321
9387
  });
9322
- return filterStream;
9323
- };
9324
- // the rootStream represents the stream coming from the actual device
9325
- // e.g. camera or microphone stream
9326
- rootStream = this.getStream(constraints);
9327
- // we publish the last MediaStream of the chain
9328
- stream = await this.filters.reduce((parent, entry) => parent
9329
- .then((inputStream) => {
9330
- const { stop, output } = entry.start(inputStream);
9331
- entry.stop = stop;
9332
- return output;
9333
- })
9334
- .then(chainWith(parent), (error) => {
9335
- this.logger('warn', 'Filter failed to start and will be ignored', error);
9336
- return parent;
9337
- }), rootStream);
9338
- }
9339
- if (this.call.state.callingState === CallingState.JOINED) {
9340
- await this.publishStream(stream);
9388
+ }
9341
9389
  }
9342
- if (this.state.mediaStream !== stream) {
9343
- this.state.setMediaStream(stream, await rootStream);
9344
- const handleTrackEnded = async () => {
9345
- await this.statusChangeSettled();
9346
- if (this.enabled) {
9347
- this.isTrackStoppedDueToTrackEnd = true;
9348
- setTimeout(() => {
9349
- this.isTrackStoppedDueToTrackEnd = false;
9350
- }, 2000);
9351
- await this.disable();
9352
- }
9353
- };
9354
- const createTrackMuteHandler = (muted) => () => {
9355
- if (!isMobile() || this.trackType !== TrackType.VIDEO)
9356
- return;
9357
- this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
9358
- this.logger('warn', 'Error while notifying track mute state', err);
9359
- });
9360
- };
9361
- stream.getTracks().forEach((track) => {
9362
- const muteHandler = createTrackMuteHandler(true);
9363
- const unmuteHandler = createTrackMuteHandler(false);
9364
- track.addEventListener('mute', muteHandler);
9365
- track.addEventListener('unmute', unmuteHandler);
9366
- track.addEventListener('ended', handleTrackEnded);
9367
- this.subscriptions.push(() => {
9368
- track.removeEventListener('mute', muteHandler);
9369
- track.removeEventListener('unmute', unmuteHandler);
9370
- track.removeEventListener('ended', handleTrackEnded);
9371
- });
9372
- });
9390
+ catch (err) {
9391
+ if (rootStream) {
9392
+ disposeOfMediaStream(await rootStream);
9393
+ }
9394
+ throw err;
9373
9395
  }
9374
9396
  }
9375
9397
  get mediaDeviceKind() {
@@ -9491,6 +9513,9 @@ class InputMediaDeviceManagerState {
9491
9513
  this.hasBrowserPermission$ = permission
9492
9514
  ? permission.asObservable().pipe(shareReplay(1))
9493
9515
  : of(true);
9516
+ this.browserPermissionState$ = permission
9517
+ ? permission.asStateObservable().pipe(shareReplay(1))
9518
+ : of('prompt');
9494
9519
  this.isPromptingPermission$ = permission
9495
9520
  ? permission.getIsPromptingObservable().pipe(shareReplay(1))
9496
9521
  : of(false);
@@ -10556,7 +10581,7 @@ class Call {
10556
10581
  const currentUserId = this.currentUserId;
10557
10582
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
10558
10583
  this.logger('info', 'Leaving call because of being blocked');
10559
- await this.leave({ reason: 'user blocked' }).catch((err) => {
10584
+ await this.leave({ message: 'user blocked' }).catch((err) => {
10560
10585
  this.logger('error', 'Error leaving call after being blocked', err);
10561
10586
  });
10562
10587
  }
@@ -10709,7 +10734,7 @@ class Call {
10709
10734
  /**
10710
10735
  * Leave the call and stop the media streams that were published by the call.
10711
10736
  */
10712
- this.leave = async ({ reject, reason = 'user is leaving the call', } = {}) => {
10737
+ this.leave = async ({ reject, reason, message } = {}) => {
10713
10738
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
10714
10739
  const callingState = this.state.callingState;
10715
10740
  if (callingState === CallingState.LEFT) {
@@ -10727,7 +10752,7 @@ class Call {
10727
10752
  }
10728
10753
  if (callingState === CallingState.RINGING && reject !== false) {
10729
10754
  if (reject) {
10730
- await this.reject('decline');
10755
+ await this.reject(reason ?? 'decline');
10731
10756
  }
10732
10757
  else {
10733
10758
  // if reject was undefined, we still have to cancel the call automatically
@@ -10746,7 +10771,7 @@ class Call {
10746
10771
  this.subscriber = undefined;
10747
10772
  this.publisher?.dispose();
10748
10773
  this.publisher = undefined;
10749
- await this.sfuClient?.leaveAndClose(reason);
10774
+ await this.sfuClient?.leaveAndClose(message ?? reason ?? 'user is leaving the call');
10750
10775
  this.sfuClient = undefined;
10751
10776
  this.dynascaleManager.setSfuClient(undefined);
10752
10777
  this.state.setCallingState(CallingState.LEFT);
@@ -11453,7 +11478,7 @@ class Call {
11453
11478
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
11454
11479
  return;
11455
11480
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
11456
- this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
11481
+ this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
11457
11482
  this.logger('warn', `Can't leave call after disconnect request`, err);
11458
11483
  });
11459
11484
  }
@@ -12083,7 +12108,11 @@ class Call {
12083
12108
  // e.g. it was already accepted and joined
12084
12109
  if (this.state.callingState !== CallingState.RINGING)
12085
12110
  return;
12086
- this.leave({ reject: true, reason: 'timeout' }).catch((err) => {
12111
+ this.leave({
12112
+ reject: true,
12113
+ reason: 'timeout',
12114
+ message: `ringing timeout - ${this.isCreatedByMe ? 'no one accepted' : `user didn't interact with incoming call screen`}`,
12115
+ }).catch((err) => {
12087
12116
  this.logger('error', 'Failed to drop call', err);
12088
12117
  });
12089
12118
  }, timeoutInMs);
@@ -13458,7 +13487,7 @@ class StreamClient {
13458
13487
  this.getUserAgent = () => {
13459
13488
  if (!this.cachedUserAgent) {
13460
13489
  const { clientAppIdentifier = {} } = this.options;
13461
- const { sdkName = 'js', sdkVersion = "1.20.0", ...extras } = clientAppIdentifier;
13490
+ const { sdkName = 'js', sdkVersion = "1.20.2", ...extras } = clientAppIdentifier;
13462
13491
  this.cachedUserAgent = [
13463
13492
  `stream-video-${sdkName}-v${sdkVersion}`,
13464
13493
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),