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.
Files changed (88) hide show
  1. package/README.md +105 -1
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +1 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +1175 -1341
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/api/utils.d.ts +1 -0
  12. package/dist/src/api/utils.d.ts.map +1 -1
  13. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  14. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/options.d.ts +4 -1
  17. package/dist/src/options.d.ts.map +1 -1
  18. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  19. package/dist/src/room/RTCEngine.d.ts +5 -0
  20. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  21. package/dist/src/room/RegionUrlProvider.d.ts +7 -0
  22. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  23. package/dist/src/room/Room.d.ts +1 -1
  24. package/dist/src/room/Room.d.ts.map +1 -1
  25. package/dist/src/room/data-stream/incoming/StreamReader.d.ts +3 -3
  26. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  27. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -1
  28. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  29. package/dist/src/room/errors.d.ts +74 -5
  30. package/dist/src/room/errors.d.ts.map +1 -1
  31. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  32. package/dist/src/room/participant/Participant.d.ts +1 -1
  33. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  34. package/dist/src/room/token-source/TokenSource.d.ts +10 -2
  35. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  36. package/dist/src/room/track/LocalTrack.d.ts +0 -4
  37. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  38. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  39. package/dist/src/room/track/create.d.ts.map +1 -1
  40. package/dist/src/room/track/processor/types.d.ts +0 -6
  41. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  42. package/dist/src/room/types.d.ts +1 -1
  43. package/dist/src/room/types.d.ts.map +1 -1
  44. package/dist/src/room/utils.d.ts +4 -4
  45. package/dist/src/room/utils.d.ts.map +1 -1
  46. package/dist/src/test/mocks.d.ts.map +1 -1
  47. package/dist/ts4.2/api/utils.d.ts +1 -0
  48. package/dist/ts4.2/options.d.ts +4 -1
  49. package/dist/ts4.2/room/RTCEngine.d.ts +5 -0
  50. package/dist/ts4.2/room/RegionUrlProvider.d.ts +7 -0
  51. package/dist/ts4.2/room/Room.d.ts +1 -1
  52. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +3 -3
  53. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -1
  54. package/dist/ts4.2/room/errors.d.ts +74 -5
  55. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  56. package/dist/ts4.2/room/token-source/TokenSource.d.ts +1 -1
  57. package/dist/ts4.2/room/track/LocalTrack.d.ts +0 -4
  58. package/dist/ts4.2/room/track/processor/types.d.ts +0 -6
  59. package/dist/ts4.2/room/types.d.ts +1 -1
  60. package/dist/ts4.2/room/utils.d.ts +3 -3
  61. package/package.json +10 -6
  62. package/src/api/SignalClient.test.ts +12 -19
  63. package/src/api/SignalClient.ts +13 -28
  64. package/src/api/utils.ts +1 -1
  65. package/src/connectionHelper/checks/turn.ts +7 -0
  66. package/src/connectionHelper/checks/websocket.ts +40 -11
  67. package/src/e2ee/E2eeManager.ts +6 -4
  68. package/src/options.ts +4 -4
  69. package/src/room/PCTransport.ts +1 -1
  70. package/src/room/PCTransportManager.ts +4 -19
  71. package/src/room/RTCEngine.ts +64 -20
  72. package/src/room/RegionUrlProvider.test.ts +183 -9
  73. package/src/room/RegionUrlProvider.ts +97 -12
  74. package/src/room/Room.ts +25 -17
  75. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +2 -2
  76. package/src/room/data-stream/incoming/StreamReader.ts +5 -5
  77. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +0 -3
  78. package/src/room/errors.ts +144 -16
  79. package/src/room/participant/LocalParticipant.ts +12 -12
  80. package/src/room/participant/Participant.ts +2 -2
  81. package/src/room/token-source/TokenSource.ts +5 -1
  82. package/src/room/track/LocalTrack.ts +0 -4
  83. package/src/room/track/TrackPublication.ts +1 -1
  84. package/src/room/track/create.ts +6 -4
  85. package/src/room/track/processor/types.ts +0 -6
  86. package/src/room/types.ts +1 -1
  87. package/src/room/utils.ts +5 -4
  88. package/src/test/mocks.ts +0 -1
@@ -45,7 +45,7 @@ import {
45
45
  protoInt64,
46
46
  } from '@livekit/protocol';
47
47
  import log, { LoggerNames, getLogger } from '../logger';
48
- import { ConnectionError, ConnectionErrorReason } from '../room/errors';
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(target instanceof AbortSignal ? target.reason : target);
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
- new ConnectionError(
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
- new ConnectionError(
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 new ConnectionError(
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: new ConnectionError(
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: new ConnectionError(
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: new ConnectionError('Unexpected first message', ConnectionErrorReason.InternalError),
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 new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status);
1000
+ return ConnectionError.notAllowed(msg, resp.status);
1014
1001
  } else if (reason instanceof ConnectionError) {
1015
1002
  return reason;
1016
1003
  } else {
1017
- return new ConnectionError(
1004
+ return ConnectionError.internal(
1018
1005
  `Encountered unknown websocket error during connection: ${reason}`,
1019
- ConnectionErrorReason.InternalError,
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
- : new ConnectionError(
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
- const joinRes = await signalClient.join(this.url, this.token, {
17
- autoSubscribe: true,
18
- maxRetries: 0,
19
- e2eeEnabled: false,
20
- websocketTimeout: 15_000,
21
- singlePeerConnection: false,
22
- });
23
- this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
24
- if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
25
- this.appendMessage(`LiveKit Cloud: ${joinRes.serverInfo?.region}`);
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
  }
@@ -68,9 +68,11 @@ export class E2EEManager
68
68
 
69
69
  private keyProvider: BaseKeyProvider;
70
70
 
71
- private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data']>> = new Map();
71
+ private decryptDataRequests: Map<string, Future<DecryptDataResponseMessage['data'], Error>> =
72
+ new Map();
72
73
 
73
- private encryptDataRequests: Map<string, Future<EncryptDataResponseMessage['data']>> = new Map();
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
- // * @deprecated Use `encryption` field instead.
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<Omit<InternalRoomOptions, 'encryption'>> {}
114
+ export interface RoomOptions extends Partial<InternalRoomOptions> {}
115
115
 
116
116
  /**
117
117
  * @internal
@@ -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, ConnectionErrorReason } from './errors';
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
  }
@@ -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
- new ConnectionError(
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 new ConnectionError(
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 new ConnectionError(
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 new ConnectionError(
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;