livekit-client 1.7.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. package/README.md +21 -1
  2. package/dist/livekit-client.esm.mjs +14241 -12994
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/api/SignalClient.d.ts +11 -10
  7. package/dist/src/api/SignalClient.d.ts.map +1 -1
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  9. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  10. package/dist/src/connectionHelper/checks/Checker.d.ts +1 -1
  11. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  12. package/dist/src/index.d.ts +7 -7
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  15. package/dist/src/proto/livekit_models.d.ts +37 -0
  16. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  17. package/dist/src/proto/livekit_rtc.d.ts +347 -75
  18. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  19. package/dist/src/room/RTCEngine.d.ts +12 -3
  20. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  21. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  22. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  23. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  24. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  25. package/dist/src/room/Room.d.ts +23 -15
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/errors.d.ts +2 -1
  28. package/dist/src/room/errors.d.ts.map +1 -1
  29. package/dist/src/room/events.d.ts +23 -2
  30. package/dist/src/room/events.d.ts.map +1 -1
  31. package/dist/src/room/participant/LocalParticipant.d.ts +14 -2
  32. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  33. package/dist/src/room/participant/Participant.d.ts +4 -2
  34. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  35. package/dist/src/room/participant/RemoteParticipant.d.ts +2 -2
  36. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  37. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  38. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  39. package/dist/src/room/track/LocalTrack.d.ts +4 -3
  40. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  41. package/dist/src/room/track/LocalTrackPublication.d.ts +1 -1
  42. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
  43. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  44. package/dist/src/room/track/RemoteAudioTrack.d.ts +1 -1
  45. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  46. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  47. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  48. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
  49. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  50. package/dist/src/room/track/Track.d.ts +3 -1
  51. package/dist/src/room/track/Track.d.ts.map +1 -1
  52. package/dist/src/room/track/create.d.ts.map +1 -1
  53. package/dist/src/room/types.d.ts +5 -0
  54. package/dist/src/room/types.d.ts.map +1 -1
  55. package/dist/src/room/utils.d.ts +4 -0
  56. package/dist/src/room/utils.d.ts.map +1 -1
  57. package/dist/ts4.2/src/api/SignalClient.d.ts +14 -10
  58. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  59. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +1 -1
  60. package/dist/ts4.2/src/index.d.ts +7 -6
  61. package/dist/ts4.2/src/proto/livekit_models.d.ts +37 -0
  62. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +380 -84
  63. package/dist/ts4.2/src/room/RTCEngine.d.ts +12 -3
  64. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  65. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  66. package/dist/ts4.2/src/room/Room.d.ts +23 -15
  67. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  68. package/dist/ts4.2/src/room/events.d.ts +23 -2
  69. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +14 -2
  70. package/dist/ts4.2/src/room/participant/Participant.d.ts +4 -2
  71. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +2 -2
  72. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +4 -3
  73. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -1
  74. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +1 -1
  75. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  76. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
  77. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  78. package/dist/ts4.2/src/room/types.d.ts +5 -0
  79. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  80. package/package.json +21 -20
  81. package/src/api/SignalClient.ts +41 -29
  82. package/src/connectionHelper/ConnectionCheck.ts +1 -2
  83. package/src/connectionHelper/checks/Checker.ts +1 -1
  84. package/src/connectionHelper/checks/reconnect.ts +1 -1
  85. package/src/index.ts +9 -8
  86. package/src/proto/google/protobuf/timestamp.ts +15 -6
  87. package/src/proto/livekit_models.ts +917 -221
  88. package/src/proto/livekit_rtc.ts +1053 -279
  89. package/src/room/RTCEngine.ts +171 -47
  90. package/src/room/ReconnectPolicy.ts +2 -0
  91. package/src/room/RegionUrlProvider.ts +73 -0
  92. package/src/room/Room.ts +278 -177
  93. package/src/room/errors.ts +1 -0
  94. package/src/room/events.ts +24 -0
  95. package/src/room/participant/LocalParticipant.ts +30 -7
  96. package/src/room/participant/Participant.ts +27 -3
  97. package/src/room/participant/RemoteParticipant.ts +6 -3
  98. package/src/room/participant/publishUtils.test.ts +1 -1
  99. package/src/room/participant/publishUtils.ts +1 -1
  100. package/src/room/track/LocalAudioTrack.ts +14 -7
  101. package/src/room/track/LocalTrack.ts +23 -9
  102. package/src/room/track/LocalTrackPublication.ts +1 -1
  103. package/src/room/track/LocalVideoTrack.ts +15 -9
  104. package/src/room/track/RemoteAudioTrack.ts +1 -1
  105. package/src/room/track/RemoteTrackPublication.ts +4 -3
  106. package/src/room/track/RemoteVideoTrack.test.ts +1 -1
  107. package/src/room/track/RemoteVideoTrack.ts +8 -7
  108. package/src/room/track/Track.ts +46 -31
  109. package/src/room/track/create.ts +2 -2
  110. package/src/room/types.ts +17 -0
  111. package/src/room/utils.ts +53 -0
