@stream-io/video-client 1.19.3 → 1.20.1

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,21 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [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)
6
+
7
+ ### Bug Fixes
8
+
9
+ - 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))
10
+ - 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))
11
+
12
+ ## [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)
13
+
14
+ - bump test timeout ([7d922ed](https://github.com/GetStream/stream-video-js/commit/7d922ed34c46851a257fb36ee644f1ff5e4cb917))
15
+
16
+ ### Features
17
+
18
+ - add getCallReport method ([#1767](https://github.com/GetStream/stream-video-js/issues/1767)) ([12e064f](https://github.com/GetStream/stream-video-js/commit/12e064f34a08731ded289651125bbe20e2bbf4f4))
19
+
5
20
  ## [1.19.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.19.2...@stream-io/video-client-1.19.3) (2025-04-15)
6
21
 
7
22
  ### Bug Fixes
@@ -5643,7 +5643,7 @@ const aggregate = (stats) => {
5643
5643
  return report;
5644
5644
  };
5645
5645
 
5646
- const version = "1.19.3";
5646
+ const version = "1.20.1";
5647
5647
  const [major, minor, patch] = version.split('.');
5648
5648
  let sdkInfo = {
5649
5649
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5784,9 +5784,9 @@ class SfuStatsReporter {
5784
5784
  this.logger = getLogger(['SfuStatsReporter']);
5785
5785
  this.inputDevices = new Map();
5786
5786
  this.observeDevice = (device, kind) => {
5787
- const { hasBrowserPermission$ } = device.state;
5787
+ const { browserPermissionState$ } = device.state;
5788
5788
  this.unsubscribeDevicePermissionsSubscription?.();
5789
- this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([hasBrowserPermission$, this.state.ownCapabilities$]), ([hasPermission, ownCapabilities]) => {
5789
+ this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([browserPermissionState$, this.state.ownCapabilities$]), ([browserPermissionState, ownCapabilities]) => {
5790
5790
  // cleanup the previous listDevices() subscription in case
5791
5791
  // permissions or capabilities have changed.
5792
5792
  // we will subscribe again if everything is in order.
@@ -5794,7 +5794,7 @@ class SfuStatsReporter {
5794
5794
  const hasCapability = kind === 'mic'
5795
5795
  ? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
5796
5796
  : ownCapabilities.includes(OwnCapability.SEND_VIDEO);
5797
- if (!hasPermission || !hasCapability) {
5797
+ if (browserPermissionState !== 'granted' || !hasCapability) {
5798
5798
  this.inputDevices.set(kind, {
5799
5799
  currentDevice: '',
5800
5800
  availableDevices: [],
@@ -8728,6 +8728,9 @@ class BrowserPermission {
8728
8728
  // Instead of checking if a permission is granted, we check if it isn't denied
8729
8729
  map((state) => state !== 'denied'));
8730
8730
  }
8731
+ asStateObservable() {
8732
+ return this.getStateObservable();
8733
+ }
8731
8734
  getIsPromptingObservable() {
8732
8735
  return this.getStateObservable().pipe(map((state) => state === 'prompting'));
8733
8736
  }
@@ -9254,122 +9257,130 @@ class InputMediaDeviceManager {
9254
9257
  this.logger('debug', 'Starting stream');
9255
9258
  let stream;
9256
9259
  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
9260
+ try {
9261
+ if (this.state.mediaStream &&
9262
+ this.getTracks().every((t) => t.readyState === 'live')) {
9263
+ stream = this.state.mediaStream;
9264
+ this.enableTracks();
9265
+ }
9266
+ else {
9267
+ const defaultConstraints = this.state.defaultConstraints;
9268
+ const constraints = {
9269
+ ...defaultConstraints,
9270
+ deviceId: this.state.selectedDevice
9271
+ ? { exact: this.state.selectedDevice }
9272
+ : undefined,
9273
+ };
9274
+ /**
9275
+ * Chains two media streams together.
9276
+ *
9277
+ * In our case, filters MediaStreams are derived from their parent MediaStream.
9278
+ * However, once a child filter's track is stopped,
9279
+ * the tracks of the parent MediaStream aren't automatically stopped.
9280
+ * This leads to a situation where the camera indicator light is still on
9281
+ * even though the user stopped publishing video.
9282
+ *
9283
+ * This function works around this issue by stopping the parent MediaStream's tracks
9284
+ * as well once the child filter's tracks are stopped.
9285
+ *
9286
+ * It works by patching the stop() method of the child filter's tracks to also stop
9287
+ * the parent MediaStream's tracks of the same type. Here we assume that
9288
+ * the parent MediaStream has only one track of each type.
9289
+ *
9290
+ * @param parentStream the parent MediaStream. Omit for the root stream.
9291
+ */
9292
+ const chainWith = (parentStream) => async (filterStream) => {
9293
+ if (!parentStream)
9294
+ return filterStream;
9295
+ // TODO OL: take care of track.enabled property as well
9296
+ const parent = await parentStream;
9297
+ filterStream.getTracks().forEach((track) => {
9298
+ const originalStop = track.stop;
9299
+ track.stop = function stop() {
9300
+ originalStop.call(track);
9301
+ parent.getTracks().forEach((parentTrack) => {
9302
+ if (parentTrack.kind === track.kind) {
9303
+ parentTrack.stop();
9304
+ }
9305
+ });
9306
+ };
9307
+ });
9308
+ parent.getTracks().forEach((parentTrack) => {
9309
+ // When the parent stream abruptly ends, we propagate the event
9310
+ // to the filter stream.
9311
+ // This usually happens when the camera/microphone permissions
9312
+ // are revoked or when the device is disconnected.
9313
+ const handleParentTrackEnded = () => {
9314
+ filterStream.getTracks().forEach((track) => {
9315
+ if (parentTrack.kind !== track.kind)
9316
+ return;
9317
+ track.stop();
9318
+ track.dispatchEvent(new Event('ended')); // propagate the event
9319
+ });
9320
+ };
9321
+ parentTrack.addEventListener('ended', handleParentTrackEnded);
9322
+ this.subscriptions.push(() => {
9323
+ parentTrack.removeEventListener('ended', handleParentTrackEnded);
9315
9324
  });
9316
- };
9317
- parentTrack.addEventListener('ended', handleParentTrackEnded);
9325
+ });
9326
+ return filterStream;
9327
+ };
9328
+ // the rootStream represents the stream coming from the actual device
9329
+ // e.g. camera or microphone stream
9330
+ rootStream = this.getStream(constraints);
9331
+ // we publish the last MediaStream of the chain
9332
+ stream = await this.filters.reduce((parent, entry) => parent
9333
+ .then((inputStream) => {
9334
+ const { stop, output } = entry.start(inputStream);
9335
+ entry.stop = stop;
9336
+ return output;
9337
+ })
9338
+ .then(chainWith(parent), (error) => {
9339
+ this.logger('warn', 'Filter failed to start and will be ignored', error);
9340
+ return parent;
9341
+ }), rootStream);
9342
+ }
9343
+ if (this.call.state.callingState === CallingState.JOINED) {
9344
+ await this.publishStream(stream);
9345
+ }
9346
+ if (this.state.mediaStream !== stream) {
9347
+ this.state.setMediaStream(stream, await rootStream);
9348
+ const handleTrackEnded = async () => {
9349
+ await this.statusChangeSettled();
9350
+ if (this.enabled) {
9351
+ this.isTrackStoppedDueToTrackEnd = true;
9352
+ setTimeout(() => {
9353
+ this.isTrackStoppedDueToTrackEnd = false;
9354
+ }, 2000);
9355
+ await this.disable();
9356
+ }
9357
+ };
9358
+ const createTrackMuteHandler = (muted) => () => {
9359
+ if (!isMobile() || this.trackType !== TrackType.VIDEO)
9360
+ return;
9361
+ this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
9362
+ this.logger('warn', 'Error while notifying track mute state', err);
9363
+ });
9364
+ };
9365
+ stream.getTracks().forEach((track) => {
9366
+ const muteHandler = createTrackMuteHandler(true);
9367
+ const unmuteHandler = createTrackMuteHandler(false);
9368
+ track.addEventListener('mute', muteHandler);
9369
+ track.addEventListener('unmute', unmuteHandler);
9370
+ track.addEventListener('ended', handleTrackEnded);
9318
9371
  this.subscriptions.push(() => {
9319
- parentTrack.removeEventListener('ended', handleParentTrackEnded);
9372
+ track.removeEventListener('mute', muteHandler);
9373
+ track.removeEventListener('unmute', unmuteHandler);
9374
+ track.removeEventListener('ended', handleTrackEnded);
9320
9375
  });
9321
9376
  });
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);
9377
+ }
9341
9378
  }
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
- });
9379
+ catch (err) {
9380
+ if (rootStream) {
9381
+ disposeOfMediaStream(await rootStream);
9382
+ }
9383
+ throw err;
9373
9384
  }
9374
9385
  }
9375
9386
  get mediaDeviceKind() {
@@ -9491,6 +9502,9 @@ class InputMediaDeviceManagerState {
9491
9502
  this.hasBrowserPermission$ = permission
9492
9503
  ? permission.asObservable().pipe(shareReplay(1))
9493
9504
  : of(true);
9505
+ this.browserPermissionState$ = permission
9506
+ ? permission.asStateObservable().pipe(shareReplay(1))
9507
+ : of('prompt');
9494
9508
  this.isPromptingPermission$ = permission
9495
9509
  ? permission.getIsPromptingObservable().pipe(shareReplay(1))
9496
9510
  : of(false);
@@ -12124,11 +12138,26 @@ class Call {
12124
12138
  *
12125
12139
  * @param callSessionID the call session ID to retrieve statistics for.
12126
12140
  * @returns The call stats.
12141
+ * @deprecated use `call.getCallReport` instead.
12142
+ * @internal
12127
12143
  */
12128
12144
  this.getCallStats = async (callSessionID) => {
12129
12145
  const endpoint = `${this.streamClientBasePath}/stats/${callSessionID}`;
12130
12146
  return this.streamClient.get(endpoint);
12131
12147
  };
12148
+ /**
12149
+ * Retrieve call report. If the `callSessionID` is not specified, then the
12150
+ * report for the latest call session is retrieved. If it is specified, then
12151
+ * the report for that particular session is retrieved if it exists.
12152
+ *
12153
+ * @param callSessionID the optional call session ID to retrieve statistics for
12154
+ * @returns the call report
12155
+ */
12156
+ this.getCallReport = async (callSessionID = '') => {
12157
+ const endpoint = `${this.streamClientBasePath}/report`;
12158
+ const params = callSessionID !== '' ? { session_id: callSessionID } : {};
12159
+ return this.streamClient.get(endpoint, params);
12160
+ };
12132
12161
  /**
12133
12162
  * Submit user feedback for the call
12134
12163
  *
@@ -13443,7 +13472,7 @@ class StreamClient {
13443
13472
  this.getUserAgent = () => {
13444
13473
  if (!this.cachedUserAgent) {
13445
13474
  const { clientAppIdentifier = {} } = this.options;
13446
- const { sdkName = 'js', sdkVersion = "1.19.3", ...extras } = clientAppIdentifier;
13475
+ const { sdkName = 'js', sdkVersion = "1.20.1", ...extras } = clientAppIdentifier;
13447
13476
  this.cachedUserAgent = [
13448
13477
  `stream-video-${sdkName}-v${sdkVersion}`,
13449
13478
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),