livekit-client 1.7.1 → 1.8.0

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 (73) hide show
  1. package/README.md +20 -1
  2. package/dist/livekit-client.esm.mjs +2178 -1060
  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/index.d.ts +3 -1
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  9. package/dist/src/proto/livekit_models.d.ts +32 -0
  10. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_rtc.d.ts +315 -75
  12. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  13. package/dist/src/room/RTCEngine.d.ts +8 -1
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  16. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  17. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  18. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  19. package/dist/src/room/Room.d.ts +4 -0
  20. package/dist/src/room/Room.d.ts.map +1 -1
  21. package/dist/src/room/errors.d.ts +2 -1
  22. package/dist/src/room/errors.d.ts.map +1 -1
  23. package/dist/src/room/events.d.ts +8 -2
  24. package/dist/src/room/events.d.ts.map +1 -1
  25. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  26. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  27. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  29. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  30. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  31. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
  32. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/Track.d.ts +3 -1
  34. package/dist/src/room/track/Track.d.ts.map +1 -1
  35. package/dist/src/room/types.d.ts +4 -0
  36. package/dist/src/room/types.d.ts.map +1 -1
  37. package/dist/src/room/utils.d.ts +4 -0
  38. package/dist/src/room/utils.d.ts.map +1 -1
  39. package/dist/ts4.2/src/index.d.ts +3 -1
  40. package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
  41. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
  42. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -1
  43. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  44. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  45. package/dist/ts4.2/src/room/Room.d.ts +4 -0
  46. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  47. package/dist/ts4.2/src/room/events.d.ts +8 -2
  48. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  49. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  50. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
  51. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  52. package/dist/ts4.2/src/room/types.d.ts +4 -0
  53. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  54. package/package.json +19 -19
  55. package/src/api/SignalClient.ts +4 -4
  56. package/src/index.ts +3 -0
  57. package/src/proto/google/protobuf/timestamp.ts +15 -6
  58. package/src/proto/livekit_models.ts +903 -222
  59. package/src/proto/livekit_rtc.ts +1053 -279
  60. package/src/room/RTCEngine.ts +143 -40
  61. package/src/room/ReconnectPolicy.ts +2 -0
  62. package/src/room/RegionUrlProvider.ts +73 -0
  63. package/src/room/Room.ts +201 -132
  64. package/src/room/errors.ts +1 -0
  65. package/src/room/events.ts +7 -0
  66. package/src/room/track/LocalAudioTrack.ts +13 -6
  67. package/src/room/track/LocalTrack.ts +22 -8
  68. package/src/room/track/LocalVideoTrack.ts +12 -6
  69. package/src/room/track/RemoteTrackPublication.ts +4 -3
  70. package/src/room/track/RemoteVideoTrack.ts +5 -4
  71. package/src/room/track/Track.ts +46 -31
  72. package/src/room/types.ts +6 -0
  73. package/src/room/utils.ts +53 -0
@@ -40,6 +40,7 @@ import type { SimulcastTrackInfo } from './track/LocalVideoTrack';
40
40
  import type { TrackPublishOptions, VideoCodec } from './track/options';
41
41
  import { Track } from './track/Track';
42
42
  import {
43
+ isCloud,
43
44
  isWeb,
44
45
  Mutex,
45
46
  sleep,
@@ -47,6 +48,7 @@ import {
47
48
  supportsSetCodecPreferences,
48
49
  supportsTransceiver,
49
50
  } from './utils';
51
+ import { RegionUrlProvider } from './RegionUrlProvider';
50
52
 
51
53
  const lossyDataChannel = '_lossy';
52
54
  const reliableDataChannel = '_reliable';
@@ -84,6 +86,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
84
86
 
85
87
  private reliableDC?: RTCDataChannel;
86
88
 
89
+ private dcBufferStatus: Map<DataPacket_Kind, boolean>;
90
+
87
91
  // @ts-ignore noUnusedLocals
88
92
  private reliableDCSub?: RTCDataChannel;
89
93
 
@@ -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,62 @@ 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
+ await this.waitForPCReconnected();
883
+ this.client.setReconnected();
884
+ this.regionUrlProvider?.resetAttempts();
885
+ // reconnect success
886
+ this.emit(EngineEvent.Restarted, joinResponse);
887
+ } catch (error) {
888
+ const nextRegionUrl = await this.regionUrlProvider?.getNextBestRegionUrl();
889
+ if (nextRegionUrl) {
890
+ await this.restartConnection(nextRegionUrl);
891
+ return;
892
+ } else {
893
+ // no more regions to try (or we're not on cloud)
894
+ this.regionUrlProvider?.resetAttempts();
895
+ throw error;
896
+ }
897
+ }
853
898
  }