@@ -22,6 +22,9 @@ import {
22
22
  SignalTarget,
23
23
  TrackPublishedResponse,
24
24
  } from '../proto/livekit_rtc';
25
+ import PCTransport, { PCEvents } from './PCTransport';
26
+ import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
27
+ import { RegionUrlProvider } from './RegionUrlProvider';
25
28
  import { roomConnectOptionDefaults } from './defaults';
26
29
  import {
27
30
  ConnectionError,
@@ -31,17 +34,16 @@ import {
31
34
  UnexpectedConnectionState,
32
35
  } from './errors';
33
36
  import { EngineEvent } from './events';
34
- import PCTransport, { PCEvents } from './PCTransport';
35
- import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
36
37
  import CriticalTimers from './timers';
37
38
  import type LocalTrack from './track/LocalTrack';
38
39
  import type LocalVideoTrack from './track/LocalVideoTrack';
39
40
  import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
40
- import type { TrackPublishOptions, VideoCodec } from './track/options';
41
41
  import { Track } from './track/Track';
42
+ import type { TrackPublishOptions, VideoCodec } from './track/options';
42
43
  import {
43
- isWeb,
44
44
  Mutex,
45
+ isCloud,
46
+ isWeb,
45
47
  sleep,
46
48
  supportsAddTrack,
47
49
  supportsSetCodecPreferences,
@@ -73,6 +75,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
73
75
 
74
76
  peerConnectionTimeout: number = roomConnectOptionDefaults.peerConnectionTimeout;
75
77
 
78
+ fullReconnectOnNext: boolean = false;
79
+
76
80
  get isClosed() {
77
81
  return this._isClosed;
78
82
  }
@@ -84,6 +88,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
84
88
 
85
89
  private reliableDC?: RTCDataChannel;
86
90
 
91
+ private dcBufferStatus: Map<DataPacket_Kind, boolean>;
92
+
87
93
  // @ts-ignore noUnusedLocals
88
94
  private reliableDCSub?: RTCDataChannel;
89
95
 
@@ -114,8 +120,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
114
120
 
115
121
  private reconnectStart: number = 0;
116
122
 
117
- private fullReconnectOnNext: boolean = false;
118
-
119
123
  private clientConfiguration?: ClientConfiguration;
120
124
 
121
125
  private attemptingReconnect: boolean = false;
@@ -138,6 +142,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
138
142
 
139
143
  private shouldFailNext: boolean = false;
140
144
 
145
+ private regionUrlProvider?: RegionUrlProvider;
146
+
141
147
  constructor(private options: InternalRoomOptions) {
142
148
  super();
143
149
  this.client = new SignalClient();
@@ -146,6 +152,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
146
152
  this.registerOnLineListener();
147
153
  this.closingLock = new Mutex();
148
154
  this.dataProcessLock = new Mutex();
155
+ this.dcBufferStatus = new Map([
156
+ [DataPacket_Kind.LOSSY, true],
157
+ [DataPacket_Kind.RELIABLE, true],
158
+ ]);
149
159
  }
150
160
 
151
161
  async join(
@@ -509,6 +519,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
509
519
  // handle datachannel errors
510
520
  this.lossyDC.onerror = this.handleDataError;
511
521
  this.reliableDC.onerror = this.handleDataError;
522
+
523
+ // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
524
+ this.lossyDC.bufferedAmountLowThreshold = 65535;
525
+ this.reliableDC.bufferedAmountLowThreshold = 65535;
526
+
527
+ // handle buffer amount low events
528
+ this.lossyDC.onbufferedamountlow = this.handleBufferedAmountLow;
529
+ this.reliableDC.onbufferedamountlow = this.handleBufferedAmountLow;
512
530
  }
513
531
 
514
532
  private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
@@ -564,6 +582,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
564
582
  }
565
583
  };
