@stream-io/video-client 1.11.5 → 1.11.7

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +77 -569
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +62 -554
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +77 -569
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/coordinator/connection/client.d.ts +0 -18
  9. package/dist/src/coordinator/connection/connection.d.ts +4 -12
  10. package/dist/src/coordinator/connection/signing.d.ts +1 -7
  11. package/dist/src/coordinator/connection/token_manager.d.ts +0 -2
  12. package/dist/src/coordinator/connection/types.d.ts +5 -6
  13. package/dist/src/coordinator/connection/utils.d.ts +6 -8
  14. package/dist/src/rtc/codecs.d.ts +2 -1
  15. package/package.json +6 -10
  16. package/src/__tests__/Call.test.ts +3 -2
  17. package/src/coordinator/connection/client.ts +12 -149
  18. package/src/coordinator/connection/connection.ts +40 -109
  19. package/src/coordinator/connection/signing.ts +31 -17
  20. package/src/coordinator/connection/token_manager.ts +3 -9
  21. package/src/coordinator/connection/types.ts +5 -9
  22. package/src/coordinator/connection/utils.ts +18 -50
  23. package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +13 -8
  24. package/src/devices/__tests__/mocks.ts +0 -4
  25. package/src/rtc/Publisher.ts +8 -3
  26. package/src/rtc/codecs.ts +7 -3
  27. package/dist/src/coordinator/connection/base64.d.ts +0 -2
  28. package/dist/src/coordinator/connection/connection_fallback.d.ts +0 -39
  29. package/dist/src/coordinator/connection/errors.d.ts +0 -16
  30. package/dist/src/coordinator/connection/insights.d.ts +0 -57
  31. package/src/coordinator/connection/base64.ts +0 -80
  32. package/src/coordinator/connection/connection_fallback.ts +0 -242
  33. package/src/coordinator/connection/errors.ts +0 -80
  34. package/src/coordinator/connection/insights.ts +0 -88
@@ -1,13 +1,7 @@
1
- import WebSocket from 'isomorphic-ws';
2
1
  import { StreamClient } from './client';
