@stream-io/video-client 1.11.4 → 1.11.6

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.
@@ -21,8 +21,7 @@ export declare class StreamVideoClient {
21
21
  protected readonly writeableStateStore: StreamVideoWriteableStateStore;
22
22
  streamClient: StreamClient;
23
23
  protected eventHandlersToUnregister: Array<() => void>;
24
- protected connectionPromise: Promise<void | ConnectedEvent> | undefined;
25
- protected disconnectionPromise: Promise<void> | undefined;
24
+ private readonly connectionConcurrencyTag;
26
25
  private static _instanceMap;
27
26
  /**
28
27
  * You should create only one instance of `StreamVideoClient`.
@@ -159,5 +158,5 @@ export declare class StreamVideoClient {
159
158
  * @param user the user to connect.
160
159
  * @param tokenOrProvider a token or a function that returns a token.
161
160
  */
162
- protected connectAnonymousUser: (user: UserWithId, tokenOrProvider: TokenOrProvider) => Promise<void | ConnectedEvent>;
161
+ protected connectAnonymousUser: (user: UserWithId, tokenOrProvider: TokenOrProvider) => Promise<void>;
163
162
  }
@@ -4,7 +4,7 @@ import { TokenManager } from './token_manager';
4
4
  import { WSConnectionFallback } from './connection_fallback';
5
5
  import { AllClientEvents, AllClientEventTypes, APIErrorResponse, ClientEventListener, ConnectAPIResponse, ErrorFromResponse, Logger, StreamClientOptions, StreamVideoEvent, TokenOrProvider, User, UserWithId } from './types';
6
6
  import { InsightMetrics } from './insights';