566
584
 
585
+ private handleBufferedAmountLow = (event: Event) => {
586
+ const channel = event.currentTarget as RTCDataChannel;
587
+ const channelKind =
588
+ channel.maxRetransmits === 0 ? DataPacket_Kind.LOSSY : DataPacket_Kind.RELIABLE;
589
+
590
+ this.updateAndEmitDCBufferStatus(channelKind);
591
+ };
592
+
567
593
  private setPreferredCodec(
568
594
  transceiver: RTCRtpTransceiver,
569
595
  kind: Track.Kind,
@@ -739,6 +765,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
739
765
  log.debug(`reconnecting in ${delay}ms`);
740
766
 
741
767
  this.clearReconnectTimeout();
768
+ if (this.url && this.token && isCloud(new URL(this.url))) {
769
+ this.regionUrlProvider = new RegionUrlProvider(this.url, this.token);
770
+ }
742
771
  this.reconnectTimeout = CriticalTimers.setTimeout(
743
772
  () => this.attemptReconnect(disconnectReason),
744
773
  delay,
@@ -810,46 +839,64 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
810
839
  return null;
811
840
  }
812
841
 
813
- private async restartConnection() {
814
- if (!this.url || !this.token) {
815
- // permanent failure, don't attempt reconnection
816
- throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
817
- }
818
-
819
- log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
820
- this.emit(EngineEvent.Restarting);
842
+ private async restartConnection(regionUrl?: string) {
843
+ try {
844
+ if (!this.url || !this.token) {
845
+ // permanent failure, don't attempt reconnection
846
+ throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
847
+ }
821
848
 
822
- if (this.client.isConnected) {
823
- await this.client.sendLeave();
824
- }
825
- await this.client.close();
826
- this.primaryPC = undefined;
827
- this.publisher?.close();
828
- this.publisher = undefined;
829
- this.subscriber?.close();
830
- this.subscriber = undefined;
849
+ log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
850
+ this.emit(EngineEvent.Restarting);
831
851
 
832
- let joinResponse: JoinResponse;
833
- try {
834
- if (!this.signalOpts) {
835
- log.warn('attempted connection restart, without signal options present');
852
+ if (this.client.isConnected) {
853
+ await this.client.sendLeave();
854
+ }
855
+ await this.client.close();
856
+ this.primaryPC = undefined;
857
+ this.publisher?.close();
858
+ this.publisher = undefined;
859
+ this.subscriber?.close();
860
+ this.subscriber = undefined;
861
+
862
+ let joinResponse: JoinResponse;
863
+ try {
864
+ if (!this.signalOpts) {
865
+ log.warn('attempted connection restart, without signal options present');
866
+ throw new SignalReconnectError();
867
+ }
868
+ // in case a regionUrl is passed, the region URL takes precedence
869
+ joinResponse = await this.join(regionUrl ?? this.url, this.token, this.signalOpts);
870
+ } catch (e) {
871
+ if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
872
+ throw new UnexpectedConnectionState('could not reconnect, token might be expired');
873
+ }
836
874
  throw new SignalReconnectError();
837
875
  }
838
- joinResponse = await this.join(this.url, this.token, this.signalOpts);
839
- } catch (e) {
840
- throw new SignalReconnectError();
841
- }
842
-
843
- if (this.shouldFailNext) {
844
- this.shouldFailNext = false;
845
- throw new Error('simulated failure');
846
- }
847
876
 
848
- await this.waitForPCConnected();
849
- this.client.setReconnected();
877
+ if (this.shouldFailNext) {
878
+ this.shouldFailNext = false;
879
+ throw new Error('simulated failure');
880
+ }
850
881
 
851
- // reconnect success
852
- this.emit(EngineEvent.Restarted, joinResponse);
882
+ this.client.setReconnected();
883
+ this.emit(EngineEvent.SignalRestarted, joinResponse);
884
+
885
+ await this.waitForPCReconnected();
886
+ this.regionUrlProvider?.resetAttempts();
887
+ // reconnect success
888
+ this.emit(EngineEvent.Restarted);
889
+ } catch (error) {
890
+ const nextRegionUrl = await this.regionUrlProvider?.getNextBestRegionUrl();
891
+ if (nextRegionUrl) {
892
+ await this.restartConnection(nextRegionUrl);
893
+ return;
894
+ } else {
895
+ // no more regions to try (or we're not on cloud)
896
+ this.regionUrlProvider?.resetAttempts();
897
+ throw error;
898
+ }
899
+ }
853
900
  }
854
901
 
855
902
  private async resumeConnection(reason?: ReconnectReason): Promise<void> {
@@ -877,6 +924,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
877
924
  if (e instanceof Error) {
878
925
  message = e.message;
879
926
  }
927
+ if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
928
+ throw new UnexpectedConnectionState('could not reconnect, token might be expired');
929
+ }
880
930
  throw new SignalReconnectError(message);
881
931
  }
882
932
  this.emit(EngineEvent.SignalResumed);
@@ -893,7 +943,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
893
943
  await this.publisher.createAndSendOffer({ iceRestart: true });
894
944
  }
