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.
- package/README.md +20 -1
- package/dist/livekit-client.esm.mjs +2178 -1060
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +32 -0
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +315 -75
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +8 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +3 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +4 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +3 -1
- package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
- package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -1
- package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
- package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
- package/dist/ts4.2/src/room/Room.d.ts +4 -0
- package/dist/ts4.2/src/room/errors.d.ts +2 -1
- package/dist/ts4.2/src/room/events.d.ts +8 -2
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
- package/dist/ts4.2/src/room/types.d.ts +4 -0
- package/dist/ts4.2/src/room/utils.d.ts +4 -0
- package/package.json +19 -19
- package/src/api/SignalClient.ts +4 -4
- package/src/index.ts +3 -0
- package/src/proto/google/protobuf/timestamp.ts +15 -6
- package/src/proto/livekit_models.ts +903 -222
- package/src/proto/livekit_rtc.ts +1053 -279
- package/src/room/RTCEngine.ts +143 -40
- package/src/room/ReconnectPolicy.ts +2 -0
- package/src/room/RegionUrlProvider.ts +73 -0
- package/src/room/Room.ts +201 -132
- package/src/room/errors.ts +1 -0
- package/src/room/events.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +13 -6
- package/src/room/track/LocalTrack.ts +22 -8
- package/src/room/track/LocalVideoTrack.ts +12 -6
- package/src/room/track/RemoteTrackPublication.ts +4 -3
- package/src/room/track/RemoteVideoTrack.ts +5 -4
- package/src/room/track/Track.ts +46 -31
- package/src/room/types.ts +6 -0
- package/src/room/utils.ts +53 -0
package/src/room/RTCEngine.ts
CHANGED
@@ -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
|
-
|
815
|
-
|
816
|
-
|
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
|
-
|
823
|
-
|
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
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
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
|
-
|
849
|
-
|
877
|
+
if (this.shouldFailNext) {
|
878
|
+
this.shouldFailNext = false;
|
879
|
+
throw new Error('simulated failure');
|
880
|
+
}
|
850
881
|
|
851
|
-
|
852
|
-
|
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.
|
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
|
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
|
-
|
947
|
-
|
948
|
-
|
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
|
};
|
@@ -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
|
+
}
|