@stream-io/video-client 1.4.3 → 1.4.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.
@@ -3,7 +3,7 @@ import { Call } from '../Call';
3
3
  import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
4
4
  import { Logger } from '../coordinator/connection/types';
5
5
  import { TrackType } from '../gen/video/sfu/models/models';
6
- export type MediaStreamFilter = (stream: MediaStream) => Promise<MediaStream>;
6
+ import { MediaStreamFilter, MediaStreamFilterRegistrationResult } from './filters';
7
7
  export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState<C>, C = MediaTrackConstraints> {
8
8
  protected readonly call: Call;
9
9
  readonly state: T;
@@ -17,6 +17,7 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
17
17
  private isTrackStoppedDueToTrackEnd;
18
18
  private filters;
19
19
  private statusChangeConcurrencyTag;
20
+ private filterRegistrationConcurrencyTag;
20
21
  protected constructor(call: Call, state: T, trackType: TrackType);
21
22
  /**
22
23
  * Lists the available audio/video devices
@@ -55,9 +56,9 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
55
56
  * a new stream with the applied filter.
56
57
  *
57
58
  * @param filter the filter to register.
58
- * @returns a function that will unregister the filter.
59
+ * @returns MediaStreamFilterRegistrationResult
59
60
  */
60
- registerFilter(filter: MediaStreamFilter): Promise<() => Promise<void>>;
61
+ registerFilter(filter: MediaStreamFilter): MediaStreamFilterRegistrationResult;
61
62
  /**
62
63
  * Will set the default constraints for the device.
63
64
  *
@@ -12,6 +12,7 @@ export declare class MicrophoneManager extends InputMediaDeviceManager<Microphon
12
12
  private noiseCancellation;
13
13
  private noiseCancellationChangeUnsubscribe;
14
14
  private noiseCancellationRegistration?;
15
+ private uregisterNoiseCancellation?;
15
16
  constructor(call: Call, disableMode?: TrackDisableMode);
16
17
  /**
17
18
  * Enables noise cancellation for the microphone.
@@ -0,0 +1,32 @@
1
+ export type MediaStreamFilter = (input: MediaStream) => MediaStreamFilterResult;
2
+ export type MediaStreamFilterCleanup = () => void;
3
+ export interface MediaStreamFilterResult {
4
+ /**
5
+ * Transformed media stream. If the filter is asynchronous, a promise which
6
+ * resolves with a transformed media stream.
7
+ */
8
+ output: MediaStream | Promise<MediaStream>;
9
+ /**
10
+ * An optional cleanup callback. It is called when the filter is stopped, and
11
+ * when it is unregistered.
12
+ */
13
+ stop?: MediaStreamFilterCleanup;
14
+ }
15
+ export interface MediaStreamFilterEntry {
16
+ start: MediaStreamFilter;
17
+ /**
18
+ * When the filter is running, it holds a cleanup callback returned when the filter
19
+ * was started.
20
+ */
21
+ stop: MediaStreamFilterCleanup | undefined;
22
+ }
23
+ export interface MediaStreamFilterRegistrationResult {
24
+ /**
25
+ * Promise that resolves when the filter is applied to the stream.
26
+ */
27
+ registered: Promise<void>;
28
+ /**
29
+ * Function that can be called to unregister the filter.
30
+ */
31
+ unregister: () => Promise<void>;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -8,9 +8,16 @@ import { Logger } from '../coordinator/connection/types';
8
8
  import { getLogger } from '../logger';
9
9
  import { TrackType } from '../gen/video/sfu/models/models';
10
10
  import { deviceIds$ } from './devices';
11
- import { settled, withCancellation } from '../helpers/concurrency';
12
-
13
- export type MediaStreamFilter = (stream: MediaStream) => Promise<MediaStream>;
11
+ import {
12
+ settled,
13
+ withCancellation,
14
+ withoutConcurrency,
15
+ } from '../helpers/concurrency';
16
+ import {
17
+ MediaStreamFilter,
18
+ MediaStreamFilterEntry,
19
+ MediaStreamFilterRegistrationResult,
20
+ } from './filters';
14
21
 
15
22
  export abstract class InputMediaDeviceManager<
16
23
  T extends InputMediaDeviceManagerState<C>,
@@ -24,8 +31,11 @@ export abstract class InputMediaDeviceManager<
24
31
 
25
32
  protected subscriptions: Function[] = [];
26
33
  private isTrackStoppedDueToTrackEnd = false;
27
- private filters: MediaStreamFilter[] = [];
34
+ private filters: MediaStreamFilterEntry[] = [];
28
35
  private statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
36
+ private filterRegistrationConcurrencyTag = Symbol(
37
+ 'filterRegistrationConcurrencyTag',
38
+ );
29
39
 
30
40
  protected constructor(
31
41
  protected readonly call: Call,
@@ -139,14 +149,32 @@ export abstract class InputMediaDeviceManager<
139
149
  * a new stream with the applied filter.
140
150
  *
141
151
  * @param filter the filter to register.
142
- * @returns a function that will unregister the filter.
152
+ * @returns MediaStreamFilterRegistrationResult
143
153
  */
144
- async registerFilter(filter: MediaStreamFilter) {
145
- this.filters.push(filter);
146
- await this.applySettingsToStream();
147
- return async () => {
148
- this.filters = this.filters.filter((f) => f !== filter);
149
- await this.applySettingsToStream();
154
+ registerFilter(
155
+ filter: MediaStreamFilter,
156
+ ): MediaStreamFilterRegistrationResult {
157
+ const entry: MediaStreamFilterEntry = {
158
+ start: filter,
159
+ stop: undefined,
160
+ };
161
+
162
+ const registered = withoutConcurrency(
163
+ this.filterRegistrationConcurrencyTag,
164
+ async () => {
165
+ this.filters.push(entry);
166
+ await this.applySettingsToStream();
167
+ },
168
+ );
169
+
170
+ return {
171
+ registered,
172
+ unregister: () =>
173
+ withoutConcurrency(this.filterRegistrationConcurrencyTag, async () => {
174
+ entry.stop?.();
175
+ this.filters = this.filters.filter((f) => f !== entry);
176
+ await this.applySettingsToStream();
177
+ }),
150
178
  };
151
179
  }
152
180
 
@@ -224,6 +252,7 @@ export abstract class InputMediaDeviceManager<
224
252
  this.state.mediaStream.release();
225
253
  }
226
254
  this.state.setMediaStream(undefined, undefined);
255
+ this.filters.forEach((entry) => entry.stop?.());
227
256
  }
228
257
  }
229
258
 
@@ -335,7 +364,21 @@ export abstract class InputMediaDeviceManager<
335
364
  rootStream = this.getStream(constraints as C);
336
365
  // we publish the last MediaStream of the chain
337
366
  stream = await this.filters.reduce(
338
- (parent, filter) => parent.then(filter).then(chainWith(parent)),
367
+ (parent, entry) =>
368
+ parent
369
+ .then((inputStream) => {
370
+ const { stop, output } = entry.start(inputStream);
371
+ entry.stop = stop;
372
+ return output;
373
+ })
374
+ .then(chainWith(parent), (error) => {
375
+ this.logger(
376
+ 'warn',
377
+ 'Fitler failed to start and will be ignored',
378
+ error,
379
+ );
380
+ return parent;
381
+ }),
339
382
  rootStream,
340
383
  );
341
384
  }
