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.
- 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
|
+
}
|