livekit-client 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }