livekit-client 1.7.0 → 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 (84) hide show
  1. package/README.md +20 -1
  2. package/dist/livekit-client.esm.mjs +2240 -1067
  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/options.d.ts +5 -0
  9. package/dist/src/options.d.ts.map +1 -1
  10. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_models.d.ts +32 -0
  12. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  13. package/dist/src/proto/livekit_rtc.d.ts +315 -75
  14. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +9 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  18. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  19. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  20. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  21. package/dist/src/room/Room.d.ts +6 -1
  22. package/dist/src/room/Room.d.ts.map +1 -1
  23. package/dist/src/room/defaults.d.ts.map +1 -1
  24. package/dist/src/room/errors.d.ts +2 -1
  25. package/dist/src/room/errors.d.ts.map +1 -1
  26. package/dist/src/room/events.d.ts +15 -2
  27. package/dist/src/room/events.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  29. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  31. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  34. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
  36. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  37. package/dist/src/room/track/Track.d.ts +3 -1
  38. package/dist/src/room/track/Track.d.ts.map +1 -1
  39. package/dist/src/room/track/utils.d.ts.map +1 -1
  40. package/dist/src/room/types.d.ts +4 -0
  41. package/dist/src/room/types.d.ts.map +1 -1
  42. package/dist/src/room/utils.d.ts +4 -0
  43. package/dist/src/room/utils.d.ts.map +1 -1
  44. package/dist/ts4.2/src/index.d.ts +3 -1
  45. package/dist/ts4.2/src/options.d.ts +5 -0
  46. package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
  47. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
  48. package/dist/ts4.2/src/room/RTCEngine.d.ts +9 -1
  49. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  50. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  51. package/dist/ts4.2/src/room/Room.d.ts +6 -1
  52. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  53. package/dist/ts4.2/src/room/events.d.ts +15 -2
  54. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
  55. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  56. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  57. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
  58. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  59. package/dist/ts4.2/src/room/types.d.ts +4 -0
  60. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  61. package/package.json +19 -19
  62. package/src/api/SignalClient.ts +4 -4
  63. package/src/index.ts +3 -0
  64. package/src/options.ts +6 -0
  65. package/src/proto/google/protobuf/timestamp.ts +15 -6
  66. package/src/proto/livekit_models.ts +903 -222
  67. package/src/proto/livekit_rtc.ts +1053 -279
  68. package/src/room/RTCEngine.ts +168 -56
  69. package/src/room/ReconnectPolicy.ts +2 -0
  70. package/src/room/RegionUrlProvider.ts +73 -0
  71. package/src/room/Room.ts +212 -133
  72. package/src/room/defaults.ts +1 -0
  73. package/src/room/errors.ts +1 -0
  74. package/src/room/events.ts +15 -0
  75. package/src/room/track/LocalAudioTrack.ts +14 -6
  76. package/src/room/track/LocalTrack.ts +22 -8
  77. package/src/room/track/LocalVideoTrack.ts +12 -6
  78. package/src/room/track/RemoteTrackPublication.ts +10 -4
  79. package/src/room/track/RemoteVideoTrack.test.ts +2 -0
  80. package/src/room/track/RemoteVideoTrack.ts +53 -9
  81. package/src/room/track/Track.ts +46 -31
  82. package/src/room/track/utils.ts +3 -2
  83. package/src/room/types.ts +6 -0
  84. 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
 
@@ -134,8 +138,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
134
138
 
135
139
  private closingLock: Mutex;
136
140
 
141
+ private dataProcessLock: Mutex;
142
+
137
143
  private shouldFailNext: boolean = false;
138
144
 
145
+ private regionUrlProvider?: RegionUrlProvider;
146
+
139
147
  constructor(private options: InternalRoomOptions) {
140
148
  super();
141
149
  this.client = new SignalClient();
@@ -143,6 +151,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
143
151
  this.reconnectPolicy = this.options.reconnectPolicy;
144
152
  this.registerOnLineListener();
145
153
  this.closingLock = new Mutex();
154
+ this.dataProcessLock = new Mutex();
155
+ this.dcBufferStatus = new Map([
156
+ [DataPacket_Kind.LOSSY, true],
157
+ [DataPacket_Kind.RELIABLE, true],
158
+ ]);
146
159
  }
147
160
 
148
161
  async join(
@@ -506,6 +519,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
506
519
  // handle datachannel errors
507
520
  this.lossyDC.onerror = this.handleDataError;
508
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;
509
530
  }
510
531
 
511
532
  private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
@@ -524,22 +545,28 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
524
545
  };
525
546
 
526
547
  private handleDataMessage = async (message: MessageEvent) => {
527
- // decode
528
- let buffer: ArrayBuffer | undefined;
529
- if (message.data instanceof ArrayBuffer) {
530
- buffer = message.data;
531
- } else if (message.data instanceof Blob) {
532
- buffer = await message.data.arrayBuffer();
533
- } else {
534
- log.error('unsupported data type', message.data);
535
- return;
536
- }
537
- const dp = DataPacket.decode(new Uint8Array(buffer));
538
- if (dp.value?.$case === 'speaker') {
539
- // dispatch speaker updates
540
- this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.speaker.speakers);
541
- } else if (dp.value?.$case === 'user') {
542
- this.emit(EngineEvent.DataPacketReceived, dp.value.user, dp.kind);
548
+ // make sure to respect incoming data message order by processing message events one after the other
549
+ const unlock = await this.dataProcessLock.lock();
550
+ try {
551
+ // decode
552
+ let buffer: ArrayBuffer | undefined;
553
+ if (message.data instanceof ArrayBuffer) {
554
+ buffer = message.data;
555
+ } else if (message.data instanceof Blob) {
556
+ buffer = await message.data.arrayBuffer();
557
+ } else {
558
+ log.error('unsupported data type', message.data);
559
+ return;
560
+ }
561
+ const dp = DataPacket.decode(new Uint8Array(buffer));
562
+ if (dp.value?.$case === 'speaker') {
563
+ // dispatch speaker updates
564
+ this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.speaker.speakers);
565
+ } else if (dp.value?.$case === 'user') {
566
+ this.emit(EngineEvent.DataPacketReceived, dp.value.user, dp.kind);
567
+ }
568
+ } finally {
569
+ unlock();
543
570
  }