7
- import { CreateGuestResponse } from '../../gen/coordinator';
7
+ import { ConnectedEvent, CreateGuestResponse } from '../../gen/coordinator';
8
8
  export declare class StreamClient {
9
9
  _user?: UserWithId;
10
10
  anonymous: boolean;
@@ -28,14 +28,14 @@ export declare class StreamClient {
28
28
  wsBaseURL?: string;
29
29
  wsConnection: StableWSConnection | null;
30
30
  wsFallback?: WSConnectionFallback;
31
- wsPromise: ConnectAPIResponse | null;
31
+ private wsPromiseSafe;
32
32
  consecutiveFailures: number;
33
33
  insightMetrics: InsightMetrics;
34
34
  defaultWSTimeoutWithFallback: number;
35
35
  defaultWSTimeout: number;
36
36
  resolveConnectionId?: Function;
37
37
  rejectConnectionId?: Function;
38
- connectionIdPromise?: Promise<string | undefined>;
38
+ private connectionIdPromiseSafe?;
39
39
  guestUserCreatePromise?: Promise<CreateGuestResponse>;
40
40
  /**
41
41
  * Initialize a client.
@@ -64,7 +64,7 @@ export declare class StreamClient {
64
64
  *
65
65
  * @return {ConnectAPIResponse} Returns a promise that resolves when the connection is setup
66
66
  */
67
- connectUser: (user: UserWithId, userTokenOrProvider: TokenOrProvider) => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
67
+ connectUser: (user: UserWithId, userTokenOrProvider: TokenOrProvider) => Promise<void | ConnectedEvent>;
68
68
  _setToken: (user: UserWithId, userTokenOrProvider: TokenOrProvider, isAnonymous: boolean) => Promise<void>;
69
69
  _setUser: (user: UserWithId) => void;
70
70
  /**
@@ -84,7 +84,7 @@ export declare class StreamClient {
84
84
  /**
85
85
  * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
86
86
  */
87
- openConnection: () => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
87
+ openConnection: () => Promise<ConnectedEvent | undefined>;
88
88
  /**
89
89
  * Disconnects the websocket and removes the user from client.
90
90
  *
@@ -94,7 +94,7 @@ export declare class StreamClient {
94
94
  disconnectUser: (timeout?: number) => Promise<void>;
95
95
  connectGuestUser: (user: User & {
96
96
  type: "guest";
97
- }) => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
97
+ }) => Promise<void | ConnectedEvent>;
98
98
  /**
99
99
  * connectAnonymousUser - Set an anonymous user and open a WebSocket connection
100
100
  */
@@ -117,7 +117,10 @@ export declare class StreamClient {
117
117
  /**
118
118
  * sets up the this.connectionIdPromise
119
119
  */
120
- _setupConnectionIdPromise: () => Promise<void>;
120
+ _setupConnectionIdPromise: () => void;
121
+ get connectionIdPromise(): Promise<string | undefined> | undefined;
122
+ get isConnectionIsPromisePending(): boolean;
123
+ get wsPromise(): Promise<ConnectedEvent | undefined> | undefined;
121
124
  _logApiRequest: (type: string, url: string, data: unknown, config: AxiosRequestConfig & {
122
125
  config?: AxiosRequestConfig & {
123
126
  maxBodyLength?: number;
@@ -143,7 +146,7 @@ export declare class StreamClient {
143
146
  /**
144
147
  * @private
145
148
  */
146
- connect: () => Promise<void | import("../../gen/coordinator").ConnectedEvent>;
149
+ connect: () => Promise<ConnectedEvent | undefined>;
147
150
  /**
148
151
  * Check the connectivity with server for warmup purpose.
149
152
  *
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'isomorphic-ws';
2
2
  import { StreamClient } from './client';
3
- import type { ConnectAPIResponse, LogLevel, UR } from './types';
3
+ import type { LogLevel, UR } from './types';
4
4
  import type { ConnectedEvent } from '../../gen/coordinator';
5
5
  /**
6
6
  * StableWSConnection - A WS connection that reconnects upon failure.
@@ -21,7 +21,7 @@ import type { ConnectedEvent } from '../../gen/coordinator';
21
21
  */
22
22
  export declare class StableWSConnection {
23
23
  connectionID?: string;
24
- connectionOpen?: ConnectAPIResponse;
24
+ private connectionOpenSafe?;
25
25
  authenticationSent: boolean;
26
26
  consecutiveFailures: number;
27
27
  pingInterval: number;
@@ -52,13 +52,13 @@ export declare class StableWSConnection {
52
52
  * the default 15s timeout allows between 2~3 tries
53
53
  * @return {ConnectAPIResponse<ConnectedEvent>} Promise that completes once the first health check message is received
54
54
  */
55
- connect(timeout?: number): Promise<void | ConnectedEvent>;
55
+ connect(timeout?: number): Promise<ConnectedEvent | undefined>;
56
56
  /**
57
57
  * _waitForHealthy polls the promise connection to see if its resolved until it times out
58
58
  * the default 15s timeout allows between 2~3 tries
59
59
  * @param timeout duration(ms)
60
60
  */
61
- _waitForHealthy(timeout?: number): Promise<void | ConnectedEvent>;
61
+ _waitForHealthy(timeout?: number): Promise<ConnectedEvent | undefined>;
62
62
  /**
63
63
  * Builds and returns the url for websocket.
64
64
  * @private
@@ -126,6 +126,7 @@ export declare class StableWSConnection {
126
126
  * _setupPromise - sets up the this.connectOpen promise
127
127
  */
128
128
  _setupConnectionPromise: () => void;
129
+ get connectionOpen(): Promise<ConnectedEvent> | undefined;
129
130
  /**
130
131
  * Schedules a next health check ping for websocket.
131
132
  */
@@ -0,0 +1,18 @@
1
+ export interface SafePromise<T> {
2
+ (): Promise<T>;
3
+ checkPending(): boolean;
4
+ }
5
+ /**
6
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
7
+ * since rejecting the promise causes an unhandled rejection error (even if the
8
+ * rejection is handled everywhere promise result is expected).
9
+ *
10
+ * To avoid that, we add both resolution and rejection handlers to the promise.
11
+ * That way, the saved promise never rejects. A callback is provided as return
12
+ * value to build a *new* promise, that resolves and rejects along with
13
+ * the original promise.
14
+ * @param promise Promise to wrap, which possibly rejects
15
+ * @returns Callback to build a new promise, which resolves and rejects along
16
+ * with the original promise
17
+ */
18
+ export declare function makeSafePromise<T>(promise: Promise<T>): SafePromise<T>;
@@ -5,8 +5,9 @@ import type { PreferredCodec } from '../types';
5
5
  * @param kind the kind of codec to get.
6
6
  * @param preferredCodec the codec to prioritize (vp8, h264, vp9, av1...).
7
7
  * @param codecToRemove the codec to exclude from the list.
8
+ * @param codecPreferencesSource the source of the codec preferences.
8
9
  */
9
- export declare const getPreferredCodecs: (kind: "audio" | "video", preferredCodec: string, codecToRemove?: string) => RTCRtpCodecCapability[] | undefined;
10
+ export declare const getPreferredCodecs: (kind: "audio" | "video", preferredCodec: string, codecToRemove?: string, codecPreferencesSource?: "sender" | "receiver") => RTCRtpCodec[] | undefined;
10
11
  /**
11
12
  * Returns a generic SDP for the given direction.
12
13
  * We use this SDP to send it as part of our JoinRequest so that the SFU
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -30,6 +30,7 @@ import {
30
30
  import { getLogger, logToConsole, setLogger } from './logger';
31
31
  import { getSdkInfo } from './client-details';
32
32
  import { SdkType } from './gen/video/sfu/models/models';
33
+ import { withoutConcurrency } from './helpers/concurrency';
33
34
 
34
35
  /**
35
36
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
@@ -51,8 +52,9 @@ export class StreamVideoClient {
51
52
  streamClient: StreamClient;
52
53
 
53
54
  protected eventHandlersToUnregister: Array<() => void> = [];
54
- protected connectionPromise: Promise<void | ConnectedEvent> | undefined;
55
- protected disconnectionPromise: Promise<void> | undefined;
55
+ private readonly connectionConcurrencyTag = Symbol(
56
+ 'connectionConcurrencyTag',
57
+ );
56
58
 
57
59
  private static _instanceMap: Map<string, StreamVideoClient> = new Map();
58
60
 
@@ -209,12 +211,11 @@ export class StreamVideoClient {
209
211
  return this.streamClient.connectGuestUser(user);
210
212
  };
211
213
  }
212
- this.connectionPromise = this.disconnectionPromise
213
- ? this.disconnectionPromise.then(() => connectUser())
214
- : connectUser();
215
214
 
216
- this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
217
- const connectUserResponse = await this.connectionPromise;
215
+ const connectUserResponse = await withoutConcurrency(
216
+ this.connectionConcurrencyTag,
217
+ () => connectUser(),
218
+ );
218
219
  // connectUserResponse will be void if connectUser called twice for the same user
219
220
  if (connectUserResponse?.me) {
220
221
  this.writeableStateStore.setConnectedUser(connectUserResponse.me);
@@ -316,19 +317,15 @@ export class StreamVideoClient {
316
317
  * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
317
318
  */
318
319
  disconnectUser = async (timeout?: number) => {
319
- if (!this.streamClient.user && !this.connectionPromise) {
320
+ if (!this.streamClient.user) {
320
321
  return;
321
322
  }
322
323
  const userId = this.streamClient.user?.id;
323
324
  const apiKey = this.streamClient.key;
324
325
  const disconnectUser = () => this.streamClient.disconnectUser(timeout);
325
- this.disconnectionPromise = this.connectionPromise
326
- ? this.connectionPromise.then(() => disconnectUser())
327
- : disconnectUser();
328
- this.disconnectionPromise.finally(
329
- () => (this.disconnectionPromise = undefined),
326
+ await withoutConcurrency(this.connectionConcurrencyTag, () =>
327
+ disconnectUser(),
330
328
  );
331
- await this.disconnectionPromise;
332
329
  if (userId) {
333
330
  StreamVideoClient._instanceMap.delete(apiKey + userId);
334
331
  }
@@ -556,10 +553,8 @@ export class StreamVideoClient {
556
553
  ) => {
557
554
  const connectAnonymousUser = () =>
558
555
  this.streamClient.connectAnonymousUser(user, tokenOrProvider);
559
- this.connectionPromise = this.disconnectionPromise
560
- ? this.disconnectionPromise.then(() => connectAnonymousUser())
561
- : connectAnonymousUser();
562
- this.connectionPromise.finally(() => (this.connectionPromise = undefined));
563
- return this.connectionPromise;
556
+ return await withoutConcurrency(this.connectionConcurrencyTag, () =>
557
+ connectAnonymousUser(),
558
+ );
564
559
  };
565
560
  }
@@ -271,8 +271,8 @@ describe('muting logic', () => {
271
271
  .mockImplementation(() => Promise.resolve({ duration: '0ms' }));
272
272
  });
273
273
 
274
- it('should mute self', () => {
275
- call.muteSelf('audio');
274
+ it('should mute self', async () => {
275
+ await call.muteSelf('audio');
276
276
 
277
277
  expect(spy).toHaveBeenCalledWith(userId, 'audio');
278
278
  });
@@ -38,7 +38,12 @@ import {
38
38
  } from './types';
39
39
  import { InsightMetrics, postInsights } from './insights';
40
40
  import { getLocationHint } from './location';
41
- import { CreateGuestRequest, CreateGuestResponse } from '../../gen/coordinator';
41
+ import {
42
+ ConnectedEvent,
43
+ CreateGuestRequest,
44
+ CreateGuestResponse,
45
+ } from '../../gen/coordinator';
46
+ import { makeSafePromise, type SafePromise } from '../../helpers/promise';
42
47
 
43
48
  export class StreamClient {
44
49
  _user?: UserWithId;
@@ -67,14 +72,14 @@ export class StreamClient {
67
72
  wsBaseURL?: string;
68
73
  wsConnection: StableWSConnection | null;
69
74
  wsFallback?: WSConnectionFallback;
70
- wsPromise: ConnectAPIResponse | null;
75
+ private wsPromiseSafe: SafePromise<ConnectedEvent | undefined> | null;
71
76
  consecutiveFailures: number;
72
77
  insightMetrics: InsightMetrics;
73
78
  defaultWSTimeoutWithFallback: number;
74
79
  defaultWSTimeout: number;
75
80
  resolveConnectionId?: Function;
76
81
  rejectConnectionId?: Function;
77
- connectionIdPromise?: Promise<string | undefined>;
82
+ private connectionIdPromiseSafe?: SafePromise<string | undefined>;
78
83
  guestUserCreatePromise?: Promise<CreateGuestResponse>;
79
84
 
80
85
  /**
@@ -155,7 +160,7 @@ export class StreamClient {
155
160
 
156
161
  // WS connection is initialized when setUser is called
157
162
  this.wsConnection = null;
158
- this.wsPromise = null;
163
+ this.wsPromiseSafe = null;
159
164
  this.setUserPromise = null;
160
165
 
161
166
  // mapping between channel groups and configs
@@ -340,12 +345,13 @@ export class StreamClient {
340
345
  );
341
346
  }
342
347
 
343
- if (this.wsConnection?.isConnecting && this.wsPromise) {
348
+ const wsPromise = this.wsPromiseSafe?.();
349
+ if (this.wsConnection?.isConnecting && wsPromise) {
344
350
  this.logger(
345
351
  'info',
346
352
  'client:openConnection() - connection already in progress',
347
353
  );
348
- return this.wsPromise;
354
+ return await wsPromise;
349
355
  }
350
356
 
351
357
  if (
@@ -357,14 +363,15 @@ export class StreamClient {
357
363
  'client:openConnection() - openConnection called twice, healthy connection already exists',
358
364
  );
359
365
 
360
- return Promise.resolve();
366
+ return;
361
367
  }
362
368
 
363
369
  this._setupConnectionIdPromise();
364
370
 
365
371
  this.clientID = `${this.userID}--${randomId()}`;
366
- this.wsPromise = this.connect();
367
- return this.wsPromise;
372
+ const newWsPromise = this.connect();
373
+ this.wsPromiseSafe = makeSafePromise(newWsPromise);
374
+ return await newWsPromise;
368
375
  };
369
376
 
370
377
  /**
@@ -388,7 +395,7 @@ export class StreamClient {
388
395
 
389
396
  this.tokenManager.reset();
390
397
 
391
- this.connectionIdPromise = undefined;
398
+ this.connectionIdPromiseSafe = undefined;
392
399
  this.rejectConnectionId = undefined;
393
400
  this.resolveConnectionId = undefined;
394
401
  };
@@ -481,16 +488,28 @@ export class StreamClient {
481
488
  /**
482
489
  * sets up the this.connectionIdPromise
483
490
  */
484
- _setupConnectionIdPromise = async () => {
491
+ _setupConnectionIdPromise = () => {
485
492
  /** a promise that is resolved once connection id is set */
486
- this.connectionIdPromise = new Promise<string | undefined>(
487
- (resolve, reject) => {
493
+ this.connectionIdPromiseSafe = makeSafePromise(
494
+ new Promise<string | undefined>((resolve, reject) => {
488
495
  this.resolveConnectionId = resolve;
489
496
  this.rejectConnectionId = reject;
490
- },
497
+ }),
491
498
  );
492
499
  };
493
500
 
501
+ get connectionIdPromise() {
502
+ return this.connectionIdPromiseSafe?.();
503
+ }
504
+
505
+ get isConnectionIsPromisePending() {
506
+ return this.connectionIdPromiseSafe?.checkPending() ?? false;
507
+ }
508
+
509
+ get wsPromise() {
510
+ return this.wsPromiseSafe?.();
511
+ }
512
+
494
513
  _logApiRequest = (
495
514
  type: string,
496
515
  url: string,
@@ -8,20 +8,15 @@ import {
8
8
  import {
9
9
  addConnectionEventListeners,
10
10
  convertErrorToJson,
11
- isPromisePending,
12
11
  KnownCodes,
13
12
  randomId,
14
13
  removeConnectionEventListeners,
15
14
  retryInterval,
16
15
  sleep,
17
16
  } from './utils';
18
- import type {
19
- ConnectAPIResponse,
20
- LogLevel,
21
- StreamVideoEvent,
22
- UR,
23
- } from './types';
17
+ import type { LogLevel, StreamVideoEvent, UR } from './types';
24
18
  import type { ConnectedEvent, WSAuthMessage } from '../../gen/coordinator';
19
+ import { makeSafePromise, type SafePromise } from '../../helpers/promise';
25
20
 
26
21
  // Type guards to check WebSocket error type
27
22
  const isCloseEvent = (
@@ -54,7 +49,7 @@ const isErrorEvent = (
54
49
  export class StableWSConnection {
55
50
  // local vars
56
51
  connectionID?: string;
57
- connectionOpen?: ConnectAPIResponse;
52
+ private connectionOpenSafe?: SafePromise<ConnectedEvent>;
58
53
  authenticationSent: boolean;
59
54
  consecutiveFailures: number;
60
55
  pingInterval: number;
@@ -338,13 +333,7 @@ export class StableWSConnection {
338
333
  await this.client.tokenManager.loadToken();
339
334
  }
340
335
 
341
- let mustSetupConnectionIdPromise = true;
342
- if (this.client.connectionIdPromise) {
343
- if (await isPromisePending(this.client.connectionIdPromise)) {
344
- mustSetupConnectionIdPromise = false;
345
- }
346
- }
347
- if (mustSetupConnectionIdPromise) {
336
+ if (!this.client.isConnectionIsPromisePending) {
348
337
  this.client._setupConnectionIdPromise();
349
338
  }
350
339
  this._setupConnectionPromise();
@@ -747,12 +736,18 @@ export class StableWSConnection {
747
736
  _setupConnectionPromise = () => {
748
737
  this.isResolved = false;
749
738
  /** a promise that is resolved once ws.open is called */
750
- this.connectionOpen = new Promise<ConnectedEvent>((resolve, reject) => {
751
- this.resolvePromise = resolve;
752
- this.rejectPromise = reject;
753
- });
739
+ this.connectionOpenSafe = makeSafePromise(
740
+ new Promise<ConnectedEvent>((resolve, reject) => {
741
+ this.resolvePromise = resolve;
742
+ this.rejectPromise = reject;
743
+ }),
744
+ );
754
745
  };
755
746
 
747
+ get connectionOpen() {
748
+ return this.connectionOpenSafe?.();
749
+ }
750
+
756
751
  /**
757
752
  * Schedules a next health check ping for websocket.
758
753
  */
@@ -0,0 +1,47 @@
1
+ export interface SafePromise<T> {
2
+ (): Promise<T>;
3
+ checkPending(): boolean;
4
+ }
5
+
6
+ type Fulfillment<T> =
7
+ | {
8
+ status: 'resolved';
9
+ result: T;
10
+ }
11
+ | {
12
+ status: 'rejected';
13
+ error: unknown;
14
+ };
15
+
16
+ /**
17
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
18
+ * since rejecting the promise causes an unhandled rejection error (even if the
19
+ * rejection is handled everywhere promise result is expected).
20
+ *
21
+ * To avoid that, we add both resolution and rejection handlers to the promise.
22
+ * That way, the saved promise never rejects. A callback is provided as return
23
+ * value to build a *new* promise, that resolves and rejects along with
24
+ * the original promise.
25
+ * @param promise Promise to wrap, which possibly rejects
26
+ * @returns Callback to build a new promise, which resolves and rejects along
27
+ * with the original promise
28
+ */
29
+ export function makeSafePromise<T>(promise: Promise<T>): SafePromise<T> {
30
+ let isPending = true;
31
+
32
+ const safePromise: Promise<Fulfillment<T>> = promise
33
+ .then(
34
+ (result) => ({ status: 'resolved' as const, result }),
35
+ (error) => ({ status: 'rejected' as const, error }),
36
+ )
37
+ .finally(() => (isPending = false));
38
+
39
+ const unwrapPromise = () =>
40
+ safePromise.then((fulfillment) => {
41
+ if (fulfillment.status === 'rejected') throw fulfillment.error;
42
+ return fulfillment.result;
43
+ });
44
+
45
+ unwrapPromise.checkPending = () => isPending;
46
+ return unwrapPromise;
47
+ }
@@ -459,9 +459,14 @@ export class Publisher {
459
459
  private getCodecPreferences = (
460
460
  trackType: TrackType,
461
461
  preferredCodec?: string,
462
+ codecPreferencesSource?: 'sender' | 'receiver',
462
463
  ) => {
463
464
  if (trackType === TrackType.VIDEO) {
464
- return getPreferredCodecs('video', preferredCodec || 'vp8');
465
+ return getPreferredCodecs(
466
+ 'video',
467
+ preferredCodec || 'vp8',
468
+ codecPreferencesSource,
469
+ );
465
470
  }
466
471
  if (trackType === TrackType.AUDIO) {
467
472
  const defaultAudioCodec = this.isRedEnabled ? 'red' : 'opus';
@@ -575,8 +580,8 @@ export class Publisher {
575
580
  const opts = this.publishOptsForTrack.get(trackType);
576
581
  if (!opts || !opts.forceSingleCodec) return sdp;
577
582
 
578
- const codec = opts.forceCodec || opts.preferredCodec;
579
- const orderedCodecs = this.getCodecPreferences(trackType, codec);
583
+ const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec);
584
+ const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender');
580
585
  if (!orderedCodecs || orderedCodecs.length === 0) return sdp;
581
586
 
582
587
  const transceiver = this.transceiverCache.get(trackType);
package/src/rtc/codecs.ts CHANGED
@@ -9,15 +9,19 @@ import type { PreferredCodec } from '../types';
9
9
  * @param kind the kind of codec to get.
10
10
  * @param preferredCodec the codec to prioritize (vp8, h264, vp9, av1...).
11
11
  * @param codecToRemove the codec to exclude from the list.
12
+ * @param codecPreferencesSource the source of the codec preferences.
12
13
  */
13
14
  export const getPreferredCodecs = (
14
15
  kind: 'audio' | 'video',
15
16
  preferredCodec: string,
16
17
  codecToRemove?: string,
17
- ): RTCRtpCodecCapability[] | undefined => {
18
- if (!('getCapabilities' in RTCRtpReceiver)) return;
18
+ codecPreferencesSource: 'sender' | 'receiver' = 'receiver',
19
+ ): RTCRtpCodec[] | undefined => {
20
+ const source =
21
+ codecPreferencesSource === 'receiver' ? RTCRtpReceiver : RTCRtpSender;
22
+ if (!('getCapabilities' in source)) return;
19
23
 
20
- const capabilities = RTCRtpReceiver.getCapabilities(kind);
24
+ const capabilities = source.getCapabilities(kind);
21
25
  if (!capabilities) return;
22
26
 
23
27
  const preferred: RTCRtpCodecCapability[] = [];