@stream-io/video-client 1.33.1 → 1.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.33.1",
3
+ "version": "1.34.1",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
@@ -26,34 +26,33 @@
26
26
  "CHANGELOG.md"
27
27
  ],
28
28
  "dependencies": {
29
- "@protobuf-ts/runtime": "^2.9.4",
30
- "@protobuf-ts/runtime-rpc": "^2.9.4",
31
- "@protobuf-ts/twirp-transport": "^2.9.4",
29
+ "@protobuf-ts/runtime": "^2.11.1",
30
+ "@protobuf-ts/runtime-rpc": "^2.11.1",
31
+ "@protobuf-ts/twirp-transport": "^2.11.1",
32
32
  "@stream-io/worker-timer": "^1.2.4",
33
- "axios": "^1.8.1",
34
- "rxjs": "~7.8.1",
33
+ "axios": "^1.12.2",
34
+ "rxjs": "~7.8.2",
35
35
  "sdp-transform": "^2.15.0",
36
- "ua-parser-js": "^1.0.40",
37
- "webrtc-adapter": "^8.2.3"
36
+ "ua-parser-js": "^1.0.41",
37
+ "webrtc-adapter": "^8.2.4"
38
38
  },
39
39
  "devDependencies": {
40
- "@openapitools/openapi-generator-cli": "^2.13.4",
40
+ "@openapitools/openapi-generator-cli": "^2.25.0",
41
41
  "@rollup/plugin-replace": "^6.0.2",
42
- "@rollup/plugin-typescript": "^12.1.2",
43
- "@stream-io/audio-filters-web": "^0.5.0",
44
- "@stream-io/node-sdk": "^0.4.24",
42
+ "@rollup/plugin-typescript": "^12.1.4",
43
+ "@stream-io/audio-filters-web": "^0.6.0",
44
+ "@stream-io/node-sdk": "^0.7.9",
45
45
  "@total-typescript/shoehorn": "^0.1.2",
46
- "@types/sdp-transform": "^2.4.9",
46
+ "@types/sdp-transform": "^2.15.0",
47
47
  "@types/ua-parser-js": "^0.7.39",
48
- "@vitest/coverage-v8": "^3.1.3",
49
- "dotenv": "^16.5.0",
50
- "happy-dom": "^17.5.6",
51
- "prettier": "^3.5.3",
48
+ "@vitest/coverage-v8": "^3.2.4",
49
+ "dotenv": "^16.6.1",
50
+ "happy-dom": "^20.0.0",
51
+ "prettier": "^3.6.2",
52
52
  "rimraf": "^6.0.1",
53
- "rollup": "^4.40.2",
54
- "typescript": "^5.8.3",
55
- "vite": "^6.3.5",
56
- "vitest": "^3.1.3",
53
+ "rollup": "^4.52.4",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^3.2.4",
57
56
  "vitest-mock-extended": "^3.1.0"
58
57
  }
59
58
  }
package/src/Call.ts CHANGED
@@ -1288,6 +1288,7 @@ export class Call {
1288
1288
  }
1289
1289
 
1290
1290
  this.tracer.setEnabled(enableTracing);
1291
+ this.sfuStatsReporter?.flush();
1291
1292
  this.sfuStatsReporter?.stop();
1292
1293
  if (statsOptions?.reporting_interval_ms > 0) {
1293
1294
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
@@ -43,12 +43,13 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
43
43
  await videoTrack?.applyConstraints({
44
44
  facingMode: direction === 'front' ? 'user' : 'environment',
45
45
  });
46
- this.state.setDirection(direction);
47
- return;
48
46
  }
49
47
  // providing both device id and direction doesn't work, so we deselect the device
50
48
  this.state.setDirection(direction);
51
49
  this.state.setDevice(undefined);
50
+ if (isReactNative()) {
51
+ return;
52
+ }
52
53
  this.getTracks().forEach((track) => track.stop());