@@ -24,7 +24,8 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
24
24
  private rnSpeechDetector: RNSpeechDetector | undefined;
25
25
  private noiseCancellation: INoiseCancellation | undefined;
26
26
  private noiseCancellationChangeUnsubscribe: (() => void) | undefined;
27
- private noiseCancellationRegistration?: Promise<() => Promise<void>>;
27
+ private noiseCancellationRegistration?: Promise<void>;
28
+ private uregisterNoiseCancellation?: () => Promise<void>;
28
29
 
29
30
  constructor(
30
31
  call: Call,
@@ -139,9 +140,11 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
139
140
  },
140
141
  );
141
142
 
142
- this.noiseCancellationRegistration = this.registerFilter(
143
+ const registrationResult = this.registerFilter(
143
144
  noiseCancellation.toFilter(),
144
145
  );
146
+ this.noiseCancellationRegistration = registrationResult.registered;
147
+ this.uregisterNoiseCancellation = registrationResult.unregister;
145
148
  await this.noiseCancellationRegistration;
146
149
 
147
150
  // handles an edge case where a noise cancellation is enabled after
@@ -170,8 +173,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
170
173
  if (isReactNative()) {
171
174
  throw new Error('Noise cancellation is not supported in React Native');
172
175
  }
173
- await this.noiseCancellationRegistration
174
- ?.then((unregister) => unregister())
176
+ await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
175
177
  .then(() => this.noiseCancellation?.disable())
176
178
  .then(() => this.noiseCancellationChangeUnsubscribe?.())
177
179
  .catch((err) => {
@@ -58,8 +58,9 @@ describe('MediaStream Filters', () => {
58
58
  const track = new MediaStreamTrack();
59
59
  vi.spyOn(mediaStream, 'getTracks').mockReturnValue([track]);
60
60
  vi.spyOn(track, 'getSettings').mockReturnValue({ deviceId: '123' });
61
- const filter = vi.fn().mockReturnValue(mediaStream);
62
- const unregister = await manager.registerFilter(filter);
61
+ const filter = vi.fn().mockReturnValue({ output: mediaStream });
62
+ const { registered, unregister } = manager.registerFilter(filter);
63
+ await registered;
63
64
  await manager.enable();
64
65
 
65
66
  expect(filter).toHaveBeenCalled();
@@ -92,10 +93,10 @@ describe('MediaStream Filters', () => {
92
93
 
93
94
  const rootMediaStream = createMediaStream();
94
95
  const filterMediaStream = createMediaStream();
95
- const filter1 = vi.fn().mockReturnValue(rootMediaStream);
96
- const filter2 = vi.fn().mockReturnValue(filterMediaStream);
97
- await manager.registerFilter(filter1);
98
- await manager.registerFilter(filter2);
96
+ const filter1 = vi.fn().mockReturnValue({ output: rootMediaStream });
97
+ const filter2 = vi.fn().mockReturnValue({ output: filterMediaStream });
98
+ await manager.registerFilter(filter1).registered;
99
+ await manager.registerFilter(filter2).registered;
99
100
  await manager.enable();
100
101
 
101
102
  expect(filter1).toHaveBeenCalled();
@@ -0,0 +1,38 @@
1
+ export type MediaStreamFilter = (input: MediaStream) => MediaStreamFilterResult;
2
+ export type MediaStreamFilterCleanup = () => void;
3
+
4
+ export interface MediaStreamFilterResult {
5
+ /**
6
+ * Transformed media stream. If the filter is asynchronous, a promise which
7
+ * resolves with a transformed media stream.
8
+ */
9
+ output: MediaStream | Promise<MediaStream>;
10
+
11
+ /**
12
+ * An optional cleanup callback. It is called when the filter is stopped, and
13
+ * when it is unregistered.
14
+ */
15
+ stop?: MediaStreamFilterCleanup;
16
+ }
17
+
18
+ export interface MediaStreamFilterEntry {
19
+ start: MediaStreamFilter;
20
+
21
+ /**
22
+ * When the filter is running, it holds a cleanup callback returned when the filter
23
+ * was started.
24
+ */
25
+ stop: MediaStreamFilterCleanup | undefined;
26
+ }
27
+
28
+ export interface MediaStreamFilterRegistrationResult {
29
+ /**
30
+ * Promise that resolves when the filter is applied to the stream.
31
+ */
32
+ registered: Promise<void>;
33
+
34
+ /**
35
+ * Function that can be called to unregister the filter.
36
+ */
37
+ unregister: () => Promise<void>;
38
+ }