895
945
 
896
- await this.waitForPCConnected();
946
+ await this.waitForPCReconnected();
897
947
  this.client.setReconnected();
898
948
 
899
949
  // recreate publish datachannel if it's id is null
@@ -906,7 +956,45 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
906
956
  this.emit(EngineEvent.Resumed);
907
957
  }
908
958
 
909
- async waitForPCConnected() {
959
+ async waitForPCInitialConnection(timeout?: number, abortController?: AbortController) {
960
+ if (this.pcState === PCState.Connected) {
961
+ return;
962
+ }
963
+ if (this.pcState !== PCState.New) {
964
+ throw new UnexpectedConnectionState(
965
+ 'Expected peer connection to be new on initial connection',
966
+ );
967
+ }
968
+ return new Promise<void>((resolve, reject) => {
969
+ const abortHandler = () => {
970
+ log.warn('closing engine');
971
+ CriticalTimers.clearTimeout(connectTimeout);
972
+
973
+ reject(
974
+ new ConnectionError(
975
+ 'room connection has been cancelled',
976
+ ConnectionErrorReason.Cancelled,
977
+ ),
978
+ );
979
+ };
980
+ if (abortController?.signal.aborted) {
981
+ abortHandler();
982
+ }
983
+ abortController?.signal.addEventListener('abort', abortHandler);
984
+ const onConnected = () => {
985
+ CriticalTimers.clearTimeout(connectTimeout);
986
+ abortController?.signal.removeEventListener('abort', abortHandler);
987
+ resolve();
988
+ };
989
+ const connectTimeout = CriticalTimers.setTimeout(() => {
990
+ this.off(EngineEvent.Connected, onConnected);
991
+ reject(new ConnectionError('could not establish pc connection'));
992
+ }, timeout ?? this.peerConnectionTimeout);
993
+ this.once(EngineEvent.Connected, onConnected);
994
+ });
995
+ }
996
+
997
+ private async waitForPCReconnected() {
910
998
  const startTime = Date.now();
911
999
  let now = startTime;
912
1000
  this.pcState = PCState.Reconnecting;
@@ -936,6 +1024,24 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
936
1024
  throw new ConnectionError('could not establish PC connection');
937
1025
  }
938
1026
 
1027
+ waitForRestarted = () => {
1028
+ return new Promise<void>((resolve, reject) => {
1029
+ if (this.pcState === PCState.Connected) {
1030
+ resolve();
1031
+ }
1032
+ const onRestarted = () => {
1033
+ this.off(EngineEvent.Disconnected, onDisconnected);
1034
+ resolve();
1035
+ };
1036
+ const onDisconnected = () => {
1037
+ this.off(EngineEvent.Restarted, onRestarted);
1038
+ reject();
1039
+ };
1040
+ this.once(EngineEvent.Restarted, onRestarted);
1041
+ this.once(EngineEvent.Disconnected, onDisconnected);
1042
+ });
1043
+ };
1044
+
939
1045
  /* @internal */
940
1046
  async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
941
1047
  const msg = DataPacket.encode(packet).finish();
@@ -943,13 +1049,29 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
943
1049
  // make sure we do have a data connection
944
1050
  await this.ensurePublisherConnected(kind);
945
1051
 
946
- if (kind === DataPacket_Kind.LOSSY && this.lossyDC) {
947
- this.lossyDC.send(msg);
948
- } else if (kind === DataPacket_Kind.RELIABLE && this.reliableDC) {
949
- this.reliableDC.send(msg);
1052
+ const dc = this.dataChannelForKind(kind);
1053
+ if (dc) {
1054
+ dc.send(msg);
950
1055
  }
1056
+
1057
+ this.updateAndEmitDCBufferStatus(kind);
951
1058
  }
952
1059
 
1060
+ private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
1061
+ const status = this.isBufferStatusLow(kind);
1062
+ if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
1063
+ this.dcBufferStatus.set(kind, status);
1064
+ this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
1065
+ }
1066
+ };
1067
+
1068
+ private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
1069
+ const dc = this.dataChannelForKind(kind);
1070
+ if (dc) {
1071
+ return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
1072
+ }
1073
+ };
1074
+
953
1075
  /**
954
1076
  * @internal
955
1077
  */