854
899
 
855
900
  private async resumeConnection(reason?: ReconnectReason): Promise<void> {
@@ -877,6 +922,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
877
922
  if (e instanceof Error) {
878
923
  message = e.message;
879
924
  }
925
+ if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
926
+ throw new UnexpectedConnectionState('could not reconnect, token might be expired');
927
+ }
880
928
  throw new SignalReconnectError(message);
881
929
  }
882
930
  this.emit(EngineEvent.SignalResumed);
@@ -893,7 +941,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
893
941
  await this.publisher.createAndSendOffer({ iceRestart: true });
894
942
  }
895
943
 
896
- await this.waitForPCConnected();
944
+ await this.waitForPCReconnected();
897
945
  this.client.setReconnected();
898
946
 
899
947
  // recreate publish datachannel if it's id is null
@@ -906,7 +954,45 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
906
954
  this.emit(EngineEvent.Resumed);
907
955
  }
908
956
 
909
- async waitForPCConnected() {
957
+ async waitForPCInitialConnection(timeout?: number, abortController?: AbortController) {
958
+ if (this.pcState === PCState.Connected) {
959
+ return;
960
+ }
961
+ if (this.pcState !== PCState.New) {
962
+ throw new UnexpectedConnectionState(
963
+ 'Expected peer connection to be new on initial connection',
964
+ );
965
+ }
966
+ return new Promise<void>((resolve, reject) => {
967
+ const abortHandler = () => {
968
+ log.warn('closing engine');
969
+ CriticalTimers.clearTimeout(connectTimeout);
970
+
971
+ reject(
972
+ new ConnectionError(
973
+ 'room connection has been cancelled',
974
+ ConnectionErrorReason.Cancelled,
975
+ ),
976
+ );
977
+ };
978
+ if (abortController?.signal.aborted) {
979
+ abortHandler();
980
+ }
981
+ abortController?.signal.addEventListener('abort', abortHandler);
982
+ const onConnected = () => {
983
+ CriticalTimers.clearTimeout(connectTimeout);
984
+ abortController?.signal.removeEventListener('abort', abortHandler);
985
+ resolve();
986
+ };
987
+ const connectTimeout = CriticalTimers.setTimeout(() => {
988
+ this.off(EngineEvent.Connected, onConnected);
989
+ reject(new ConnectionError('could not establish pc connection'));
990
+ }, timeout ?? this.peerConnectionTimeout);
991
+ this.once(EngineEvent.Connected, onConnected);
992
+ });
993
+ }
994
+
995
+ async waitForPCReconnected() {
910
996
  const startTime = Date.now();
911
997
  let now = startTime;
912
998
  this.pcState = PCState.Reconnecting;
@@ -943,13 +1029,29 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
943
1029
  // make sure we do have a data connection
944
1030
  await this.ensurePublisherConnected(kind);
945
1031
 
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);
1032
+ const dc = this.dataChannelForKind(kind);
1033
+ if (dc) {
1034
+ dc.send(msg);
950
1035
  }
1036
+
1037
+ this.updateAndEmitDCBufferStatus(kind);
951
1038
  }
952
1039
 
1040
+ private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
1041
+ const status = this.isBufferStatusLow(kind);
1042
+ if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
1043
+ this.dcBufferStatus.set(kind, status);
1044
+ this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
1045
+ }
1046
+ };
1047
+
1048
+ private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
1049
+ const dc = this.dataChannelForKind(kind);
1050
+ if (dc) {
1051
+ return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
1052
+ }
1053
+ };
1054
+
953
1055
  /**
954
1056
  * @internal
955
1057
  */
@@ -1155,4 +1257,5 @@ export type EngineEventCallbacks = {
1155
1257
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
1156
1258
  dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
1157
1259
  transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
1260
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1158
1261
  };
@@ -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 type { RegionInfo, RegionSettings } from '../proto/livekit_rtc';
2
+ import { ConnectionError, ConnectionErrorReason } from './errors';
3
+ import log from '../logger';
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
+ }