livekit-client 1.7.0 → 1.8.0

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