@@ -1144,8 +1266,9 @@ export type EngineEventCallbacks = {
1144
1266
  resuming: () => void;
1145
1267
  resumed: () => void;
1146
1268
  restarting: () => void;
1147
- restarted: (joinResp: JoinResponse) => void;
1269
+ restarted: () => void;
1148
1270
  signalResumed: () => void;
1271
+ signalRestarted: (joinResp: JoinResponse) => void;
1149
1272
  closing: () => void;
1150
1273
  mediaTrackAdded: (
1151
1274
  track: MediaStreamTrack,
@@ -1155,4 +1278,5 @@ export type EngineEventCallbacks = {
1155
1278
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
1156
1279
  dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
1157
1280
  transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
1281
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1158
1282
  };
@@ -22,4 +22,6 @@ export interface ReconnectContext {
22
22
  * Reason for retrying
23
23
  */
24
24
  readonly retryReason?: Error;
25
+
26
+ readonly serverUrl?: string;
25
27
  }
@@ -0,0 +1,73 @@
1
+ import log from '../logger';
2
+ import type { RegionInfo, RegionSettings } from '../proto/livekit_rtc';
3
+ import { ConnectionError, ConnectionErrorReason } from './errors';
4
+ import { isCloud } from './utils';
5
+
6
+ export class RegionUrlProvider {
7
+ private serverUrl: URL;
8
+
9
+ private token: string;
10
+
11
+ private regionSettings: RegionSettings | undefined;
12
+
13
+ private lastUpdateAt: number = 0;
14
+
15
+ private settingsCacheTime = 3_000;
16
+
17
+ private attemptedRegions: RegionInfo[] = [];
18
+
19
+ constructor(url: string, token: string) {
20
+ this.serverUrl = new URL(url);
21
+ this.token = token;
22
+ }
23
+
24
+ isCloud() {
25
+ return isCloud(this.serverUrl);
26
+ }
27
+
28
+ async getNextBestRegionUrl(abortSignal?: AbortSignal) {
29
+ if (!this.isCloud()) {
30
+ throw Error('region availability is only supported for LiveKit Cloud domains');
31
+ }
32
+ if (!this.regionSettings || Date.now() - this.lastUpdateAt > this.settingsCacheTime) {
33
+ this.regionSettings = await this.fetchRegionSettings(abortSignal);
34
+ }
35
+ const regionsLeft = this.regionSettings.regions.filter(
36
+ (region) => !this.attemptedRegions.find((attempted) => attempted.url === region.url),
37
+ );
38
+ if (regionsLeft.length > 0) {
39
+ const nextRegion = regionsLeft[0];
40
+ this.attemptedRegions.push(nextRegion);
41
+ log.debug(`next region: ${nextRegion.region}`);
42
+ return nextRegion.url;
43
+ } else {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ resetAttempts() {
49
+ this.attemptedRegions = [];
50
+ }
51
+
52
+ private async fetchRegionSettings(signal?: AbortSignal) {
53
+ const regionSettingsResponse = await fetch(`${getCloudConfigUrl(this.serverUrl)}/regions`, {
54
+ headers: { authorization: `Bearer ${this.token}` },
55
+ signal,
56
+ });
57
+ if (regionSettingsResponse.ok) {
58
+ const regionSettings = (await regionSettingsResponse.json()) as RegionSettings;
59
+ this.lastUpdateAt = Date.now();
60
+ return regionSettings;
61
+ } else {
62
+ throw new ConnectionError(
63
+ `Could not fetch region settings: ${regionSettingsResponse.statusText}`,
64
+ regionSettingsResponse.status === 401 ? ConnectionErrorReason.NotAllowed : undefined,
65
+ regionSettingsResponse.status,
66
+ );
67
+ }
68
+ }
69
+ }
70
+
71
+ function getCloudConfigUrl(serverUrl: URL) {
72
+ return `${serverUrl.protocol.replace('ws', 'http')}//${serverUrl.host}/settings`;
73
+ }