@stream-io/video-client 0.3.20 → 0.3.22

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/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.3.20";
1
+ export declare const version = "0.3.22";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -48,7 +48,6 @@
48
48
  "@rollup/plugin-replace": "^5.0.2",
49
49
  "@rollup/plugin-typescript": "^11.1.2",
50
50
  "@types/jsonwebtoken": "^9.0.1",
51
- "@types/rimraf": "^3.0.2",
52
51
  "@types/sdp-transform": "^2.4.6",
53
52
  "@types/ua-parser-js": "^0.7.36",
54
53
  "@types/ws": "^8.5.4",
@@ -56,7 +55,7 @@
56
55
  "dotenv": "^16.3.1",
57
56
  "happy-dom": "^11.0.2",
58
57
  "prettier": "^2.8.4",
59
- "rimraf": "^3.0.2",
58
+ "rimraf": "^5.0.1",
60
59
  "rollup": "^3.28.1",
61
60
  "typescript": "^4.9.5",
62
61
  "vite": "^4.4.9",
@@ -120,9 +120,9 @@ describe('call API', () => {
120
120
  response = await call.update({
121
121
  settings_override: {
122
122
  recording: {
123
- mode: RecordSettingsRequestModeEnum.AVAILABLE,
124
123
  audio_only: false,
125
124
  quality: RecordSettingsRequestQualityEnum._1080P,
125
+ mode: RecordSettingsRequestModeEnum.AUTO_ON,
126
126
  },
127
127
  },
128
128
  });
@@ -22,6 +22,7 @@ import {
22
22
  import { ViewportTracker } from './ViewportTracker';
23
23
  import { getLogger } from '../logger';
24
24
  import { getSdkInfo } from '../client-details';
25
+ import { isFirefox, isSafari } from './browsers';
25
26
 
26
27
  const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
27
28
  VideoTrackType,
@@ -152,6 +153,14 @@ export class DynascaleManager {
152
153
  debounceType: DebounceType,
153
154
  dimension: VideoDimension | undefined,
154
155
  ) => {
156
+ if (dimension && (dimension.width === 0 || dimension.height === 0)) {
157
+ // ignore 0x0 dimensions. this can happen when the video element
158
+ // is not visible (e.g., has display: none).
159
+ // we treat this as "unsubscription" as we don't want to keep
160
+ // consuming bandwidth for a video that is not visible on the screen.
161
+ this.logger('debug', `Ignoring 0x0 dimension`, boundParticipant);
162
+ dimension = undefined;
163
+ }
155
164
  this.call.updateSubscriptionsPartial(
156
165
  trackType,
157
166
  { [sessionId]: { dimension } },
@@ -171,6 +180,12 @@ export class DynascaleManager {
171
180
  shareReplay(1),
172
181
  );
173
182
 
183
+ /**
184
+ * Since the video elements are now being removed from the DOM (React SDK) upon
185
+ * visibility change, this subscription is not in use an stays here only for the
186
+ * plain JS integrations where integrators might choose not to remove the video
187
+ * elements from the DOM.
188
+ */
174
189
  // keep copy for resize observer handler
175
190
  let viewportVisibilityState: VisibilityState | undefined;
176
191
  const viewportVisibilityStateSubscription =
@@ -231,6 +246,8 @@ export class DynascaleManager {
231
246
  });
232
247
  resizeObserver?.observe(videoElement);
233
248
 
249
+ // element renders and gets bound - track subscription gets
250
+ // triggered first other ones get skipped on initial subscriptions
234
251
  const publishedTracksSubscription = boundParticipant.isLocalParticipant
235
252
  ? null
236
253
  : participant$
@@ -248,16 +265,24 @@ export class DynascaleManager {
248
265
  .subscribe((isPublishing) => {
249
266
  if (isPublishing) {
250
267
  // the participant just started to publish a track
251
- requestTrackWithDimensions(DebounceType.IMMEDIATE, {
268
+ requestTrackWithDimensions(DebounceType.FAST, {
252
269
  width: videoElement.clientWidth,
253
270
  height: videoElement.clientHeight,
254
271
  });
255
272
  } else {
256
273
  // the participant just stopped publishing a track
257
- requestTrackWithDimensions(DebounceType.IMMEDIATE, undefined);
274
+ requestTrackWithDimensions(DebounceType.FAST, undefined);
258
275
  }
259
276
  });
260
277
 
278
+ videoElement.autoplay = true;
279
+ videoElement.playsInline = true;
280
+
281
+ // explicitly marking the element as muted will allow autoplay to work
282
+ // without prior user interaction:
283
+ // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
284
+ videoElement.muted = true;
285
+
261
286
  const streamSubscription = participant$
262
287
  .pipe(
263
288
  distinctUntilKeyChanged(
@@ -268,25 +293,22 @@ export class DynascaleManager {
268
293
  const source =
269
294
  trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
270
295
  if (videoElement.srcObject === source) return;
271
- setTimeout(() => {
272
- videoElement.srcObject = source ?? null;
273
- if (videoElement.srcObject) {
296
+ videoElement.srcObject = source ?? null;
297
+ if (isSafari() || isFirefox()) {
298
+ setTimeout(() => {
299
+ videoElement.srcObject = source ?? null;
274
300
  videoElement.play().catch((e) => {
275
301
  this.logger('warn', `Failed to play stream`, e);
276
302
  });
277
- }
278
- }, 0);
303
+ // we add extra delay until we attempt to force-play
304
+ // the participant's media stream in Firefox and Safari,
305
+ // as they seem to have some timing issues
306
+ }, 25);
307
+ }
279
308
  });
280
- videoElement.playsInline = true;
281
- videoElement.autoplay = true;
282
-
283
- // explicitly marking the element as muted will allow autoplay to work
284
- // without prior user interaction:
285
- // https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
286
- videoElement.muted = true;
287
309
 
288
310
  return () => {
289
- requestTrackWithDimensions(DebounceType.IMMEDIATE, undefined);
311
+ requestTrackWithDimensions(DebounceType.FAST, undefined);
290
312
  viewportVisibilityStateSubscription?.unsubscribe();
291
313
  publishedTracksSubscription?.unsubscribe();
292
314
  streamSubscription.unsubscribe();
@@ -4,7 +4,7 @@
4
4
 
5
5
  import '../../rtc/__tests__/mocks/webrtc.mocks';
6
6
 
7
- import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
8
8
  import { DynascaleManager } from '../DynascaleManager';
9
9
  import { Call } from '../../Call';
10
10
  import { StreamClient } from '../../coordinator/connection/client';
@@ -195,7 +195,7 @@ describe('DynascaleManager', () => {
195
195
  expect(updateSubscription).toHaveBeenCalledWith(
196
196
  'videoTrack',
197
197
  { 'session-id': { dimension: undefined } },
198
- DebounceType.IMMEDIATE,
198
+ DebounceType.FAST,
199
199
  );
200
200
 
201
201
  call.state.updateParticipant('session-id', {
@@ -212,7 +212,7 @@ describe('DynascaleManager', () => {
212
212
  },
213
213
  },
214
214
  },
215
- DebounceType.IMMEDIATE,
215
+ DebounceType.FAST,
216
216
  );
217
217
 
218
218
  call.state.updateParticipant('session-id', {
@@ -222,7 +222,7 @@ describe('DynascaleManager', () => {
222
222
  expect(updateSubscription).toHaveBeenCalledWith(
223
223
  'videoTrack',
224
224
  { 'session-id': { dimension: undefined } },
225
- DebounceType.IMMEDIATE,
225
+ DebounceType.FAST,
226
226
  );
227
227
 
228
228
  cleanup?.();
@@ -230,7 +230,7 @@ describe('DynascaleManager', () => {
230
230
  expect(updateSubscription).toHaveBeenCalledWith(
231
231
  'videoTrack',
232
232
  { 'session-id': { dimension: undefined } },
233
- DebounceType.IMMEDIATE,
233
+ DebounceType.FAST,
234
234
  );
235
235
  });
236
236
 
@@ -271,7 +271,7 @@ describe('DynascaleManager', () => {
271
271
  },
272
272
  },
273
273
  },
274
- DebounceType.IMMEDIATE,
274
+ DebounceType.FAST,
275
275
  );
276
276
  expect(play).toHaveBeenCalled();
277
277
  expect(videoElement.srcObject).toBe(mediaStream);
@@ -281,7 +281,7 @@ describe('DynascaleManager', () => {
281
281
  expect(updateSubscription).toHaveBeenCalledWith(
282
282
  'videoTrack',
283
283
  { 'session-id': { dimension: undefined } },
284
- DebounceType.IMMEDIATE,
284
+ DebounceType.FAST,
285
285
  );
286
286
  });
287
287
 
@@ -308,7 +308,7 @@ describe('DynascaleManager', () => {
308
308
  expect(updateSubscription).toHaveBeenCalledWith(
309
309
  'videoTrack',
310
310
  { 'session-id': { dimension: undefined } },
311
- DebounceType.IMMEDIATE,
311
+ DebounceType.FAST,
312
312
  );
313
313
 
314
314
  call.state.updateParticipant('session-id', {
@@ -369,7 +369,7 @@ describe('DynascaleManager', () => {
369
369
  expect(updateSubscription).toHaveBeenCalledWith(
370
370
  'videoTrack',
371
371
  { 'session-id': { dimension: undefined } },
372
- DebounceType.IMMEDIATE,
372
+ DebounceType.FAST,
373
373
  );
374
374
  });
375
375
 
@@ -417,7 +417,7 @@ describe('DynascaleManager', () => {
417
417
  },
418
418
  },
419
419
  },
420
- DebounceType.IMMEDIATE,
420
+ DebounceType.FAST,
421
421
  );
422
422
 
423
423
  // @ts-ignore simulate resize
@@ -439,7 +439,47 @@ describe('DynascaleManager', () => {
439
439
  expect(updateSubscription).toHaveBeenCalledWith(
440
440
  'videoTrack',
441
441
  { 'session-id': { dimension: undefined } },
442
- DebounceType.IMMEDIATE,
442
+ DebounceType.FAST,
443
+ );
444
+ });
445
+
446
+ it('video: should unsubscribe when element dimensions are zero', () => {
447
+ // @ts-ignore
448
+ call.state.updateOrAddParticipant('session-id', {
449
+ userId: 'user-id',
450
+ sessionId: 'session-id',
451
+ publishedTracks: [TrackType.VIDEO],
452
+ viewportVisibilityState: {
453
+ videoTrack: VisibilityState.VISIBLE,
454
+ screenShareTrack: VisibilityState.UNKNOWN,
455
+ },
456
+ });
457
+
458
+ let updateSubscription = vi.spyOn(call, 'updateSubscriptionsPartial');
459
+
460
+ // @ts-ignore simulate resize
461
+ videoElement.clientHeight = 0;
462
+ // @ts-ignore simulate resize
463
+ videoElement.clientWidth = 0;
464
+
465
+ const cleanup = dynascaleManager.bindVideoElement(
466
+ videoElement,
467
+ 'session-id',
468
+ 'videoTrack',
469
+ );
470
+
471
+ expect(updateSubscription).toHaveBeenCalledWith(
472
+ 'videoTrack',
473
+ { 'session-id': { dimension: undefined } },
474
+ DebounceType.FAST,
475
+ );
476
+
477
+ cleanup?.();
478
+
479
+ expect(updateSubscription).toHaveBeenLastCalledWith(
480
+ 'videoTrack',
481
+ { 'session-id': { dimension: undefined } },
482
+ DebounceType.FAST,
443
483
  );
444
484
  });
445
485
  });