livekit-client 2.15.16 → 2.16.1
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/README.md +105 -1
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +1 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1175 -1341
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/options.d.ts +4 -1
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +5 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts +7 -0
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +3 -3
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +74 -5
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +10 -2
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +0 -4
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +0 -6
- package/dist/src/room/track/processor/types.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -1
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -4
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/mocks.d.ts.map +1 -1
- package/dist/ts4.2/api/utils.d.ts +1 -0
- package/dist/ts4.2/options.d.ts +4 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +5 -0
- package/dist/ts4.2/room/RegionUrlProvider.d.ts +7 -0
- package/dist/ts4.2/room/Room.d.ts +1 -1
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +3 -3
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -1
- package/dist/ts4.2/room/errors.d.ts +74 -5
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +0 -4
- package/dist/ts4.2/room/track/processor/types.d.ts +0 -6
- package/dist/ts4.2/room/types.d.ts +1 -1
- package/dist/ts4.2/room/utils.d.ts +3 -3
- package/package.json +10 -6
- package/src/api/SignalClient.test.ts +12 -19
- package/src/api/SignalClient.ts +13 -28
- package/src/api/utils.ts +1 -1
- package/src/connectionHelper/checks/turn.ts +7 -0
- package/src/connectionHelper/checks/websocket.ts +40 -11
- package/src/e2ee/E2eeManager.ts +6 -4
- package/src/options.ts +4 -4
- package/src/room/PCTransport.ts +1 -1
- package/src/room/PCTransportManager.ts +4 -19
- package/src/room/RTCEngine.ts +64 -20
- package/src/room/RegionUrlProvider.test.ts +183 -9
- package/src/room/RegionUrlProvider.ts +97 -12
- package/src/room/Room.ts +25 -17
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +2 -2
- package/src/room/data-stream/incoming/StreamReader.ts +5 -5
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +0 -3
- package/src/room/errors.ts +144 -16
- package/src/room/participant/LocalParticipant.ts +12 -12
- package/src/room/participant/Participant.ts +2 -2
- package/src/room/token-source/TokenSource.ts +5 -1
- package/src/room/track/LocalTrack.ts +0 -4
- package/src/room/track/TrackPublication.ts +1 -1
- package/src/room/track/create.ts +6 -4
- package/src/room/track/processor/types.ts +0 -6
- package/src/room/types.ts +1 -1
- package/src/room/utils.ts +5 -4
- package/src/test/mocks.ts +0 -1
package/src/api/SignalClient.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
protoInt64,
|
|
46
46
|
} from '@livekit/protocol';
|
|
47
47
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
48
|
-
import { ConnectionError
|
|
48
|
+
import { ConnectionError } from '../room/errors';
|
|
49
49
|
import CriticalTimers from '../room/timers';
|
|
50
50
|
import type { LoggerOptions } from '../room/types';
|
|
51
51
|
import { getClientInfo, isReactNative, sleep } from '../room/utils';
|
|
@@ -319,7 +319,7 @@ export class SignalClient {
|
|
|
319
319
|
this.close();
|
|
320
320
|
}
|
|
321
321
|
cleanupAbortHandlers();
|
|
322
|
-
reject(
|
|
322
|
+
reject(ConnectionError.cancelled(reason));
|
|
323
323
|
};
|
|
324
324
|
|
|
325
325
|
abortSignal?.addEventListener('abort', abortHandler);
|
|
@@ -330,12 +330,7 @@ export class SignalClient {
|
|
|
330
330
|
};
|
|
331
331
|
|
|
332
332
|
const wsTimeout = setTimeout(() => {
|
|
333
|
-
abortHandler(
|
|
334
|
-
new ConnectionError(
|
|
335
|
-
'room connection has timed out (signal)',
|
|
336
|
-
ConnectionErrorReason.ServerUnreachable,
|
|
337
|
-
),
|
|
338
|
-
);
|
|
333
|
+
abortHandler(ConnectionError.timeout('room connection has timed out (signal)'));
|
|
339
334
|
}, opts.websocketTimeout);
|
|
340
335
|
|
|
341
336
|
const handleSignalConnected = (
|
|
@@ -364,9 +359,8 @@ export class SignalClient {
|
|
|
364
359
|
.then((closeInfo) => {
|
|
365
360
|
if (this.isEstablishingConnection) {
|
|
366
361
|
reject(
|
|
367
|
-
|
|
362
|
+
ConnectionError.internal(
|
|
368
363
|
`Websocket got closed during a (re)connection attempt: ${closeInfo.reason}`,
|
|
369
|
-
ConnectionErrorReason.InternalError,
|
|
370
364
|
),
|
|
371
365
|
);
|
|
372
366
|
}
|
|
@@ -387,9 +381,8 @@ export class SignalClient {
|
|
|
387
381
|
.catch((reason) => {
|
|
388
382
|
if (this.isEstablishingConnection) {
|
|
389
383
|
reject(
|
|
390
|
-
|
|
384
|
+
ConnectionError.internal(
|
|
391
385
|
`Websocket error during a (re)connection attempt: ${reason}`,
|
|
392
|
-
ConnectionErrorReason.InternalError,
|
|
393
386
|
),
|
|
394
387
|
);
|
|
395
388
|
}
|
|
@@ -416,10 +409,7 @@ export class SignalClient {
|
|
|
416
409
|
const firstMessage = await signalReader.read();
|
|
417
410
|
signalReader.releaseLock();
|
|
418
411
|
if (!firstMessage.value) {
|
|
419
|
-
throw
|
|
420
|
-
'no message received as first message',
|
|
421
|
-
ConnectionErrorReason.InternalError,
|
|
422
|
-
);
|
|
412
|
+
throw ConnectionError.internal('no message received as first message');
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
const firstSignalResponse = parseSignalResponse(firstMessage.value);
|
|
@@ -971,10 +961,8 @@ export class SignalClient {
|
|
|
971
961
|
} else if (this.isEstablishingConnection && firstSignalResponse.message?.case === 'leave') {
|
|
972
962
|
return {
|
|
973
963
|
isValid: false,
|
|
974
|
-
error:
|
|
964
|
+
error: ConnectionError.leaveRequest(
|
|
975
965
|
'Received leave request while trying to (re)connect',
|
|
976
|
-
ConnectionErrorReason.LeaveRequest,
|
|
977
|
-
undefined,
|
|
978
966
|
firstSignalResponse.message.value.reason,
|
|
979
967
|
),
|
|
980
968
|
};
|
|
@@ -982,16 +970,15 @@ export class SignalClient {
|
|
|
982
970
|
// non-reconnect case, should receive join response first
|
|
983
971
|
return {
|
|
984
972
|
isValid: false,
|
|
985
|
-
error:
|
|
973
|
+
error: ConnectionError.internal(
|
|
986
974
|
`did not receive join response, got ${firstSignalResponse.message?.case} instead`,
|
|
987
|
-
ConnectionErrorReason.InternalError,
|
|
988
975
|
),
|
|
989
976
|
};
|
|
990
977
|
}
|
|
991
978
|
|
|
992
979
|
return {
|
|
993
980
|
isValid: false,
|
|
994
|
-
error:
|
|
981
|
+
error: ConnectionError.internal('Unexpected first message'),
|
|
995
982
|
};
|
|
996
983
|
}
|
|
997
984
|
|
|
@@ -1010,22 +997,20 @@ export class SignalClient {
|
|
|
1010
997
|
const resp = await fetch(validateUrl);
|
|
1011
998
|
if (resp.status.toFixed(0).startsWith('4')) {
|
|
1012
999
|
const msg = await resp.text();
|
|
1013
|
-
return
|
|
1000
|
+
return ConnectionError.notAllowed(msg, resp.status);
|
|
1014
1001
|
} else if (reason instanceof ConnectionError) {
|
|
1015
1002
|
return reason;
|
|
1016
1003
|
} else {
|
|
1017
|
-
return
|
|
1004
|
+
return ConnectionError.internal(
|
|
1018
1005
|
`Encountered unknown websocket error during connection: ${reason}`,
|
|
1019
|
-
|
|
1020
|
-
resp.status,
|
|
1006
|
+
{ status: resp.status, statusText: resp.statusText },
|
|
1021
1007
|
);
|
|
1022
1008
|
}
|
|
1023
1009
|
} catch (e) {
|
|
1024
1010
|
return e instanceof ConnectionError
|
|
1025
1011
|
? e
|
|
1026
|
-
:
|
|
1012
|
+
: ConnectionError.serverUnreachable(
|
|
1027
1013
|
e instanceof Error ? e.message : 'server was not reachable',
|
|
1028
|
-
ConnectionErrorReason.ServerUnreachable,
|
|
1029
1014
|
);
|
|
1030
1015
|
}
|
|
1031
1016
|
}
|
package/src/api/utils.ts
CHANGED
|
@@ -14,7 +14,7 @@ export function createValidateUrl(rtcWsUrl: string) {
|
|
|
14
14
|
return appendUrlPath(urlObj, 'validate');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function ensureTrailingSlash(path: string) {
|
|
17
|
+
export function ensureTrailingSlash(path: string) {
|
|
18
18
|
return path.endsWith('/') ? path : `${path}/`;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SignalClient } from '../../api/SignalClient';
|
|
2
|
+
import { RegionUrlProvider } from '../../room/RegionUrlProvider';
|
|
3
|
+
import { isCloud } from '../../room/utils';
|
|
2
4
|
import { Checker } from './Checker';
|
|
3
5
|
|
|
4
6
|
export class TURNCheck extends Checker {
|
|
@@ -7,6 +9,11 @@ export class TURNCheck extends Checker {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
async perform(): Promise<void> {
|
|
12
|
+
if (isCloud(new URL(this.url))) {
|
|
13
|
+
this.appendMessage('Using region specific url');
|
|
14
|
+
this.url =
|
|
15
|
+
(await new RegionUrlProvider(this.url, this.token).getNextBestRegionUrl()) ?? this.url;
|
|
16
|
+
}
|
|
10
17
|
const signalClient = new SignalClient();
|
|
11
18
|
const joinRes = await signalClient.join(this.url, this.token, {
|
|
12
19
|
autoSubscribe: true,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { ServerInfo_Edition } from '@livekit/protocol';
|
|
1
|
+
import { JoinResponse, ServerInfo_Edition } from '@livekit/protocol';
|
|
2
2
|
import { SignalClient } from '../../api/SignalClient';
|
|
3
|
+
import { RegionUrlProvider } from '../../room/RegionUrlProvider';
|
|
4
|
+
import { isCloud } from '../../room/utils';
|
|
3
5
|
import { Checker } from './Checker';
|
|
4
6
|
|
|
5
7
|
export class WebSocketCheck extends Checker {
|
|
@@ -13,16 +15,43 @@ export class WebSocketCheck extends Checker {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
let signalClient = new SignalClient();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
let joinRes: JoinResponse | undefined;
|
|
19
|
+
try {
|
|
20
|
+
joinRes = await signalClient.join(this.url, this.token, {
|
|
21
|
+
autoSubscribe: true,
|
|
22
|
+
maxRetries: 0,
|
|
23
|
+
e2eeEnabled: false,
|
|
24
|
+
websocketTimeout: 15_000,
|
|
25
|
+
singlePeerConnection: false,
|
|
26
|
+
});
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
if (isCloud(new URL(this.url))) {
|
|
29
|
+
this.appendMessage(
|
|
30
|
+
`Initial connection failed with error ${e.message}. Retrying with region fallback`,
|
|
31
|
+
);
|
|
32
|
+
const regionProvider = new RegionUrlProvider(this.url, this.token);
|
|
33
|
+
const regionUrl = await regionProvider.getNextBestRegionUrl();
|
|
34
|
+
if (regionUrl) {
|
|
35
|
+
joinRes = await signalClient.join(regionUrl, this.token, {
|
|
36
|
+
autoSubscribe: true,
|
|
37
|
+
maxRetries: 0,
|
|
38
|
+
e2eeEnabled: false,
|
|
39
|
+
websocketTimeout: 15_000,
|
|
40
|
+
singlePeerConnection: false,
|
|
41
|
+
});
|
|
42
|
+
this.appendMessage(
|
|
43
|
+
`Fallback to region worked. To avoid initial connections failing, ensure you're calling room.prepareConnection() ahead of time`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (joinRes) {
|
|
49
|
+
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
|
|
50
|
+
if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
|
|
51
|
+
this.appendMessage(`LiveKit Cloud: ${joinRes.serverInfo?.region}`);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
this.appendError(`Websocket connection could not be established`);
|
|
26
55
|
}
|
|
27
56
|
await signalClient.close();
|
|
28
57
|
}
|
package/src/e2ee/E2eeManager.ts
CHANGED
|
@@ -68,9 +68,11 @@ export class E2EEManager
|
|
|
68
68
|
|
|
69
69
|
private keyProvider: BaseKeyProvider;
|
|
70
70
|
|
|
71
|
-
private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data']>> =
|
|
71
|
+
private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data'], Error>> =
|
|
72
|
+
new Map();
|
|
72
73
|
|
|
73
|
-
private encryptDataRequests: Map<string, Future<EncryptDataResponseMessage['data']>> =
|
|
74
|
+
private encryptDataRequests: Map<string, Future<EncryptDataResponseMessage['data'], Error>> =
|
|
75
|
+
new Map();
|
|
74
76
|
|
|
75
77
|
private dataChannelEncryptionEnabled: boolean;
|
|
76
78
|
|
|
@@ -322,7 +324,7 @@ export class E2EEManager
|
|
|
322
324
|
participantIdentity: this.room!.localParticipant.identity,
|
|
323
325
|
},
|
|
324
326
|
};
|
|
325
|
-
const future = new Future<EncryptDataResponseMessage['data']>();
|
|
327
|
+
const future = new Future<EncryptDataResponseMessage['data'], Error>();
|
|
326
328
|
future.onFinally = () => {
|
|
327
329
|
this.encryptDataRequests.delete(uuid);
|
|
328
330
|
};
|
|
@@ -351,7 +353,7 @@ export class E2EEManager
|
|
|
351
353
|
keyIndex,
|
|
352
354
|
},
|
|
353
355
|
};
|
|
354
|
-
const future = new Future<DecryptDataResponseMessage['data']>();
|
|
356
|
+
const future = new Future<DecryptDataResponseMessage['data'], Error>();
|
|
355
357
|
future.onFinally = () => {
|
|
356
358
|
this.decryptDataRequests.delete(uuid);
|
|
357
359
|
};
|
package/src/options.ts
CHANGED
|
@@ -87,9 +87,9 @@ export interface InternalRoomOptions {
|
|
|
87
87
|
|
|
88
88
|
webAudioMix: boolean | WebAudioSettings;
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated Use `encryption` field instead.
|
|
92
|
+
*/
|
|
93
93
|
e2ee?: E2EEOptions;
|
|
94
94
|
|
|
95
95
|
/**
|
|
@@ -111,7 +111,7 @@ export interface InternalRoomOptions {
|
|
|
111
111
|
/**
|
|
112
112
|
* Options for when creating a new room
|
|
113
113
|
*/
|
|
114
|
-
export interface RoomOptions extends Partial<
|
|
114
|
+
export interface RoomOptions extends Partial<InternalRoomOptions> {}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* @internal
|
package/src/room/PCTransport.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Mutex } from '@livekit/mutex';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
-
import type { MediaDescription, SessionDescription } from 'sdp-transform';
|
|
4
3
|
import { parse, write } from 'sdp-transform';
|
|
5
4
|
import { debounce } from 'ts-debounce';
|
|
5
|
+
import type { MediaDescription, SessionDescription } from 'sdp-transform';
|
|
6
6
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
7
7
|
import { NegotiationError, UnexpectedConnectionState } from './errors';
|
|
8
8
|
import type { LoggerOptions } from './types';
|
|
@@ -3,7 +3,7 @@ import { SignalTarget } from '@livekit/protocol';
|
|
|
3
3
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
4
4
|
import PCTransport, { PCEvents } from './PCTransport';
|
|
5
5
|
import { roomConnectOptionDefaults } from './defaults';
|
|
6
|
-
import { ConnectionError
|
|
6
|
+
import { ConnectionError } from './errors';
|
|
7
7
|
import CriticalTimers from './timers';
|
|
8
8
|
import type { LoggerOptions } from './types';
|
|
9
9
|
import { sleep } from './utils';
|
|
@@ -345,12 +345,7 @@ export class PCTransportManager {
|
|
|
345
345
|
this.log.warn('abort transport connection', this.logContext);
|
|
346
346
|
CriticalTimers.clearTimeout(connectTimeout);
|
|
347
347
|
|
|
348
|
-
reject(
|
|
349
|
-
new ConnectionError(
|
|
350
|
-
'room connection has been cancelled',
|
|
351
|
-
ConnectionErrorReason.Cancelled,
|
|
352
|
-
),
|
|
353
|
-
);
|
|
348
|
+
reject(ConnectionError.cancelled('room connection has been cancelled'));
|
|
354
349
|
};
|
|
355
350
|
if (abortController?.signal.aborted) {
|
|
356
351
|
abortHandler();
|
|
@@ -359,23 +354,13 @@ export class PCTransportManager {
|
|
|
359
354
|
|
|
360
355
|
const connectTimeout = CriticalTimers.setTimeout(() => {
|
|
361
356
|
abortController?.signal.removeEventListener('abort', abortHandler);
|
|
362
|
-
reject(
|
|
363
|
-
new ConnectionError(
|
|
364
|
-
'could not establish pc connection',
|
|
365
|
-
ConnectionErrorReason.InternalError,
|
|
366
|
-
),
|
|
367
|
-
);
|
|
357
|
+
reject(ConnectionError.internal('could not establish pc connection'));
|
|
368
358
|
}, timeout);
|
|
369
359
|
|
|
370
360
|
while (this.state !== PCTransportState.CONNECTED) {
|
|
371
361
|
await sleep(50); // FIXME we shouldn't rely on `sleep` in the connection paths, as it invokes `setTimeout` which can be drastically throttled by browser implementations
|
|
372
362
|
if (abortController?.signal.aborted) {
|
|
373
|
-
reject(
|
|
374
|
-
new ConnectionError(
|
|
375
|
-
'room connection has been cancelled',
|
|
376
|
-
ConnectionErrorReason.Cancelled,
|
|
377
|
-
),
|
|
378
|
-
);
|
|
363
|
+
reject(ConnectionError.cancelled('room connection has been cancelled'));
|
|
379
364
|
return;
|
|
380
365
|
}
|
|
381
366
|
}
|
package/src/room/RTCEngine.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
ConnectionError,
|
|
63
63
|
ConnectionErrorReason,
|
|
64
64
|
NegotiationError,
|
|
65
|
+
SignalReconnectError,
|
|
65
66
|
TrackInvalidError,
|
|
66
67
|
UnexpectedConnectionState,
|
|
67
68
|
} from './errors';
|
|
@@ -92,6 +93,8 @@ const reliableDataChannel = '_reliable';
|
|
|
92
93
|
const minReconnectWait = 2 * 1000;
|
|
93
94
|
const leaveReconnect = 'leave-reconnect';
|
|
94
95
|
const reliabeReceiveStateTTL = 30_000;
|
|
96
|
+
const lossyDataChannelBufferThresholdMin = 8 * 1024;
|
|
97
|
+
const lossyDataChannelBufferThresholdMax = 256 * 1024;
|
|
95
98
|
|
|
96
99
|
enum PCState {
|
|
97
100
|
New,
|
|
@@ -203,6 +206,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
203
206
|
|
|
204
207
|
private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
|
|
205
208
|
|
|
209
|
+
private lossyDataStatCurrentBytes: number = 0;
|
|
210
|
+
|
|
211
|
+
private lossyDataStatByterate: number = 0;
|
|
212
|
+
|
|
213
|
+
private lossyDataStatInterval: ReturnType<typeof setInterval> | undefined;
|
|
214
|
+
|
|
215
|
+
private lossyDataDropCount: number = 0;
|
|
216
|
+
|
|
206
217
|
private midToTrackId: { [key: string]: string } = {};
|
|
207
218
|
|
|
208
219
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
@@ -312,6 +323,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
312
323
|
this.removeAllListeners();
|
|
313
324
|
this.deregisterOnLineListener();
|
|
314
325
|
this.clearPendingReconnect();
|
|
326
|
+
this.cleanupLossyDataStats();
|
|
315
327
|
await this.cleanupPeerConnections();
|
|
316
328
|
await this.cleanupClient();
|
|
317
329
|
} finally {
|
|
@@ -347,6 +359,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
347
359
|
this.reliableReceivedState.clear();
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
cleanupLossyDataStats() {
|
|
363
|
+
this.lossyDataStatByterate = 0;
|
|
364
|
+
this.lossyDataStatCurrentBytes = 0;
|
|
365
|
+
if (this.lossyDataStatInterval) {
|
|
366
|
+
clearInterval(this.lossyDataStatInterval);
|
|
367
|
+
this.lossyDataStatInterval = undefined;
|
|
368
|
+
}
|
|
369
|
+
this.lossyDataDropCount = 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
350
372
|
async cleanupClient() {
|
|
351
373
|
await this.client.close();
|
|
352
374
|
this.client.resetCallbacks();
|
|
@@ -360,10 +382,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
360
382
|
const publicationTimeout = setTimeout(() => {
|
|
361
383
|
delete this.pendingTrackResolvers[req.cid];
|
|
362
384
|
reject(
|
|
363
|
-
|
|
364
|
-
'publication of local track timed out, no response from server',
|
|
365
|
-
ConnectionErrorReason.Timeout,
|
|
366
|
-
),
|
|
385
|
+
ConnectionError.timeout('publication of local track timed out, no response from server'),
|
|
367
386
|
);
|
|
368
387
|
}, 10_000);
|
|
369
388
|
this.pendingTrackResolvers[req.cid] = {
|
|
@@ -569,6 +588,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
569
588
|
|
|
570
589
|
this.client.onTokenRefresh = (token: string) => {
|
|
571
590
|
this.token = token;
|
|
591
|
+
this.regionUrlProvider?.updateToken(token);
|
|
572
592
|
};
|
|
573
593
|
|
|
574
594
|
this.client.onRemoteMuteChanged = (trackSid: string, muted: boolean) => {
|
|
@@ -712,6 +732,22 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
712
732
|
// handle buffer amount low events
|
|
713
733
|
this.lossyDC.onbufferedamountlow = this.handleBufferedAmountLow;
|
|
714
734
|
this.reliableDC.onbufferedamountlow = this.handleBufferedAmountLow;
|
|
735
|
+
|
|
736
|
+
this.cleanupLossyDataStats();
|
|
737
|
+
this.lossyDataStatInterval = setInterval(() => {
|
|
738
|
+
this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
|
|
739
|
+
this.lossyDataStatCurrentBytes = 0;
|
|
740
|
+
|
|
741
|
+
const dc = this.dataChannelForKind(DataPacket_Kind.LOSSY);
|
|
742
|
+
if (dc) {
|
|
743
|
+
// control buffered latency to ~100ms
|
|
744
|
+
const threshold = this.lossyDataStatByterate / 10;
|
|
745
|
+
dc.bufferedAmountLowThreshold = Math.min(
|
|
746
|
+
Math.max(threshold, lossyDataChannelBufferThresholdMin),
|
|
747
|
+
lossyDataChannelBufferThresholdMax,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}, 1000);
|
|
715
751
|
}
|
|
716
752
|
|
|
717
753
|
private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
|
|
@@ -1190,10 +1226,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1190
1226
|
} catch (e: any) {
|
|
1191
1227
|
// TODO do we need a `failed` state here for the PC?
|
|
1192
1228
|
this.pcState = PCState.Disconnected;
|
|
1193
|
-
throw
|
|
1194
|
-
`could not establish PC connection, ${e.message}`,
|
|
1195
|
-
ConnectionErrorReason.InternalError,
|
|
1196
|
-
);
|
|
1229
|
+
throw ConnectionError.internal(`could not establish PC connection, ${e.message}`);
|
|
1197
1230
|
}
|
|
1198
1231
|
}
|
|
1199
1232
|
|
|
@@ -1251,7 +1284,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1251
1284
|
}),
|
|
1252
1285
|
},
|
|
1253
1286
|
});
|
|
1254
|
-
|
|
1255
1287
|
await this.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
|
1256
1288
|
}
|
|
1257
1289
|
|
|
@@ -1285,7 +1317,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1285
1317
|
const dc = this.dataChannelForKind(kind);
|
|
1286
1318
|
if (dc) {
|
|
1287
1319
|
if (kind === DataPacket_Kind.RELIABLE) {
|
|
1320
|
+
await this.waitForBufferStatusLow(kind);
|
|
1288
1321
|
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence });
|
|
1322
|
+
} else {
|
|
1323
|
+
// lossy channel, drop messages to reduce latency
|
|
1324
|
+
if (!this.isBufferStatusLow(kind)) {
|
|
1325
|
+
this.lossyDataDropCount += 1;
|
|
1326
|
+
if (this.lossyDataDropCount % 100 === 0) {
|
|
1327
|
+
this.log.warn(
|
|
1328
|
+
`dropping lossy data channel messages, total dropped: ${this.lossyDataDropCount}`,
|
|
1329
|
+
this.logContext,
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
this.lossyDataStatCurrentBytes += msg.byteLength;
|
|
1289
1335
|
}
|
|
1290
1336
|
|
|
1291
1337
|
if (this.attemptingReconnect) {
|
|
@@ -1311,6 +1357,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1311
1357
|
}
|
|
1312
1358
|
|
|
1313
1359
|
private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
|
|
1360
|
+
if (kind === DataPacket_Kind.RELIABLE) {
|
|
1361
|
+
const dc = this.dataChannelForKind(kind);
|
|
1362
|
+
if (dc) {
|
|
1363
|
+
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1314
1367
|
const status = this.isBufferStatusLow(kind);
|
|
1315
1368
|
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
|
1316
1369
|
this.dcBufferStatus.set(kind, status);
|
|
@@ -1321,9 +1374,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1321
1374
|
private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
|
|
1322
1375
|
const dc = this.dataChannelForKind(kind);
|
|
1323
1376
|
if (dc) {
|
|
1324
|
-
if (kind === DataPacket_Kind.RELIABLE) {
|
|
1325
|
-
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
1326
|
-
}
|
|
1327
1377
|
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
1328
1378
|
}
|
|
1329
1379
|
};
|
|
@@ -1357,10 +1407,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1357
1407
|
const transport = subscriber ? this.pcManager.subscriber : this.pcManager.publisher;
|
|
1358
1408
|
const transportName = subscriber ? 'Subscriber' : 'Publisher';
|
|
1359
1409
|
if (!transport) {
|
|
1360
|
-
throw
|
|
1361
|
-
`${transportName} connection not set`,
|
|
1362
|
-
ConnectionErrorReason.InternalError,
|
|
1363
|
-
);
|
|
1410
|
+
throw ConnectionError.internal(`${transportName} connection not set`);
|
|
1364
1411
|
}
|
|
1365
1412
|
|
|
1366
1413
|
let needNegotiation = false;
|
|
@@ -1401,9 +1448,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1401
1448
|
await sleep(50);
|
|
1402
1449
|
}
|
|
1403
1450
|
|
|
1404
|
-
throw
|
|
1451
|
+
throw ConnectionError.internal(
|
|
1405
1452
|
`could not establish ${transportName} connection, state: ${transport.getICEConnectionState()}`,
|
|
1406
|
-
ConnectionErrorReason.InternalError,
|
|
1407
1453
|
);
|
|
1408
1454
|
}
|
|
1409
1455
|
|
|
@@ -1693,8 +1739,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1693
1739
|
}
|
|
1694
1740
|
}
|
|
1695
1741
|
|
|
1696
|
-
class SignalReconnectError extends Error {}
|
|
1697
|
-
|
|
1698
1742
|
export type EngineEventCallbacks = {
|
|
1699
1743
|
connected: (joinResp: JoinResponse) => void;
|
|
1700
1744
|
disconnected: (reason?: DisconnectReason) => void;
|