3
- import {
4
- buildWsFatalInsight,
5
- buildWsSuccessAfterFailureInsight,
6
- postInsights,
7
- } from './insights';
8
2
  import {
9
3
  addConnectionEventListeners,
10
- convertErrorToJson,
4
+ isCloseEvent,
11
5
  KnownCodes,
12
6
  randomId,
13
7
  removeConnectionEventListeners,
@@ -15,20 +9,13 @@ import {
15
9
  sleep,
16
10
  } from './utils';
17
11
  import type { LogLevel, StreamVideoEvent, UR } from './types';
18
- import type { ConnectedEvent, WSAuthMessage } from '../../gen/coordinator';
12
+ import type {
13
+ ConnectedEvent,
14
+ ConnectionErrorEvent,
15
+ WSAuthMessage,
16
+ } from '../../gen/coordinator';
19
17
  import { makeSafePromise, type SafePromise } from '../../helpers/promise';
20
18
 
21
- // Type guards to check WebSocket error type
22
- const isCloseEvent = (
23
- res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
24
- ): res is WebSocket.CloseEvent =>
25
- (res as WebSocket.CloseEvent).code !== undefined;
26
-
27
- const isErrorEvent = (
28
- res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
29
- ): res is WebSocket.ErrorEvent =>
30
- (res as WebSocket.ErrorEvent).error !== undefined;
31
-
32
19
  /**
33
20
  * StableWSConnection - A WS connection that reconnects upon failure.
34
21
  * - the browser will sometimes report that you're online or offline
@@ -50,7 +37,6 @@ export class StableWSConnection {
50
37
  // local vars
51
38
  connectionID?: string;
52
39
  private connectionOpenSafe?: SafePromise<ConnectedEvent>;
53
- authenticationSent: boolean;
54
40
  consecutiveFailures: number;
55
41
  pingInterval: number;
56
42
  healthCheckTimeoutRef?: NodeJS.Timeout;
@@ -84,8 +70,6 @@ export class StableWSConnection {
84
70
  this.totalFailures = 0;
85
71
  /** We only make 1 attempt to reconnect at the same time.. */
86
72
  this.isConnecting = false;
87
- /** True after the auth payload is sent to the server */
88
- this.authenticationSent = false;
89
73
  /** To avoid reconnect if client is disconnected */
90
74
  this.isDisconnected = false;
91
75
  /** Boolean that indicates if the connection promise is resolved */
@@ -219,12 +203,9 @@ export class StableWSConnection {
219
203
  */
220
204
  _buildUrl = () => {
221
205
  const params = new URLSearchParams();
222
- // const qs = encodeURIComponent(this.client._buildWSPayload(this.requestID));
223
- // params.set('json', qs);
224
206
  params.set('api_key', this.client.key);
225
207
  params.set('stream-auth-type', this.client.getAuthType());
226
208
  params.set('X-Stream-Client', this.client.getUserAgent());
227
- // params.append('authorization', this.client._getToken()!);
228
209
 
229
210
  return `${this.client.wsBaseURL}/connect?${params.toString()}`;
230
211
  };
@@ -243,22 +224,13 @@ export class StableWSConnection {
243
224
  this.isDisconnected = true;
244
225
 
245
226
  // start by removing all the listeners
246
- if (this.healthCheckTimeoutRef) {
247
- clearInterval(this.healthCheckTimeoutRef);
248
- }
249
- if (this.connectionCheckTimeoutRef) {
250
- clearInterval(this.connectionCheckTimeoutRef);
251
- }
227
+ clearInterval(this.healthCheckTimeoutRef);
228
+ clearInterval(this.connectionCheckTimeoutRef);
252
229
 
253
230
  removeConnectionEventListeners(this.onlineStatusChanged);
254
231
 
255
232
  this.isHealthy = false;
256
233
 
257
- // remove ws handlers...
258
- if (this.ws && this.ws.removeAllListeners) {
259
- this.ws.removeAllListeners();
260
- }
261
-
262
234
  let isClosedPromise: Promise<void>;
263
235
  // and finally close...
264
236
  // Assigning to local here because we will remove it from this before the
@@ -266,7 +238,7 @@ export class StableWSConnection {
266
238
  const { ws } = this;
267
239
  if (ws && ws.close && ws.readyState === ws.OPEN) {
268
240
  isClosedPromise = new Promise((resolve) => {
269
- const onclose = (event: WebSocket.CloseEvent) => {
241
+ const onclose = (event: CloseEvent) => {
270
242
  this._log(
271
243
  `disconnect() - resolving isClosedPromise ${
272
244
  event ? 'with' : 'without'
@@ -308,14 +280,9 @@ export class StableWSConnection {
308
280
  * @return {ConnectAPIResponse<ConnectedEvent>} Promise that completes once the first health check message is received
309
281
  */
310
282
  async _connect() {
311
- if (
312
- this.isConnecting ||
313
- (this.isDisconnected && this.client.options.enableWSFallback)
314
- )
315
- return; // simply ignore _connect if it's currently trying to connect
283
+ if (this.isConnecting) return; // simply ignore _connect if it's currently trying to connect
316
284
  this.isConnecting = true;
317
285
  this.requestID = randomId();
318
- this.client.insightMetrics.connectionStartTimestamp = new Date().getTime();
319
286
  let isTokenReady = false;
320
287
  try {
321
288
  this._log(`_connect() - waiting for token`);
@@ -342,7 +309,8 @@ export class StableWSConnection {
342
309
  wsURL,
343
310
  requestID: this.requestID,
344
311
  });
345
- this.ws = new WebSocket(wsURL);
312
+ const WS = this.client.options.WebSocketImpl ?? WebSocket;
313
+ this.ws = new WS(wsURL);
346
314
  this.ws.onopen = this.onopen.bind(this, this.wsID);
347
315
  this.ws.onclose = this.onclose.bind(this, this.wsID);
348
316
  this.ws.onerror = this.onerror.bind(this, this.wsID);
@@ -353,18 +321,6 @@ export class StableWSConnection {
353
321
  if (response) {
354
322
  this.connectionID = response.connection_id;
355
323
  this.client.resolveConnectionId?.(this.connectionID);
356
- if (
357
- this.client.insightMetrics.wsConsecutiveFailures > 0 &&
358
- this.client.options.enableInsights
359
- ) {
360
- postInsights(
361
- 'ws_success_after_failure',
362
- buildWsSuccessAfterFailureInsight(
363
- this as unknown as StableWSConnection,
364
- ),
365
- );
366
- this.client.insightMetrics.wsConsecutiveFailures = 0;
367
- }
368
324
  return response;
369
325
  }
370
326
  } catch (err) {
@@ -372,16 +328,6 @@ export class StableWSConnection {
372
328
  this.isConnecting = false;
373
329
  // @ts-ignore
374
330
  this._log(`_connect() - Error - `, err);
375
- if (this.client.options.enableInsights) {
376
- this.client.insightMetrics.wsConsecutiveFailures++;
377
- this.client.insightMetrics.wsTotalFailures++;
378
-
379
- const insights = buildWsFatalInsight(
380
- this as unknown as StableWSConnection,
381
- convertErrorToJson(err as Error),
382
- );
383
- postInsights?.('ws_fatal', insights);
384
- }
385
331
  this.client.rejectConnectionId?.(err);
386
332
  throw err;
387
333
  }
@@ -422,7 +368,7 @@ export class StableWSConnection {
422
368
  return;
423
369
  }
424
370
 
425
- if (this.isDisconnected && this.client.options.enableWSFallback) {
371
+ if (this.isDisconnected) {
426
372
  this._log('_reconnect() - Abort (3) since disconnect() is called');
427
373
  return;
428
374
  }
@@ -518,12 +464,11 @@ export class StableWSConnection {
518
464
  },
519
465
  };
520
466
 
521
- this.authenticationSent = true;
522
467
  this.ws?.send(JSON.stringify(authMessage));
523
468
  this._log('onopen() - onopen callback', { wsID });
524
469
  };
525
470
 
526
- onmessage = (wsID: number, event: WebSocket.MessageEvent) => {
471
+ onmessage = (wsID: number, event: MessageEvent) => {
527
472
  if (this.wsID !== wsID) return;
528
473
 
529
474
  this._log('onmessage() - onmessage callback', { event, wsID });
@@ -538,7 +483,6 @@ export class StableWSConnection {
538
483
  if (!this.isResolved && data && data.type === 'connection.error') {
539
484
  this.isResolved = true;
540
485
  if (data.error) {
541
- // @ts-expect-error - the types of _errorFromWSEvent are incorrect
542
486
  this.rejectPromise?.(this._errorFromWSEvent(data, false));
543
487
  return;
544
488
  }
@@ -584,7 +528,7 @@ export class StableWSConnection {
584
528
  this.scheduleConnectionCheck();
585
529
  };
586
530
 
587
- onclose = (wsID: number, event: WebSocket.CloseEvent) => {
531
+ onclose = (wsID: number, event: CloseEvent) => {
588
532
  if (this.wsID !== wsID) return;
589
533
 
590
534
  this._log('onclose() - onclose callback - ' + event.code, { event, wsID });
@@ -594,11 +538,15 @@ export class StableWSConnection {
594
538
  // usually caused by invalid auth details
595
539
  const error = new Error(
596
540
  `WS connection reject with error ${event.reason}`,
597
- ) as Error & WebSocket.CloseEvent;
541
+ );
598
542
 
543
+ // @ts-expect-error
599
544
  error.reason = event.reason;
545
+ // @ts-expect-error
600
546
  error.code = event.code;
547
+ // @ts-expect-error
601
548
  error.wasClean = event.wasClean;
549
+ // @ts-expect-error
602
550
  error.target = event.target;
603
551
 
604
552
  this.rejectPromise?.(error);
@@ -622,14 +570,14 @@ export class StableWSConnection {
622
570
  }
623
571
  };
624
572
 
625
- onerror = (wsID: number, event: WebSocket.ErrorEvent) => {
573
+ onerror = (wsID: number, event: Event) => {
626
574
  if (this.wsID !== wsID) return;
627
575
 
628
576
  this.consecutiveFailures += 1;
629
577
  this.totalFailures += 1;
630
578
  this._setHealth(false);
631
579
  this.isConnecting = false;
632
- this.rejectPromise?.(this._errorFromWSEvent(event));
580
+ this.rejectPromise?.(new Error(`WebSocket error: ${event}`));
633
581
  this._log(`onerror() - WS connection resulted into error`, { event });
634
582
 
635
583
  this._reconnect();
@@ -641,7 +589,6 @@ export class StableWSConnection {
641
589
  *
642
590
  * @param {boolean} healthy boolean indicating if the connection is healthy or not
643
591
  * @param {boolean} dispatchImmediately boolean indicating to dispatch event immediately even if the connection is unhealthy
644
- *
645
592
  */
646
593
  _setHealth = (healthy: boolean, dispatchImmediately = false) => {
647
594
  if (healthy === this.isHealthy) return;
@@ -668,40 +615,31 @@ export class StableWSConnection {
668
615
 
669
616
  /**
670
617
  * _errorFromWSEvent - Creates an error object for the WS event
671
- *
672
618
  */
673
- _errorFromWSEvent = (
674
- event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
619
+ private _errorFromWSEvent = (
620
+ event: CloseEvent | ConnectionErrorEvent,
675
621
  isWSFailure = true,
676
622
  ) => {
677
- let code;
678
- let statusCode;
679
- let message;
623
+ let code: number;
624
+ let statusCode: number;
625
+ let message: string;
680
626
  if (isCloseEvent(event)) {
681
627
  code = event.code;
682
- statusCode = 'unknown';
683
628
  message = event.reason;
629
+ statusCode = 0;
630
+ } else {
631
+ const { error } = event;
632
+ code = error.code;
633
+ message = error.message;
634
+ statusCode = error.StatusCode;
684
635
  }
685
636
 
686
- if (isErrorEvent(event)) {
687
- code = event.error.code;
688
- statusCode = event.error.StatusCode;
689
- message = event.error.message;
690
- }
691
-
692
- // Keeping this `warn` level log, to avoid cluttering of error logs from ws failures.
693
- this._log(
694
- `_errorFromWSEvent() - WS failed with code ${code}`,
695
- { event },
696
- 'warn',
697
- );
698
-
699
- const error = new Error(
700
- `WS failed with code ${code} and reason - ${message}`,
701
- ) as Error & {
702
- code?: string | number;
637
+ const msg = `WS failed with code: ${code} and reason: ${message}`;
638
+ this._log(msg, { event }, 'warn');
639
+ const error = new Error(msg) as Error & {
640
+ code?: number;
703
641
  isWSFailure?: boolean;
704
- StatusCode?: string | number;
642
+ StatusCode?: number;
705
643
  };
706
644
  error.code = code;
707
645
  /**
@@ -723,7 +661,6 @@ export class StableWSConnection {
723
661
  this.wsID += 1;
724
662
 
725
663
  try {
726
- this?.ws?.removeAllListeners();
727
664
  this?.ws?.close();
728
665
  } catch (e) {
729
666
  // we don't care
@@ -752,11 +689,8 @@ export class StableWSConnection {
752
689
  * Schedules a next health check ping for websocket.
753
690
  */
754
691
  scheduleNextPing = () => {
755
- if (this.healthCheckTimeoutRef) {
756
- clearTimeout(this.healthCheckTimeoutRef);
757
- }
758
-
759
692
  // 30 seconds is the recommended interval (messenger uses this)
693
+ clearTimeout(this.healthCheckTimeoutRef);
760
694
  this.healthCheckTimeoutRef = setTimeout(() => {
761
695
  // send the healthcheck..., server replies with a health check event
762
696
  const data = [{ type: 'health.check', client_id: this.client.clientID }];
@@ -775,10 +709,7 @@ export class StableWSConnection {
775
709
  * to be reconnected.
776
710
  */
777
711
  scheduleConnectionCheck = () => {
778
- if (this.connectionCheckTimeoutRef) {
779
- clearTimeout(this.connectionCheckTimeoutRef);
780
- }
781
-
712
+ clearTimeout(this.connectionCheckTimeoutRef);
782
713
  this.connectionCheckTimeoutRef = setTimeout(() => {
783
714
  const now = new Date();
784
715
  if (
@@ -1,19 +1,4 @@
1
- import { decodeBase64, encodeBase64 } from './base64';
2
-
3
- /**
4
- *
5
- * @param {string} userId the id of the user
6
- * @return {string}
7
- */
8
- export function DevToken(userId: string) {
9
- return [
10
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', //{"alg": "HS256", "typ": "JWT"}
11
- encodeBase64(JSON.stringify({ user_id: userId })),
12
- 'devtoken', // hardcoded signature
13
- ].join('.');
14
- }
15
-
16
- export function UserFromToken(token: string) {
1
+ export function getUserFromToken(token: string) {
17
2
  const fragments = token.split('.');
18
3
  if (fragments.length !== 3) {
19
4
  return '';
@@ -21,5 +6,34 @@ export function UserFromToken(token: string) {
21
6
  const b64Payload = fragments[1];
22
7
  const payload = decodeBase64(b64Payload);
23
8
  const data = JSON.parse(payload);
24
- return data.user_id as string;
9
+ return data.user_id as string | undefined;
25
10
  }
11
+
12
+ // base-64 decoder throws exception if encoded string is not padded by '=' to make string length
13
+ // in multiples of 4. So gonna use our own method for this purpose to keep backwards compatibility
14
+ // https://github.com/beatgammit/base64-js/blob/master/index.js#L26
15
+ const decodeBase64 = (s: string): string => {
16
+ const e = {} as { [key: string]: number },
17
+ w = String.fromCharCode,
18
+ L = s.length;
19
+ let i,
20
+ b = 0,
21
+ c,
22
+ x,
23
+ l = 0,
24
+ a,
25
+ r = '';
26
+ const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
27
+ for (i = 0; i < 64; i++) {
28
+ e[A.charAt(i)] = i;
29
+ }
30
+ for (x = 0; x < L; x++) {
31
+ c = e[s.charAt(x)];
32
+ b = (b << 6) + c;
33
+ l += 6;
34
+ while (l >= 8) {
35
+ ((a = (b >>> (l -= 8)) & 0xff) || x < L - 2) && (r += w(a));
36
+ }
37
+ }
38
+ return r;
39
+ };
@@ -1,4 +1,4 @@
1
- import { UserFromToken } from './signing';
1
+ import { getUserFromToken } from './signing';
2
2
  import { isFunction } from './utils';
3
3
  import type { TokenOrProvider, UserWithId } from './types';
4
4
 
@@ -16,15 +16,10 @@ export class TokenManager {
16
16
  user?: UserWithId;
17
17
  /**
18
18
  * Constructor
19
- *
20
- * @param {Secret} secret
21
19
  */
22
20
  constructor(secret?: string) {
23
21
  this.loadTokenPromise = null;
24
- if (secret) {
25
- this.secret = secret;
26
- }
27
-
22
+ this.secret = secret;
28
23
  this.type = 'static';
29
24
  }
30
25
 
@@ -95,7 +90,7 @@ export class TokenManager {
95
90
  // Allow empty token for anonymous users
96
91
  if (isAnonymous && tokenOrProvider === '') return;
97
92
 
98
- const tokenUserId = UserFromToken(tokenOrProvider);
93
+ const tokenUserId = getUserFromToken(tokenOrProvider);
99
94
  if (
100
95
  tokenOrProvider != null &&
101
96
  (tokenUserId == null ||
@@ -116,7 +111,6 @@ export class TokenManager {
116
111
  // Fetches a token from tokenProvider function and sets in tokenManager.
117
112
  // In case of static token, it will simply resolve to static token.
118
113
  loadToken = () => {
119
- // eslint-disable-next-line no-async-promise-executor
120
114
  this.loadTokenPromise = new Promise(async (resolve, reject) => {
121
115
  if (this.type === 'static') {
122
116
  return resolve(this.token as string);
@@ -1,5 +1,4 @@
1
1
  import { AxiosRequestConfig, AxiosResponse } from 'axios';
2
- import { StableWSConnection } from './connection';
3
2
  import { ConnectedEvent, UserRequest, WSEvent } from '../../gen/coordinator';
4
3
  import { AllSfuEvents } from '../../rtc';
5
4
 
@@ -115,10 +114,6 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
115
114
  */
116
115
  baseURL?: string;
117
116
  browser?: boolean;
118
- // device?: BaseDeviceFields;
119
- enableInsights?: boolean;
120
- /** experimental feature, please contact support if you want this feature enabled for you */
121
- enableWSFallback?: boolean;
122
117
  logger?: Logger;
123
118
  logLevel?: LogLevel;
124
119
  /**
@@ -146,10 +141,11 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
146
141
  */
147
142
  secret?: string;
148
143
 
149
- warmUp?: boolean;
150
- // Set the instance of StableWSConnection on chat client. Its purely for testing purpose and should
151
- // not be used in production apps.
152
- wsConnection?: StableWSConnection;
144
+ /**
145
+ * The WebSocket implementation to use. This is mainly useful for testing.
146
+ * In Node.js environment, you can use the `ws` package.
147
+ */
148
+ WebSocketImpl?: typeof WebSocket;
153
149
  };
154
150
 
155
151
  export type TokenProvider = () => Promise<string>;
@@ -1,7 +1,8 @@
1
- import { Logger } from './types';
1
+ import type { AxiosResponse } from 'axios';
2
+ import type { APIErrorResponse } from './types';
3
+ import type { ConnectionErrorEvent } from '../../gen/coordinator';
2
4
 
3
- export const sleep = (m: number): Promise<void> =>
4
- new Promise((r) => setTimeout(r, m));
5
+ export const sleep = (m: number) => new Promise((r) => setTimeout(r, m));
5
6
 
6
7
  export function isFunction<T>(value: Function | T): value is Function {
7
8
  return (
@@ -92,63 +93,17 @@ function getRandomBytes(length: number): Uint8Array {
92
93
  return bytes;
93
94
  }
94
95
 
95
- export function convertErrorToJson(err: Error) {
96
- const jsonObj = {} as Record<string, unknown>;
97
-
98
- if (!err) return jsonObj;
99
-
100
- try {
101
- Object.getOwnPropertyNames(err).forEach((key) => {
102
- jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
103
- });
104
- } catch (_) {
105
- return {
106
- error: 'failed to serialize the error',
107
- };
108
- }
109
-
110
- return jsonObj;
111
- }
112
-
113
96
  /**
114
97
  * Informs if a promise is yet to be resolved or rejected
115
98
  */
116
99
  export async function isPromisePending<T>(promise: Promise<T>) {
117
100
  const emptyObj = {};
118
101
  return Promise.race([promise, emptyObj]).then(
119
- (value) => (value === emptyObj ? true : false),
102
+ (value) => value === emptyObj,
120
103
  () => false,
121
104
  );
122
105
  }
123
106
 
124
- /**
125
- * isOnline safely return the navigator.online value for browser env
126
- * if navigator is not in global object, it always return true
127
- */
128
- export function isOnline(logger: Logger) {
129
- const nav =
130
- typeof navigator !== 'undefined'
131
- ? navigator
132
- : typeof window !== 'undefined' && window.navigator
133
- ? window.navigator
134
- : undefined;
135
-
136
- if (!nav) {
137
- logger(
138
- 'warn',
139
- 'isOnline failed to access window.navigator and assume browser is online',
140
- );
141
- return true;
142
- }
143
-
144
- // RN navigator has undefined for onLine
145
- if (typeof nav.onLine !== 'boolean') {
146
- return true;
147
- }
148
-
149
- return nav.onLine;
150
- }
151
-
152
107
  /**
153
108
  * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
154
109
  */
@@ -165,3 +120,16 @@ export function removeConnectionEventListeners(cb: (e: Event) => void) {
165
120
  window.removeEventListener('online', cb);
166
121
  }
167
122
  }
123
+
124
+ export function isErrorResponse(
125
+ res: AxiosResponse<unknown>,
126
+ ): res is AxiosResponse<APIErrorResponse> {
127
+ return !res.status || res.status < 200 || 300 <= res.status;
128
+ }
129
+
130
+ // Type guards to check WebSocket error type
131
+ export function isCloseEvent(
132
+ res: CloseEvent | ConnectionErrorEvent,
133
+ ): res is CloseEvent {
134
+ return (res as CloseEvent).code !== undefined;
135
+ }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { beforeAll, describe, expect, it, vi } from 'vitest';
2
2
  import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
3
3
  import { firstValueFrom } from 'rxjs';
4
4
  import { BrowserPermission } from '../BrowserPermission';
@@ -27,10 +27,18 @@ function mockPermissionStatus(state: PermissionState): PermissionStatus {
27
27
 
28
28
  describe('InputMediaDeviceManagerState', () => {
29
29
  describe('hasBrowserPermission', () => {
30
+ beforeAll(() => {
31
+ Object.defineProperty(navigator, 'permissions', {
32
+ value: {
33
+ query: vi.fn(),
34
+ },
35
+ });
36
+ });
37
+
30
38
  it('should emit true when permission is granted', async () => {
31
39
  const permissionStatus = mockPermissionStatus('granted');
32
40
  const query = vi.fn(() => Promise.resolve(permissionStatus));
33
- globalThis.navigator = { permissions: { query } } as any;
41
+ vi.spyOn(navigator.permissions, 'query').mockImplementation(query);
34
42
  const state = new TestInputMediaDeviceManagerState();
35
43
 
36
44
  const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
@@ -43,7 +51,7 @@ describe('InputMediaDeviceManagerState', () => {
43
51
  it('should emit false when permission is denied', async () => {
44
52
  const permissionStatus = mockPermissionStatus('denied');
45
53
  const query = vi.fn(() => Promise.resolve(permissionStatus));
46
- globalThis.navigator = { permissions: { query } } as any;
54
+ vi.spyOn(navigator.permissions, 'query').mockImplementation(query);
47
55
  const state = new TestInputMediaDeviceManagerState();
48
56
 
49
57
  const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
@@ -56,7 +64,7 @@ describe('InputMediaDeviceManagerState', () => {
56
64
  it('should emit true when prompt is needed', async () => {
57
65
  const permissionStatus = mockPermissionStatus('prompt');
58
66
  const query = vi.fn(() => Promise.resolve(permissionStatus));
59
- globalThis.navigator = { permissions: { query } } as any;
67
+ vi.spyOn(navigator.permissions, 'query').mockImplementation(query);
60
68
  const state = new TestInputMediaDeviceManagerState();
61
69
 
62
70
  const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
@@ -68,7 +76,7 @@ describe('InputMediaDeviceManagerState', () => {
68
76
 
69
77
  it('should emit true when permissions cannot be queried', async () => {
70
78
  const query = vi.fn(() => Promise.reject());
71
- globalThis.navigator = { permissions: { query } } as any;
79
+ vi.spyOn(navigator.permissions, 'query').mockImplementation(query);
72
80
  const state = new TestInputMediaDeviceManagerState();
73
81
 
74
82
  const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
@@ -78,11 +86,8 @@ describe('InputMediaDeviceManagerState', () => {
78
86
  });
79
87
 
80
88
  it('should emit true when permissions API is unavailable', async () => {
81
- globalThis.navigator = {} as any;
82
89
  const state = new TestInputMediaDeviceManagerState();
83
-
84
90
  const hasPermission = await firstValueFrom(state.hasBrowserPermission$);
85
-
86
91
  expect(hasPermission).toBe(true);
87
92
  });
88
93
  });
@@ -207,10 +207,6 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
207
207
 
208
208
  let deviceIds: Subject<MediaDeviceInfo[]>;
209
209
  export const mockDeviceIds$ = () => {
210
- global.navigator = {
211
- //@ts-expect-error
212
- mediaDevices: {},
213
- };
214
210
  deviceIds = new Subject();
215
211
  return deviceIds;
216
212
  };
@@ -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[] = [];
@@ -1,2 +0,0 @@
1
- export declare const encodeBase64: (data: string) => string;
2
- export declare const decodeBase64: (s: string) => string;