@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +70 -51
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +70 -51
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +70 -51
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamVideoClient.d.ts +2 -3
- package/dist/src/coordinator/connection/client.d.ts +11 -8
- package/dist/src/coordinator/connection/connection.d.ts +5 -4
- package/dist/src/helpers/promise.d.ts +18 -0
- package/dist/src/rtc/codecs.d.ts +2 -1
- package/package.json +1 -1
- package/src/StreamVideoClient.ts +14 -19
- package/src/__tests__/Call.test.ts +2 -2
- package/src/coordinator/connection/client.ts +33 -14
- package/src/coordinator/connection/connection.ts +14 -19
- package/src/helpers/promise.ts +47 -0
- package/src/rtc/Publisher.ts +8 -3
- package/src/rtc/codecs.ts +7 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
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<
|
|
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 |
|
|
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: () =>
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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>;
|
package/dist/src/rtc/codecs.d.ts
CHANGED
|
@@ -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) =>
|
|
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
package/src/StreamVideoClient.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
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.
|
|
326
|
-
|
|
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.
|
|
560
|
-
|
|
561
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
366
|
+
return;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
this._setupConnectionIdPromise();
|
|
364
370
|
|
|
365
371
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
366
|
-
|
|
367
|
-
|
|
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.
|
|
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 =
|
|
491
|
+
_setupConnectionIdPromise = () => {
|
|
485
492
|
/** a promise that is resolved once connection id is set */
|
|
486
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
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[] = [];
|