544
571
  };
545
572
 
@@ -555,6 +582,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
555
582
  }
556
583
  };
557
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
+
558
593
  private setPreferredCodec(
559
594
  transceiver: RTCRtpTransceiver,
560
595
  kind: Track.Kind,
@@ -730,6 +765,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
730
765
  log.debug(`reconnecting in ${delay}ms`);
731
766
 
732
767
  this.clearReconnectTimeout();
768
+ if (this.url && this.token && isCloud(new URL(this.url))) {
769
+ this.regionUrlProvider = new RegionUrlProvider(this.url, this.token);
770
+ }
733
771
  this.reconnectTimeout = CriticalTimers.setTimeout(
734
772
  () => this.attemptReconnect(disconnectReason),
735
773
  delay,
@@ -801,46 +839,62 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
801
839
  return null;
802
840
  }
803
841
 
804
- private async restartConnection() {
805
- if (!this.url || !this.token) {
806
- // permanent failure, don't attempt reconnection
807
- throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
808
- }
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
+ }
809
848
 
810
- log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
811
- this.emit(EngineEvent.Restarting);
849
+ log.info(`reconnecting, attempt: ${this.reconnectAttempts}`);
850
+ this.emit(EngineEvent.Restarting);
812
851
 
813
- if (this.client.isConnected) {
814
- await this.client.sendLeave();
815
- }
816
- await this.client.close();
817
- this.primaryPC = undefined;
818
- this.publisher?.close();
819
- this.publisher = undefined;
820
- this.subscriber?.close();
821
- this.subscriber = undefined;
822
-
823
- let joinResponse: JoinResponse;
824
- try {
825
- if (!this.signalOpts) {
826
- 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
+ }
827
874
  throw new SignalReconnectError();
828
875
  }
829
- joinResponse = await this.join(this.url, this.token, this.signalOpts);
830
- } catch (e) {
831
- throw new SignalReconnectError();
832
- }
833
876
 
834
- if (this.shouldFailNext) {
835
- this.shouldFailNext = false;
836
- throw new Error('simulated failure');
837
- }
838
-
839
- await this.waitForPCConnected();
840
- this.client.setReconnected();
877
+ if (this.shouldFailNext) {
878
+ this.shouldFailNext = false;
879
+ throw new Error('simulated failure');
880
+ }
841
881
 
842
- // reconnect success
843
- 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
+ }
844
898
  }
845
899
 
846
900
  private async resumeConnection(reason?: ReconnectReason): Promise<void> {
@@ -868,6 +922,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
868
922
  if (e instanceof Error) {
869
923
  message = e.message;
870
924
  }
925
+ if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
926
+ throw new UnexpectedConnectionState('could not reconnect, token might be expired');
927
+ }
871
928
  throw new SignalReconnectError(message);
872
929
  }
873
930
  this.emit(EngineEvent.SignalResumed);
@@ -884,7 +941,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
884
941
  await this.publisher.createAndSendOffer({ iceRestart: true });
885
942
  }
886
943
 
887
- await this.waitForPCConnected();
944
+ await this.waitForPCReconnected();
888
945
  this.client.setReconnected();
889
946
 
890
947
  // recreate publish datachannel if it's id is null
@@ -897,7 +954,45 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
897
954
  this.emit(EngineEvent.Resumed);
898
955
  }
899
956
 
900
- 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() {
901
996
  const startTime = Date.now();
902
997
  let now = startTime;
903
998
  this.pcState = PCState.Reconnecting;
@@ -934,13 +1029,29 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
934
1029
  // make sure we do have a data connection
935
1030
  await this.ensurePublisherConnected(kind);
936
1031
 
937
- if (kind === DataPacket_Kind.LOSSY && this.lossyDC) {
938
- this.lossyDC.send(msg);
939
- } else if (kind === DataPacket_Kind.RELIABLE && this.reliableDC) {
940
- this.reliableDC.send(msg);
1032
+ const dc = this.dataChannelForKind(kind);
1033
+ if (dc) {
1034
+ dc.send(msg);
941
1035
  }
1036
+
1037
+ this.updateAndEmitDCBufferStatus(kind);
942
1038
  }
943
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
+
944
1055
  /**
945
1056
  * @internal
946
1057
  */
@@ -1146,4 +1257,5 @@ export type EngineEventCallbacks = {
1146
1257
  activeSpeakersUpdate: (speakers: Array<SpeakerInfo>) => void;
1147
1258
  dataPacketReceived: (userPacket: UserPacket, kind: DataPacket_Kind) => void;
1148
1259
  transportsCreated: (publisher: PCTransport, subscriber: PCTransport) => void;
1260
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1149
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
+ }