@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +77 -569
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +62 -554
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +77 -569
- package/dist/index.es.js.map +1 -1
- package/dist/src/coordinator/connection/client.d.ts +0 -18
- package/dist/src/coordinator/connection/connection.d.ts +4 -12
- package/dist/src/coordinator/connection/signing.d.ts +1 -7
- package/dist/src/coordinator/connection/token_manager.d.ts +0 -2
- package/dist/src/coordinator/connection/types.d.ts +5 -6
- package/dist/src/coordinator/connection/utils.d.ts +6 -8
- package/dist/src/rtc/codecs.d.ts +2 -1
- package/package.json +6 -10
- package/src/__tests__/Call.test.ts +3 -2
- package/src/coordinator/connection/client.ts +12 -149
- package/src/coordinator/connection/connection.ts +40 -109
- package/src/coordinator/connection/signing.ts +31 -17
- package/src/coordinator/connection/token_manager.ts +3 -9
- package/src/coordinator/connection/types.ts +5 -9
- package/src/coordinator/connection/utils.ts +18 -50
- package/src/devices/__tests__/InputMediaDeviceManagerState.test.ts +13 -8
- package/src/devices/__tests__/mocks.ts +0 -4
- package/src/rtc/Publisher.ts +8 -3
- package/src/rtc/codecs.ts +7 -3
- package/dist/src/coordinator/connection/base64.d.ts +0 -2
- package/dist/src/coordinator/connection/connection_fallback.d.ts +0 -39
- package/dist/src/coordinator/connection/errors.d.ts +0 -16
- package/dist/src/coordinator/connection/insights.d.ts +0 -57
- package/src/coordinator/connection/base64.ts +0 -80
- package/src/coordinator/connection/connection_fallback.ts +0 -242
- package/src/coordinator/connection/errors.ts +0 -80
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
247
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
)
|
|
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:
|
|
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?.(
|
|
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:
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 {
|
|
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)
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/rtc/Publisher.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
18
|
-
|
|
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 =
|
|
24
|
+
const capabilities = source.getCapabilities(kind);
|
|
21
25
|
if (!capabilities) return;
|
|
22
26
|
|
|
23
27
|
const preferred: RTCRtpCodecCapability[] = [];
|