53
54
  try {
54
55
  await this.unmuteStream();
@@ -113,14 +113,17 @@ export class SpeakerManager {
113
113
  * @param volume a number between 0 and 1. Set it to `undefined` to use the default volume.
114
114
  */
115
115
  setParticipantVolume(sessionId: string, volume: number | undefined) {
116
- if (isReactNative()) {
117
- throw new Error(
118
- 'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
119
- );
120
- }
121
116
  if (volume && (volume < 0 || volume > 1)) {
122
117
  throw new Error('Volume must be between 0 and 1, or undefined');
123
118
  }
124
- this.call.state.updateParticipant(sessionId, { audioVolume: volume });
119
+ this.call.state.updateParticipant(sessionId, (p) => {
120
+ if (isReactNative() && p.audioStream) {
121
+ for (const track of p.audioStream.getAudioTracks()) {
122
+ // @ts-expect-error track._setVolume is present in react-native-webrtc
123
+ track?._setVolume(volume);
124
+ }
125
+ }
126
+ return { audioVolume: volume };
127
+ });
125
128
  }
126
129
  }
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { fromPartial } from '@total-typescript/shoehorn';
2
3
  import {
3
4
  emitDeviceIds,
4
5
  mockAudioDevices,
@@ -80,11 +81,13 @@ describe('SpeakerManager.test', () => {
80
81
 
81
82
  it('set participant volume', () => {
82
83
  const call = manager['call'];
83
- // @ts-expect-error - incomplete data
84
- call.state.updateOrAddParticipant('session-id', {
85
- audioVolume: undefined,
86
- sessionId: 'session-id',
87
- });
84
+ call.state.updateOrAddParticipant(
85
+ 'session-id',
86
+ fromPartial({
87
+ audioVolume: undefined,
88
+ sessionId: 'session-id',
89
+ }),
90
+ );
88
91
 
89
92
  manager.setParticipantVolume('session-id', 0.5);
90
93
  let participant = call.state.findParticipantBySessionId('session-id');
@@ -1,5 +1,6 @@
1
1
  import './mocks/webrtc.mocks';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
+ import { fromPartial } from '@total-typescript/shoehorn';
3
4
  import {
4
5
  AudioBitrateProfile,
5
6
  PublishOption,
@@ -24,13 +25,12 @@ describe('videoLayers', () => {
24
25
  const targetBitrate = 3000000;
25
26
  vi.spyOn(track, 'getSettings').mockReturnValue({ width, height });
26
27
 
27
- const publishOption: PublishOption = {
28
+ const publishOption: PublishOption = fromPartial({
28
29
  bitrate: targetBitrate,
29
- // @ts-expect-error - incomplete data
30
30
  codec: { name: 'vp8' },
31
31
  videoDimension: { width, height },
32
32
  fps: 30,
33
- };
33
+ });
34
34
  const layers = computeVideoLayers(track, publishOption);
35
35
  expect(layers).toEqual([
36
36
  {
@@ -66,12 +66,13 @@ describe('videoLayers', () => {
66
66
  it('should return undefined for audio track', () => {
67
67
  const track = new MediaStreamTrack();
68
68
  expect(
69
- // @ts-expect-error - incomplete data
70
- computeVideoLayers(track, { trackType: TrackType.AUDIO }),
69
+ computeVideoLayers(track, fromPartial({ trackType: TrackType.AUDIO })),
71
70
  ).toBeUndefined();
72
71
  expect(
73
- // @ts-expect-error - incomplete data
74
- computeVideoLayers(track, { trackType: TrackType.SCREEN_SHARE_AUDIO }),
72
+ computeVideoLayers(
73
+ track,
74
+ fromPartial({ trackType: TrackType.SCREEN_SHARE_AUDIO }),
75
+ ),
75
76
  ).toBeUndefined();
76
77
  });
77
78
 
@@ -79,13 +80,15 @@ describe('videoLayers', () => {
79
80
  const bitrate = 3000000;
80
81
  const track = new MediaStreamTrack();
81
82
  vi.spyOn(track, 'getSettings').mockReturnValue({});
82
- const layers = computeVideoLayers(track, {
83
- bitrate,
84
- // @ts-expect-error - incomplete data
85
- codec: { name: 'vp8' },
86
- fps: 30,
87
- videoDimension: { width: 320, height: 180 },
88
- });
83
+ const layers = computeVideoLayers(
84
+ track,
85
+ fromPartial({
86
+ bitrate,
87
+ codec: { name: 'vp8' },
88
+ fps: 30,
89
+ videoDimension: { width: 320, height: 180 },
90
+ }),
91
+ );
89
92
  expect(layers).toEqual([
90
93
  {
91
94
  active: true,
@@ -104,13 +107,15 @@ describe('videoLayers', () => {
104
107
  const width = 320;
105
108
  const height = 240;
106
109
  vi.spyOn(track, 'getSettings').mockReturnValue({ width, height });
107
- const layers = computeVideoLayers(track, {
108
- bitrate: 0,
109
- // @ts-expect-error - incomplete data
110
- codec: { name: 'vp8' },
111
- fps: 30,
112
- videoDimension: { width, height },
113
- });
110
+ const layers = computeVideoLayers(
111
+ track,
112
+ fromPartial({
113
+ bitrate: 0,
114
+ codec: { name: 'vp8' },
115
+ fps: 30,
116
+ videoDimension: { width, height },
117
+ }),
118
+ );
114
119
  expect(layers.length).toBe(1);
115
120
  const [q] = layers;
116
121
  expect(q.rid).toBe('q');
@@ -123,13 +128,15 @@ describe('videoLayers', () => {
123
128
  const width = 640;
124
129
  const height = 480;
125
130
  vi.spyOn(track, 'getSettings').mockReturnValue({ width, height });
126
- const layers = computeVideoLayers(track, {
127
- bitrate: 0,
128
- // @ts-expect-error - incomplete data
129
- codec: { name: 'vp8' },
130
- fps: 30,
131
- videoDimension: { width, height },
132
- });
131
+ const layers = computeVideoLayers(
132
+ track,
133
+ fromPartial({
134
+ bitrate: 0,
135
+ codec: { name: 'vp8' },
136
+ fps: 30,
137
+ videoDimension: { width, height },
138
+ }),
139
+ );
133
140
  expect(layers.length).toBe(2);
134
141
  const [q, h] = layers;
135
142
  expect(q.rid).toBe('q');
@@ -145,13 +152,15 @@ describe('videoLayers', () => {
145
152
  const width = 1280;
146
153
  const height = 720;
147
154
  vi.spyOn(track, 'getSettings').mockReturnValue({ width, height });
148
- const layers = computeVideoLayers(track, {
149
- bitrate: 0,
150
- // @ts-expect-error - incomplete data
151
- codec: { name: 'vp8' },
152
- fps: 30,
153
- videoDimension: { width, height },
154
- });
155
+ const layers = computeVideoLayers(
156
+ track,
157
+ fromPartial({
158
+ bitrate: 0,
159
+ codec: { name: 'vp8' },
160
+ fps: 30,
161
+ videoDimension: { width, height },
162
+ }),
163
+ );
155
164
  expect(layers.length).toBe(3);
156
165
  const [q, h, f] = layers;
157
166
  expect(q.rid).toBe('q');
@@ -171,13 +180,15 @@ describe('videoLayers', () => {
171
180
  width: 1280,
172
181
  height: 720,
173
182
  });
174
- const layers = computeVideoLayers(track, {
175
- maxTemporalLayers: 3,
176
- maxSpatialLayers: 3,
177
- // @ts-expect-error - incomplete data
178
- codec: { name: 'vp9' },
179
- videoDimension: { width: 1280, height: 720 },
180
- });
183
+ const layers = computeVideoLayers(
184
+ track,
185
+ fromPartial({
186
+ maxTemporalLayers: 3,
187
+ maxSpatialLayers: 3,
188
+ codec: { name: 'vp9' },
189
+ videoDimension: { width: 1280, height: 720 },
190
+ }),
191
+ );
181
192
  expect(layers.length).toBe(3);
182
193
  expect(layers[0].scalabilityMode).toBe('L3T3_KEY');
183
194
  expect(layers[0].rid).toBe('q');
@@ -187,8 +198,10 @@ describe('videoLayers', () => {
187
198
 
188
199
  it('should activate only a single layer when useSingleLayer is true', () => {
189
200
  const track = new MediaStreamTrack();
190
- // @ts-expect-error - incomplete data
191
- const layers = computeVideoLayers(track, { useSingleLayer: true });
201
+ const layers = computeVideoLayers(
202
+ track,
203
+ fromPartial({ useSingleLayer: true }),
204
+ );
192
205
  expect(layers.length).toBe(3);
193
206
  expect(layers[0].active).toBe(false);
194
207
  expect(layers[0].rid).toBe('q');
@@ -201,8 +214,10 @@ describe('videoLayers', () => {
201
214
  it('should activate only a single layer when useSingleLayer is true in single layer mode', () => {
202
215
  const track = new MediaStreamTrack();
203
216
  vi.spyOn(track, 'getSettings').mockReturnValue({ width: 320, height: 180 });
204
- // @ts-expect-error - incomplete data
205
- const layers = computeVideoLayers(track, { useSingleLayer: true });
217
+ const layers = computeVideoLayers(
218
+ track,
219
+ fromPartial({ useSingleLayer: true }),
220
+ );
206
221
  expect(layers.length).toBe(1);
207
222
  expect(layers[0].active).toBe(true);
208
223
  expect(layers[0].rid).toBe('q');
@@ -210,13 +225,15 @@ describe('videoLayers', () => {
210
225
 
211
226
  it('should activate only one temporal layer when useSingleLayer is true for SVC', () => {
212
227
  const track = new MediaStreamTrack();
213
- const layers = computeVideoLayers(track, {
214
- // @ts-expect-error - incomplete data
215
- codec: { name: 'vp9' },
216
- maxSpatialLayers: 3,
217
- maxTemporalLayers: 3,
218
- useSingleLayer: true,
219
- });
228
+ const layers = computeVideoLayers(
229
+ track,
230
+ fromPartial({
231
+ codec: { name: 'vp9' },
232
+ maxSpatialLayers: 3,
233
+ maxTemporalLayers: 3,
234
+ useSingleLayer: true,
235
+ }),
236
+ );
220
237
  expect(layers.length).toBe(3);
221
238
  expect(layers[0].rid).toBe('q');
222
239
  expect(layers[0].active).toBe(false);
@@ -318,13 +335,15 @@ describe('videoLayers', () => {
318
335
 
319
336
  it('should use integer for maxBitrate', () => {
320
337
  const track = new MediaStreamTrack();
321
- const layers = computeVideoLayers(track, {
322
- bitrate: 2999777,
323
- // @ts-expect-error - incomplete data
324
- codec: { name: 'vp8' },
325
- videoDimension: { width: 1920, height: 1080 },
326
- fps: 30,
327
- });
338
+ const layers = computeVideoLayers(
339
+ track,
340
+ fromPartial({
341
+ bitrate: 2999777,
342
+ codec: { name: 'vp8' },
343
+ videoDimension: { width: 1920, height: 1080 },
344
+ fps: 30,
345
+ }),
346
+ );
328
347
  expect(layers).toBeDefined();
329
348
  for (const layer of layers!) {
330
349
  expect(Number.isInteger(layer.width)).toBe(true);
@@ -422,22 +441,31 @@ describe('audioLayers', () => {
422
441
  it('should use predefined bitrates when publish options are missing', () => {
423
442
  expect(
424
443
  computeAudioLayers(
425
- // @ts-expect-error - incomplete data
426
- { trackType: TrackType.AUDIO, id: 1, audioBitrateProfiles: [] },
444
+ fromPartial({
445
+ trackType: TrackType.AUDIO,
446
+ id: 1,
447
+ audioBitrateProfiles: [],
448
+ }),
427
449
  { audioBitrateProfile: AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED },
428
450
  ),
429
451
  ).toEqual([{ maxBitrate: 64000 }]);
430
452
  expect(
431
453
  computeAudioLayers(
432
- // @ts-expect-error - incomplete data
433
- { trackType: TrackType.AUDIO, id: 1, audioBitrateProfiles: [] },
454
+ fromPartial({
455
+ trackType: TrackType.AUDIO,
456
+ id: 1,
457
+ audioBitrateProfiles: [],
458
+ }),
434
459
  { audioBitrateProfile: AudioBitrateProfile.VOICE_HIGH_QUALITY },
435
460
  ),
436
461
  ).toEqual([{ maxBitrate: 128000 }]);
437
462
  expect(
438
463
  computeAudioLayers(
439
- // @ts-expect-error - incomplete data
440
- { trackType: TrackType.AUDIO, id: 1, audioBitrateProfiles: [] },
464
+ fromPartial({
465
+ trackType: TrackType.AUDIO,
466
+ id: 1,
467
+ audioBitrateProfiles: [],
468
+ }),
441
469
  { audioBitrateProfile: AudioBitrateProfile.MUSIC_HIGH_QUALITY },
442
470
  ),
443
471
  ).toEqual([{ maxBitrate: 128000 }]);
@@ -445,17 +473,14 @@ describe('audioLayers', () => {
445
473
 
446
474
  it('should use the predefined bitrate when the SFU does not provide audioBitrateProfiles', () => {
447
475
  expect(
448
- computeAudioLayers(
449
- // @ts-expect-error - incomplete data
450
- { trackType: TrackType.AUDIO, id: 1 },
451
- { audioBitrateProfile: AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED },
452
- ),
476
+ computeAudioLayers(fromPartial({ trackType: TrackType.AUDIO, id: 1 }), {
477
+ audioBitrateProfile: AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED,
478
+ }),
453
479
  ).toEqual([{ maxBitrate: 64000 }]);
454
480
  });
455
481
 
456
482
  it('should respect the bitrates provided in publish options', () => {
457
- // @ts-expect-error - incomplete data
458
- const config: PublishOption = {
483
+ const config: PublishOption = fromPartial({
459
484
  trackType: TrackType.AUDIO,
460
485
  id: 1,
461
486
  audioBitrateProfiles: [
@@ -472,7 +497,7 @@ describe('audioLayers', () => {
472
497
  bitrate: 192000,
473
498
  },
474
499
  ],
475
- };
500
+ });
476
501
  expect(
477
502
  computeAudioLayers(config, {
478
503
  audioBitrateProfile: AudioBitrateProfile.VOICE_STANDARD_UNSPECIFIED,
@@ -1,6 +1,7 @@
1
1
  import '../../rtc/__tests__/mocks/webrtc.mocks';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
  import { anyNumber } from 'vitest-mock-extended';
4
+ import { fromPartial } from '@total-typescript/shoehorn';
4
5
  import { StreamVideoParticipant, VisibilityState } from '../../types';
5
6
  import { CallingState } from '../CallingState';
6
7
  import { CallState } from '../CallState';
@@ -56,8 +57,7 @@ describe('CallState', () => {
56
57
  it(`shouldn't emit when primitive (backstage) values didn't change`, () => {
57
58
  const state = new CallState();
58
59
  const updateWith = (value: boolean) => {
59
- // @ts-expect-error incomplete data
60
- state.updateFromCallResponse({ backstage: value });
60
+ state.updateFromCallResponse(fromPartial({ backstage: value }));
61
61
  };
62
62
 
63
63
  updateWith(false);
@@ -138,8 +138,7 @@ describe('CallState', () => {
138
138
  it(`shouldn't emit when string arrays (blockedUserIds) value didn't change`, () => {
139
139
  const state = new CallState();
140
140
  const updateWith = (value: string[]) => {
141
- // @ts-expect-error incomplete data
142
- state.updateFromCallResponse({ blocked_user_ids: value });
141
+ state.updateFromCallResponse(fromPartial({ blocked_user_ids: value }));
143
142
  };
144
143
 
145
144
  updateWith(['a', 'b']);
@@ -640,25 +639,21 @@ describe('CallState', () => {
640
639
  describe('recording and broadcasting events', () => {
641
640
  it('handles call.recording_started events', () => {
642
641
  const state = new CallState();
643
- // @ts-expect-error incomplete data
644
- state.updateFromEvent({ type: 'call.recording_started' });
642
+ state.updateFromEvent(fromPartial({ type: 'call.recording_started' }));
645
643
  expect(state.recording).toBe(true);
646
644
  });
647
645
 
648
646
  it('handles call.recording_stopped events', () => {
649
647
  const state = new CallState();
650
- // @ts-expect-error incomplete data
651
- state.updateFromEvent({ type: 'call.recording_stopped' });
648
+ state.updateFromEvent(fromPartial({ type: 'call.recording_stopped' }));
652
649
  expect(state.recording).toBe(false);
653
650
  });
654
651
 
655
652
  it('handles call.recording_failed events', () => {
656
653
  const state = new CallState();
657
- // @ts-expect-error incomplete data
658
- state.updateFromEvent({ type: 'call.recording_started' });
654
+ state.updateFromEvent(fromPartial({ type: 'call.recording_started' }));
659
655
  expect(state.recording).toBe(true);
660
- // @ts-expect-error incomplete data
661
- state.updateFromEvent({ type: 'call.recording_failed' });
656
+ state.updateFromEvent(fromPartial({ type: 'call.recording_failed' }));
662
657
  expect(state.recording).toBe(false);
663
658
  });
664
659