@stream-io/video-client 1.12.2 → 1.12.4

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.
@@ -540,6 +540,10 @@ export declare class Call {
540
540
  * Applicable only for ringing calls.
541
541
  */
542
542
  private scheduleAutoDrop;
543
+ /**
544
+ * Cancels a scheduled auto-drop timeout.
545
+ */
546
+ private cancelAutoDrop;
543
547
  /**
544
548
  * Retrieves the list of recordings for the current call or call session.
545
549
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.12.2",
3
+ "version": "1.12.4",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -13,7 +13,7 @@
13
13
  "start": "rollup -w -c",
14
14
  "build": "yarn clean && rollup -c",
15
15
  "test": "vitest",
16
- "test-ci": "vitest --coverage",
16
+ "test-ci": "vitest run --coverage",
17
17
  "generate:open-api": "./generate-openapi.sh protocol",
18
18
  "generate:open-api:dev": "./generate-openapi.sh chat",
19
19
  "generate:timer-worker": "./generate-timer-worker.sh"
@@ -45,7 +45,7 @@
45
45
  "@stream-io/node-sdk": "^0.4.3",
46
46
  "@types/sdp-transform": "^2.4.7",
47
47
  "@types/ua-parser-js": "^0.7.37",
48
- "@vitest/coverage-v8": "^2.1.4",
48
+ "@vitest/coverage-v8": "^2.1.8",
49
49
  "dotenv": "^16.3.1",
50
50
  "happy-dom": "^11.0.2",
51
51
  "prettier": "^3.3.2",
@@ -53,7 +53,7 @@
53
53
  "rollup": "^4.22.0",
54
54
  "typescript": "^5.5.2",
55
55
  "vite": "^5.4.6",
56
- "vitest": "^2.1.4",
56
+ "vitest": "^2.1.8",
57
57
  "vitest-mock-extended": "^2.0.2"
58
58
  }
59
59
  }
package/src/Call.ts CHANGED
@@ -342,16 +342,18 @@ export class Call {
342
342
  );
343
343
 
344
344
  this.leaveCallHooks.add(
345
- // watch for auto drop cancellation
346
- createSubscription(this.state.callingState$, (callingState) => {
345
+ // cancel auto-drop when call is
346
+ createSubscription(this.state.session$, (session) => {
347
347
  if (!this.ringing) return;
348
- if (
349
- callingState === CallingState.JOINED ||
350
- callingState === CallingState.JOINING ||
351
- callingState === CallingState.LEFT
352
- ) {
353
- clearTimeout(this.dropTimeout);
354
- this.dropTimeout = undefined;
348
+
349
+ const receiverId = this.clientStore.connectedUser?.id;
350
+ if (!receiverId) return;
351
+
352
+ const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
353
+ const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
354
+
355
+ if (isAcceptedByMe || isRejectedByMe) {
356
+ this.cancelAutoDrop();
355
357
  }
356
358
  }),
357
359
  );
@@ -2004,28 +2006,36 @@ export class Call {
2004
2006
  * Applicable only for ringing calls.
2005
2007
  */
2006
2008
  private scheduleAutoDrop = () => {
2007
- clearTimeout(this.dropTimeout);
2008
- this.leaveCallHooks.add(
2009
- createSubscription(this.state.settings$, (settings) => {
2010
- if (!settings) return;
2011
- // ignore if the call is not ringing
2012
- if (this.state.callingState !== CallingState.RINGING) return;
2013
-
2014
- const timeoutInMs = this.isCreatedByMe
2015
- ? settings.ring.auto_cancel_timeout_ms
2016
- : settings.ring.incoming_call_timeout_ms;
2017
-
2018
- // 0 means no auto-drop
2019
- if (timeoutInMs <= 0) return;
2009
+ this.cancelAutoDrop();
2010
+
2011
+ const settings = this.state.settings;
2012
+ if (!settings) return;
2013
+ // ignore if the call is not ringing
2014
+ if (this.state.callingState !== CallingState.RINGING) return;
2015
+
2016
+ const timeoutInMs = this.isCreatedByMe
2017
+ ? settings.ring.auto_cancel_timeout_ms
2018
+ : settings.ring.incoming_call_timeout_ms;
2019
+
2020
+ // 0 means no auto-drop
2021
+ if (timeoutInMs <= 0) return;
2022
+
2023
+ this.dropTimeout = setTimeout(() => {
2024
+ // the call might have stopped ringing by this point,
2025
+ // e.g. it was already accepted and joined
2026
+ if (this.state.callingState !== CallingState.RINGING) return;
2027
+ this.leave({ reject: true, reason: 'timeout' }).catch((err) => {
2028
+ this.logger('error', 'Failed to drop call', err);
2029
+ });
2030
+ }, timeoutInMs);
2031
+ };
2020
2032
 
2021
- clearTimeout(this.dropTimeout);
2022
- this.dropTimeout = setTimeout(() => {
2023
- this.leave({ reject: true, reason: 'timeout' }).catch((err) => {
2024
- this.logger('error', 'Failed to drop call', err);
2025
- });
2026
- }, timeoutInMs);
2027
- }),
2028
- );
2033
+ /**
2034
+ * Cancels a scheduled auto-drop timeout.
2035
+ */
2036
+ private cancelAutoDrop = () => {
2037
+ clearTimeout(this.dropTimeout);
2038
+ this.dropTimeout = undefined;
2029
2039
  };
2030
2040
 
2031
2041
  /**
@@ -73,9 +73,8 @@ export class BrowserPermission {
73
73
  const isGranted = this.state === 'granted';
74
74
 
75
75
  if (!isGranted && throwOnNotAllowed) {
76
- throw new DOMException(
76
+ throw new Error(
77
77
  'Permission was not granted previously, and prompting again is not allowed',
78
- 'NotAllowedError',
79
78
  );
80
79
  }
81
80
 
@@ -91,7 +90,12 @@ export class BrowserPermission {
91
90
  this.setState('granted');
92
91
  return true;
93
92
  } catch (e) {
94
- if (e instanceof DOMException && e.name === 'NotAllowedError') {
93
+ if (
94
+ e &&
95
+ typeof e === 'object' &&
96
+ 'name' in e &&
97
+ (e.name === 'NotAllowedError' || e.name === 'SecurityError')
98
+ ) {
95
99
  this.logger('info', 'Browser permission was not granted', {
96
100
  permission: this.permission,
97
101
  });
@@ -169,6 +169,17 @@ const getStream = async (constraints: MediaStreamConstraints) => {
169
169
  return stream;
170
170
  };
171
171
 
172
+ function isOverconstrainedError(error: unknown) {
173
+ return (
174
+ error &&
175
+ typeof error === 'object' &&
176
+ (('name' in error && error.name === 'OverconstrainedError') ||
177
+ ('message' in error &&
178
+ typeof error.message === 'string' &&
179
+ error.message.startsWith('OverconstrainedError')))
180
+ );
181
+ }
182
+
172
183
  /**
173
184
  * Returns an audio media stream that fulfills the given constraints.
174
185
  * If no constraints are provided, it uses the browser's default ones.
@@ -194,18 +205,14 @@ export const getAudioStream = async (
194
205
  });
195
206
  return await getStream(constraints);
196
207
  } catch (error) {
197
- if (
198
- error instanceof DOMException &&
199
- error.name === 'OverconstrainedError' &&
200
- trackConstraints?.deviceId
201
- ) {
202
- const { deviceId, ...relaxedContraints } = trackConstraints;
208
+ if (isOverconstrainedError(error) && trackConstraints?.deviceId) {
209
+ const { deviceId, ...relaxedConstraints } = trackConstraints;
203
210
  getLogger(['devices'])(
204
211
  'warn',
205
- 'Failed to get audio stream, will try again with relaxed contraints',
206
- { error, constraints, relaxedContraints },
212
+ 'Failed to get audio stream, will try again with relaxed constraints',
213
+ { error, constraints, relaxedConstraints },
207
214
  );
208
- return getAudioStream(relaxedContraints);
215
+ return getAudioStream(relaxedConstraints);
209
216
  }
210
217
 
211
218
  getLogger(['devices'])('error', 'Failed to get audio stream', {
@@ -240,18 +247,14 @@ export const getVideoStream = async (
240
247
  });
241
248
  return await getStream(constraints);
242
249
  } catch (error) {
243
- if (
244
- error instanceof DOMException &&
245
- error.name === 'OverconstrainedError' &&
246
- trackConstraints?.deviceId
247
- ) {
248
- const { deviceId, ...relaxedContraints } = trackConstraints;
250
+ if (isOverconstrainedError(error) && trackConstraints?.deviceId) {
251
+ const { deviceId, ...relaxedConstraints } = trackConstraints;
249
252
  getLogger(['devices'])(
250
253
  'warn',
251
- 'Failed to get video stream, will try again with relaxed contraints',
252
- { error, constraints, relaxedContraints },
254
+ 'Failed to get video stream, will try again with relaxed constraints',
255
+ { error, constraints, relaxedConstraints },
253
256
  );
254
- return getVideoStream(relaxedContraints);
257
+ return getVideoStream(relaxedConstraints);
255
258
  }
256
259
 
257
260
  getLogger(['devices'])('error', 'Failed to get video stream', {
@@ -371,11 +371,14 @@ export class DynascaleManager {
371
371
  });
372
372
  });
373
373
 
374
- let lastDimensions: string | undefined;
374
+ let lastDimensions: VideoDimension | undefined;
375
375
  const resizeObserver = boundParticipant.isLocalParticipant
376
376
  ? null
377
377
  : new ResizeObserver(() => {
378
- const currentDimensions = `${videoElement.clientWidth},${videoElement.clientHeight}`;
378
+ const currentDimensions = {
379
+ width: videoElement.clientWidth,
380
+ height: videoElement.clientHeight,
381
+ };
379
382
 
380
383
  // skip initial trigger
381
384
  if (!lastDimensions) {
@@ -384,13 +387,24 @@ export class DynascaleManager {
384
387
  }
385
388
 
386
389
  if (
387
- lastDimensions === currentDimensions ||
390
+ (lastDimensions.width === currentDimensions.width &&
391
+ lastDimensions.height === currentDimensions.height) ||
388
392
  viewportVisibilityState === VisibilityState.INVISIBLE
389
393
  ) {
390
394
  return;
391
395
  }
392
396
 
393
- requestTrackWithDimensions(DebounceType.SLOW, {
397
+ const relativeDelta = Math.max(
398
+ currentDimensions.width / lastDimensions.width,
399
+ currentDimensions.height / lastDimensions.height,
400
+ );
401
+ // Low quality video in an upscaled video element is very noticable.
402
+ // We try to upscale faster, and downscale slower. We also update debounce
403
+ // more if the size change is not significant, gurading against fast-firing
404
+ // resize events.
405
+ const debounceType =
406
+ relativeDelta > 1.2 ? DebounceType.IMMEDIATE : DebounceType.MEDIUM;
407
+ requestTrackWithDimensions(debounceType, {
394
408
  width: videoElement.clientWidth,
395
409
  height: videoElement.clientHeight,
396
410
  });
@@ -413,7 +427,7 @@ export class DynascaleManager {
413
427
  .subscribe((isPublishing) => {
414
428
  if (isPublishing) {
415
429
  // the participant just started to publish a track
416
- requestTrackWithDimensions(DebounceType.FAST, {
430
+ requestTrackWithDimensions(DebounceType.IMMEDIATE, {
417
431
  width: videoElement.clientWidth,
418
432
  height: videoElement.clientHeight,
419
433
  });