livekit-client 2.22.4 → 2.23.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/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +69 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1133 -289
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +26 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +53 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/rpc/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +18 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +11 -0
- package/dist/src/room/track/processor/types.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/WebSocketStream.d.ts +1 -0
- package/dist/ts4.2/api/utils.d.ts +1 -1
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/ts4.2/index.d.ts +2 -2
- package/dist/ts4.2/room/PCTransport.d.ts +26 -4
- package/dist/ts4.2/room/RTCEngine.d.ts +8 -0
- package/dist/ts4.2/room/Room.d.ts +53 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +4 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +18 -0
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/ts4.2/room/track/processor/types.d.ts +11 -0
- package/dist/ts4.2/room/utils.d.ts +28 -1
- package/package.json +1 -1
- package/src/api/SignalClient.test.ts +234 -0
- package/src/api/SignalClient.ts +58 -23
- package/src/api/WebSocketStream.test.ts +11 -0
- package/src/api/WebSocketStream.ts +22 -1
- package/src/api/utils.ts +1 -1
- package/src/e2ee/E2eeManager.test.ts +43 -0
- package/src/e2ee/E2eeManager.ts +6 -2
- package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
- package/src/e2ee/worker/FrameCryptor.ts +21 -4
- package/src/e2ee/worker/naluUtils.test.ts +123 -0
- package/src/e2ee/worker/naluUtils.ts +57 -2
- package/src/index.ts +2 -0
- package/src/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +144 -2
- package/src/room/PCTransport.ts +151 -9
- package/src/room/RTCEngine.test.ts +234 -2
- package/src/room/RTCEngine.ts +102 -7
- package/src/room/Room.test.ts +116 -0
- package/src/room/Room.ts +286 -74
- package/src/room/data-channel/DataChannelManager.test.ts +24 -7
- package/src/room/data-channel/DataChannelManager.ts +43 -38
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
- package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
- package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
- package/src/room/data-channel/LossyDataChannel.ts +10 -1
- package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
- package/src/room/data-channel/ReliableDataChannel.ts +16 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +31 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +11 -0
- package/src/room/participant/LocalParticipant.test.ts +136 -1
- package/src/room/participant/LocalParticipant.ts +66 -17
- package/src/room/participant/Participant.ts +6 -3
- package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
- package/src/room/rpc/utils.test.ts +32 -0
- package/src/room/rpc/utils.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +101 -10
- package/src/room/track/LocalTrack.ts +70 -13
- package/src/room/track/LocalVideoTrack.ts +7 -0
- package/src/room/track/RemoteAudioTrack.ts +56 -11
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +5 -0
- package/src/room/track/utils.ts +29 -5
- package/src/room/utils.test.ts +126 -0
- package/src/room/utils.ts +84 -21
package/src/room/PCTransport.ts
CHANGED
|
@@ -28,6 +28,9 @@ interface TrackBitrateInfo {
|
|
|
28
28
|
* Why same for all codecs: Target bitrate already accounts for codec efficiency
|
|
29
29
|
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
|
|
30
30
|
* Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
|
|
31
|
+
* The cap is lowered further for connections that were slow to set up, see
|
|
32
|
+
* `computeStartBitrateCap`, or that the browser reports as slow, see
|
|
33
|
+
* `connectionEffectiveTypeStartBitrateCap`.
|
|
31
34
|
*/
|
|
32
35
|
const startBitrateMultiplier = 0.9;
|
|
33
36
|
|
|
@@ -35,11 +38,35 @@ const startBitrateMultiplier = 0.9;
|
|
|
35
38
|
const maxStartBitrateKbps = 1000;
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
|
-
* Minimum
|
|
39
|
-
* real capacity costs more than the ramp it
|
|
41
|
+
* Minimum x-google-start-bitrate in kbps: libwebrtc's own default starting estimate. A target
|
|
42
|
+
* below this gets no hint, since seeding above the real capacity costs more than the ramp it
|
|
43
|
+
* saves, and a slow connection is seeded no higher than this.
|
|
40
44
|
*/
|
|
41
45
|
const minTargetBitrateKbps = 300;
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Connection setup times (ms) bounding the ramp in `computeStartBitrateCap`: at or below the
|
|
49
|
+
* first the hint is capped at `maxStartBitrateKbps`, at or above the second at
|
|
50
|
+
* `minTargetBitrateKbps`. Measured on shaped links (CLT-3380): healthy links set up in under
|
|
51
|
+
* 860 ms, a 1 Mbps link with 150 ms RTT took 1.1–1.7 s, a 500 kbps link ~2.3 s at the median,
|
|
52
|
+
* and a 300 kbps link never under 3 s. The first sits above the 1 Mbps link, which a lower seed
|
|
53
|
+
* only slows down, and the second above the 500 kbps link's median, so only links that cannot
|
|
54
|
+
* carry more land at the floor.
|
|
55
|
+
*/
|
|
56
|
+
const setupTimeForMaxStartBitrateMs = 1500;
|
|
57
|
+
const setupTimeForMinStartBitrateMs = 3500;
|
|
58
|
+
|
|
59
|
+
/** Cap on the start bitrate hint, in kbps, when the browser reports a `3g` connection. */
|
|
60
|
+
const effectiveType3gStartBitrateKbps = 500;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The part of the Network Information API (`navigator.connection`) read here. It is Chromium
|
|
64
|
+
* only, so TypeScript's DOM typings do not declare it.
|
|
65
|
+
*/
|
|
66
|
+
interface NetworkInformation {
|
|
67
|
+
readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
const debounceInterval = 20;
|
|
44
71
|
|
|
45
72
|
/**
|
|
@@ -71,17 +98,84 @@ export function findTrackCodecPayload(
|
|
|
71
98
|
* high-bitrate track; screen share is exempt, because its content needs the bitrate
|
|
72
99
|
* immediately to stay legible.
|
|
73
100
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
101
|
+
* The cap is lowered for a connection that was slow to set up (`computeStartBitrateCap`), and
|
|
102
|
+
* for one the browser reports as slow (`connectionEffectiveTypeStartBitrateCap`), whichever is
|
|
103
|
+
* lower. Screen share is exempt from both, however slow the connection.
|
|
76
104
|
*
|
|
77
105
|
* @internal
|
|
78
106
|
*/
|
|
79
|
-
export function computeTrackStartBitrate(
|
|
107
|
+
export function computeTrackStartBitrate(
|
|
108
|
+
trackbr: TrackBitrateInfo,
|
|
109
|
+
connectionSetupTimeMs?: number,
|
|
110
|
+
): number | undefined {
|
|
80
111
|
if (trackbr.maxbr < minTargetBitrateKbps) {
|
|
81
112
|
return undefined;
|
|
82
113
|
}
|
|
83
|
-
const calculated = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
84
|
-
|
|
114
|
+
const calculated = Math.min(Math.round(trackbr.maxbr * startBitrateMultiplier), trackbr.maxbr);
|
|
115
|
+
if (trackbr.isScreenShare) {
|
|
116
|
+
return calculated;
|
|
117
|
+
}
|
|
118
|
+
const cap = Math.min(
|
|
119
|
+
computeStartBitrateCap(connectionSetupTimeMs),
|
|
120
|
+
connectionEffectiveTypeStartBitrateCap(),
|
|
121
|
+
);
|
|
122
|
+
return Math.min(calculated, cap);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cap on the start bitrate hint, in kbps, from the browser's effective connection type
|
|
127
|
+
* (`navigator.connection.effectiveType`, Chromium only): `slow-2g` and `2g` cap at
|
|
128
|
+
* `minTargetBitrateKbps`, `3g` at `effectiveType3gStartBitrateKbps`.
|
|
129
|
+
*
|
|
130
|
+
* It catches links that have low bandwidth but low latency, which set up quickly and so slip
|
|
131
|
+
* past the setup time ramp. `4g`, and browsers without the API, give no guidance and stay at the
|
|
132
|
+
* `maxStartBitrateKbps` ceiling, as `computeStartBitrateCap` does without a setup time. The
|
|
133
|
+
* connection RTT the API also reports is not used: it lags behind network changes, and the setup
|
|
134
|
+
* time already reflects latency.
|
|
135
|
+
*/
|
|
136
|
+
function connectionEffectiveTypeStartBitrateCap(): number {
|
|
137
|
+
// A `typeof` guard, since `globalThis` is missing from some supported browsers (Chrome < 71,
|
|
138
|
+
// Firefox < 65, Safari < 12.1) and the build does not polyfill it.
|
|
139
|
+
const connection =
|
|
140
|
+
typeof navigator !== 'undefined'
|
|
141
|
+
? (navigator as Navigator & { connection?: NetworkInformation }).connection
|
|
142
|
+
: undefined;
|
|
143
|
+
switch (connection?.effectiveType) {
|
|
144
|
+
case 'slow-2g':
|
|
145
|
+
case '2g':
|
|
146
|
+
return minTargetBitrateKbps;
|
|
147
|
+
case '3g':
|
|
148
|
+
return effectiveType3gStartBitrateKbps;
|
|
149
|
+
default:
|
|
150
|
+
return maxStartBitrateKbps;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Cap on the start bitrate hint, in kbps, from how long the connection took to set up.
|
|
156
|
+
*
|
|
157
|
+
* Connection setup time (signaling join plus ICE/DTLS) is the network signal every platform has
|
|
158
|
+
* before the first video offer, since libwebrtc cannot probe the path until a video sender
|
|
159
|
+
* exists. It grows with round-trip time and loss, which also mark the links where a 1 Mbps
|
|
160
|
+
* seed overshoots, so the cap ramps linearly from `maxStartBitrateKbps` at
|
|
161
|
+
* `setupTimeForMaxStartBitrateMs` down to `minTargetBitrateKbps` at
|
|
162
|
+
* `setupTimeForMinStartBitrateMs`. A video offer built while the connection is still being set
|
|
163
|
+
* up (publishing during connect) passes the time elapsed so far, a lower bound on the setup time.
|
|
164
|
+
* Without any timing the cap stays at the 1 Mbps ceiling.
|
|
165
|
+
*/
|
|
166
|
+
function computeStartBitrateCap(connectionSetupTimeMs?: number): number {
|
|
167
|
+
if (connectionSetupTimeMs === undefined) {
|
|
168
|
+
return maxStartBitrateKbps;
|
|
169
|
+
}
|
|
170
|
+
const ramp = Math.min(
|
|
171
|
+
Math.max(
|
|
172
|
+
(connectionSetupTimeMs - setupTimeForMaxStartBitrateMs) /
|
|
173
|
+
(setupTimeForMinStartBitrateMs - setupTimeForMaxStartBitrateMs),
|
|
174
|
+
0,
|
|
175
|
+
),
|
|
176
|
+
1,
|
|
177
|
+
);
|
|
178
|
+
return Math.round(maxStartBitrateKbps - ramp * (maxStartBitrateKbps - minTargetBitrateKbps));
|
|
85
179
|
}
|
|
86
180
|
|
|
87
181
|
/**
|
|
@@ -114,6 +208,7 @@ export function computeTrackStartBitrate(trackbr: TrackBitrateInfo): number | un
|
|
|
114
208
|
export function computeConnectionStartBitrate(
|
|
115
209
|
media: MediaDescription[],
|
|
116
210
|
trackBitrates: TrackBitrateInfo[],
|
|
211
|
+
connectionSetupTimeMs?: number,
|
|
117
212
|
): number | undefined {
|
|
118
213
|
let connectionStartBitrate: number | undefined;
|
|
119
214
|
for (const m of media) {
|
|
@@ -128,7 +223,8 @@ export function computeConnectionStartBitrate(
|
|
|
128
223
|
if (codecPayload === undefined) {
|
|
129
224
|
continue;
|
|
130
225
|
}
|
|
131
|
-
const startBitrate =
|
|
226
|
+
const startBitrate =
|
|
227
|
+
codecPayload > 0 ? computeTrackStartBitrate(trackbr, connectionSetupTimeMs) : undefined;
|
|
132
228
|
if (
|
|
133
229
|
startBitrate !== undefined &&
|
|
134
230
|
(connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)
|
|
@@ -234,6 +330,20 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
234
330
|
*/
|
|
235
331
|
private hasAppliedVideoStartBitrate = false;
|
|
236
332
|
|
|
333
|
+
/**
|
|
334
|
+
* How long this peer connection took to set up, in ms, recorded once the initial connect
|
|
335
|
+
* succeeds. Lowers the start bitrate hint for a slow connection; see `computeStartBitrateCap`.
|
|
336
|
+
*/
|
|
337
|
+
private connectionSetupTimeMs?: number;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* When the connection attempt that built this peer connection began (`performance.now()`).
|
|
341
|
+
* Until `connectionSetupTimeMs` is known, the time elapsed since then is used as a lower bound
|
|
342
|
+
* on the setup time, so a video published during connect is still seeded from it rather than
|
|
343
|
+
* waiting for the connection, which would delay the offer.
|
|
344
|
+
*/
|
|
345
|
+
private connectionStartedAt?: number;
|
|
346
|
+
|
|
237
347
|
remoteStereoMids: string[] = [];
|
|
238
348
|
|
|
239
349
|
remoteNackMids: string[] = [];
|
|
@@ -584,7 +694,11 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
584
694
|
// published (data channel or audio only) find no target and leave the latch unset.
|
|
585
695
|
const connectionStartBitrate = this.hasAppliedVideoStartBitrate
|
|
586
696
|
? undefined
|
|
587
|
-
: computeConnectionStartBitrate(
|
|
697
|
+
: computeConnectionStartBitrate(
|
|
698
|
+
sdpParsed.media,
|
|
699
|
+
this.trackBitrates,
|
|
700
|
+
this.connectionSetupTimeEstimateMs(),
|
|
701
|
+
);
|
|
588
702
|
let appliedVideoStartBitrate = false;
|
|
589
703
|
sdpParsed.media.forEach((media) => {
|
|
590
704
|
ensureIPAddrMatchVersion(media);
|
|
@@ -641,6 +755,10 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
641
755
|
// one-shot hint once the SDP carrying it has been accepted locally.
|
|
642
756
|
if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
|
|
643
757
|
this.hasAppliedVideoStartBitrate = true;
|
|
758
|
+
this.log.info('applied x-google-start-bitrate', {
|
|
759
|
+
startBitrateKbps: connectionStartBitrate,
|
|
760
|
+
connectionSetupTimeMs: this.connectionSetupTimeEstimateMs(),
|
|
761
|
+
});
|
|
644
762
|
}
|
|
645
763
|
this.onOffer(offer, this.latestOfferId);
|
|
646
764
|
} finally {
|
|
@@ -695,6 +813,30 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
695
813
|
this.trackBitrates.push(info);
|
|
696
814
|
}
|
|
697
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Record how long this peer connection took to set up. Called once, when the initial connect
|
|
818
|
+
* succeeds; resumes and ICE restarts keep the estimator they have and never call this.
|
|
819
|
+
*/
|
|
820
|
+
setConnectionSetupTime(ms: number) {
|
|
821
|
+
this.connectionSetupTimeMs = ms;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Record when the connection attempt that built this peer connection began. */
|
|
825
|
+
setConnectionStartedAt(startedAt: number) {
|
|
826
|
+
this.connectionStartedAt = startedAt;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** The measured setup time, or while still connecting, the time elapsed so far. */
|
|
830
|
+
private connectionSetupTimeEstimateMs(): number | undefined {
|
|
831
|
+
if (this.connectionSetupTimeMs !== undefined) {
|
|
832
|
+
return this.connectionSetupTimeMs;
|
|
833
|
+
}
|
|
834
|
+
if (this.connectionStartedAt === undefined) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
return Math.round(performance.now() - this.connectionStartedAt);
|
|
838
|
+
}
|
|
839
|
+
|
|
698
840
|
setConfiguration(rtcConfig: RTCConfiguration) {
|
|
699
841
|
if (!this._pc) {
|
|
700
842
|
throw new UnexpectedConnectionState('PC closed, cannot configure');
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DataPacket,
|
|
3
3
|
DataPacket_Kind,
|
|
4
|
+
JoinResponse,
|
|
4
5
|
ConnectionQuality as ProtoConnectionQuality,
|
|
5
6
|
UserPacket,
|
|
6
7
|
} from '@livekit/protocol';
|
|
7
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { SignalConnectionState, type SignalOptions } from '../api/SignalClient';
|
|
8
10
|
import type { DataPacketBuffer } from '../utils/dataPacketBuffer';
|
|
11
|
+
import DefaultReconnectPolicy from './DefaultReconnectPolicy';
|
|
9
12
|
import { PCTransportState } from './PCTransportManager';
|
|
10
13
|
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
14
|
+
import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
|
11
15
|
import { roomOptionDefaults } from './defaults';
|
|
12
|
-
import { PublishDataError, UnexpectedConnectionState } from './errors';
|
|
16
|
+
import { ConnectionError, PublishDataError, UnexpectedConnectionState } from './errors';
|
|
13
17
|
import { EngineEvent } from './events';
|
|
18
|
+
import CriticalTimers from './timers';
|
|
19
|
+
import { Future } from './utils';
|
|
14
20
|
|
|
15
21
|
describe('RTCEngine', () => {
|
|
16
22
|
const originalRTCRtpSender = window.RTCRtpSender;
|
|
@@ -310,7 +316,13 @@ describe('RTCEngine', () => {
|
|
|
310
316
|
|
|
311
317
|
bufferedAmountLowThreshold = 64 * 1024;
|
|
312
318
|
|
|
319
|
+
readyState: RTCDataChannelState = 'open';
|
|
320
|
+
|
|
313
321
|
send = vi.fn();
|
|
322
|
+
|
|
323
|
+
close = vi.fn(() => {
|
|
324
|
+
this.readyState = 'closing';
|
|
325
|
+
});
|
|
314
326
|
}
|
|
315
327
|
|
|
316
328
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
@@ -537,6 +549,37 @@ describe('RTCEngine', () => {
|
|
|
537
549
|
});
|
|
538
550
|
});
|
|
539
551
|
|
|
552
|
+
describe('createDataChannels', () => {
|
|
553
|
+
it('makes the next sender wait for replacement channels to open', () => {
|
|
554
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
555
|
+
const pcManager = {
|
|
556
|
+
createPublisherDataChannel: vi.fn((label: string) =>
|
|
557
|
+
Object.assign(new FakeDataChannel(), { label }),
|
|
558
|
+
),
|
|
559
|
+
};
|
|
560
|
+
const connected = Promise.resolve();
|
|
561
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
562
|
+
pcManager,
|
|
563
|
+
publisherConnectionPromise: connected,
|
|
564
|
+
});
|
|
565
|
+
const internals = engine as unknown as {
|
|
566
|
+
createDataChannels: () => void;
|
|
567
|
+
publisherConnectionPromise?: Promise<void>;
|
|
568
|
+
dataChannels: { lossy: { stopThresholdTuning: () => void } };
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// First creation (nothing to replace) keeps the memoized readiness check.
|
|
572
|
+
internals.createDataChannels();
|
|
573
|
+
expect(internals.publisherConnectionPromise).toBe(connected);
|
|
574
|
+
|
|
575
|
+
// Recreation (the Safari null-id resume path) swaps in channels that start out connecting.
|
|
576
|
+
internals.createDataChannels();
|
|
577
|
+
expect(internals.publisherConnectionPromise).toBeUndefined();
|
|
578
|
+
|
|
579
|
+
internals.dataChannels.lossy.stopThresholdTuning();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
540
583
|
describe('handleDataChannelClose', () => {
|
|
541
584
|
function stubCloseEnv(
|
|
542
585
|
engine: RTCEngine,
|
|
@@ -701,6 +744,123 @@ describe('RTCEngine', () => {
|
|
|
701
744
|
});
|
|
702
745
|
});
|
|
703
746
|
|
|
747
|
+
describe('closing during a full reconnect', () => {
|
|
748
|
+
interface ReconnectInternals {
|
|
749
|
+
_isClosed: boolean;
|
|
750
|
+
url: string;
|
|
751
|
+
token: string;
|
|
752
|
+
signalOpts: SignalOptions;
|
|
753
|
+
attemptReconnect: () => Promise<void>;
|
|
754
|
+
configure: () => Promise<void>;
|
|
755
|
+
waitForPCReconnected: () => Promise<void>;
|
|
756
|
+
reconnectAbortController?: AbortController;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function primeEngine() {
|
|
760
|
+
const engine = new RTCEngine({ ...roomOptionDefaults, singlePeerConnection: false });
|
|
761
|
+
const internals = engine as unknown as ReconnectInternals;
|
|
762
|
+
internals._isClosed = false;
|
|
763
|
+
internals.url = 'wss://test.livekit.io';
|
|
764
|
+
internals.token = 'test-token';
|
|
765
|
+
internals.signalOpts = { autoSubscribe: true, maxRetries: 1, websocketTimeout: 15_000 };
|
|
766
|
+
engine.fullReconnectOnNext = true;
|
|
767
|
+
const configure = vi.spyOn(internals, 'configure').mockResolvedValue();
|
|
768
|
+
vi.spyOn(internals, 'waitForPCReconnected').mockResolvedValue();
|
|
769
|
+
const join = vi
|
|
770
|
+
.spyOn(engine.client, 'join')
|
|
771
|
+
.mockResolvedValue(new JoinResponse({ subscriberPrimary: true }));
|
|
772
|
+
return { engine, internals, configure, join };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
it('does not join after close completes while reconnect cleanup is pending', async () => {
|
|
776
|
+
const { engine, internals, join } = primeEngine();
|
|
777
|
+
const cleanupStarted = new Future<void, Error>();
|
|
778
|
+
const finishCleanup = new Future<void, Error>();
|
|
779
|
+
vi.spyOn(engine, 'cleanupPeerConnections').mockImplementationOnce(async () => {
|
|
780
|
+
cleanupStarted.resolve?.();
|
|
781
|
+
await finishCleanup.promise;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const reconnect = internals.attemptReconnect();
|
|
785
|
+
await cleanupStarted.promise;
|
|
786
|
+
const controller = internals.reconnectAbortController;
|
|
787
|
+
expect(controller).toBeDefined();
|
|
788
|
+
await engine.close();
|
|
789
|
+
expect(controller?.signal.aborted).toBe(true);
|
|
790
|
+
expect(internals.reconnectAbortController).toBeUndefined();
|
|
791
|
+
finishCleanup.resolve?.();
|
|
792
|
+
await reconnect;
|
|
793
|
+
|
|
794
|
+
expect(internals.reconnectAbortController).toBeUndefined();
|
|
795
|
+
expect(join).not.toHaveBeenCalled();
|
|
796
|
+
expect(engine.isClosed).toBe(true);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('does not join another region after close completes during region selection', async () => {
|
|
800
|
+
const { engine, internals, join } = primeEngine();
|
|
801
|
+
join.mockRejectedValue(ConnectionError.internal('signal connection failed'));
|
|
802
|
+
const selectingRegion = new Future<void, Error>();
|
|
803
|
+
const nextRegion = new Future<string | null, Error>();
|
|
804
|
+
const getNextUrl = vi.fn(async () => {
|
|
805
|
+
selectingRegion.resolve?.();
|
|
806
|
+
return nextRegion.promise;
|
|
807
|
+
});
|
|
808
|
+
engine.setRegionStrategy({ getNextUrl, resetAttempts: () => {} });
|
|
809
|
+
|
|
810
|
+
const reconnect = internals.attemptReconnect();
|
|
811
|
+
await selectingRegion.promise;
|
|
812
|
+
await engine.close();
|
|
813
|
+
nextRegion.resolve?.('wss://another-region.livekit.io');
|
|
814
|
+
// A second selection must terminate even on the unfixed implementation.
|
|
815
|
+
getNextUrl.mockResolvedValue(null);
|
|
816
|
+
await reconnect;
|
|
817
|
+
|
|
818
|
+
expect(join).toHaveBeenCalledTimes(1);
|
|
819
|
+
expect(getNextUrl).toHaveBeenCalledTimes(1);
|
|
820
|
+
expect(engine.isClosed).toBe(true);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('does not reopen the engine when a signal join resolves after close', async () => {
|
|
824
|
+
const { engine, internals, configure, join } = primeEngine();
|
|
825
|
+
const joining = new Future<void, Error>();
|
|
826
|
+
const joinResponse = new Future<JoinResponse, Error>();
|
|
827
|
+
join.mockImplementationOnce(async () => {
|
|
828
|
+
joining.resolve?.();
|
|
829
|
+
return joinResponse.promise;
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const reconnect = internals.attemptReconnect();
|
|
833
|
+
await joining.promise;
|
|
834
|
+
await engine.close();
|
|
835
|
+
joinResponse.resolve?.(new JoinResponse({ subscriberPrimary: true }));
|
|
836
|
+
await reconnect;
|
|
837
|
+
|
|
838
|
+
expect(engine.isClosed).toBe(true);
|
|
839
|
+
expect(configure).not.toHaveBeenCalled();
|
|
840
|
+
expect(join.mock.calls[0][3]?.aborted).toBe(true);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('still completes a full reconnect while the engine is active', async () => {
|
|
844
|
+
const { engine, internals, join } = primeEngine();
|
|
845
|
+
vi.spyOn(engine.client, 'sendLeave').mockResolvedValue();
|
|
846
|
+
vi.spyOn(engine.client, 'currentState', 'get').mockReturnValue(
|
|
847
|
+
SignalConnectionState.CONNECTED,
|
|
848
|
+
);
|
|
849
|
+
const restarted = vi.fn();
|
|
850
|
+
engine.on(EngineEvent.Restarted, restarted);
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
await internals.attemptReconnect();
|
|
854
|
+
|
|
855
|
+
expect(join).toHaveBeenCalledTimes(1);
|
|
856
|
+
expect(restarted).toHaveBeenCalledTimes(1);
|
|
857
|
+
expect(engine.isClosed).toBe(false);
|
|
858
|
+
} finally {
|
|
859
|
+
await engine.close();
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
704
864
|
describe('reconnect requested mid-attempt', () => {
|
|
705
865
|
// A full reconnect requested while a resume is already in flight (e.g. a server
|
|
706
866
|
// RECONNECT leave racing the resume) sets `fullReconnectOnNext` mid-attempt. A
|
|
@@ -711,7 +871,7 @@ describe('RTCEngine', () => {
|
|
|
711
871
|
clientConfiguration: unknown;
|
|
712
872
|
pcManager: unknown;
|
|
713
873
|
resumeConnection: (reason?: number) => Promise<void>;
|
|
714
|
-
restartConnection: (regionUrl?: string) => Promise<void>;
|
|
874
|
+
restartConnection: (abortSignal: AbortSignal, regionUrl?: string) => Promise<void>;
|
|
715
875
|
clearPendingReconnect: () => void;
|
|
716
876
|
handleDisconnect: (connection: string, reason?: number) => void;
|
|
717
877
|
attemptReconnect: (reason?: number) => Promise<void>;
|
|
@@ -1003,4 +1163,76 @@ describe('RTCEngine', () => {
|
|
|
1003
1163
|
expect(pendingAborts(engine).size).toBe(0);
|
|
1004
1164
|
});
|
|
1005
1165
|
});
|
|
1166
|
+
|
|
1167
|
+
describe('server-requested reconnect delay', () => {
|
|
1168
|
+
// A RECONNECT or RESUME leave from the server skips any remaining backoff. It must still go
|
|
1169
|
+
// through the reconnect policy, so that clients told to reconnect together spread out.
|
|
1170
|
+
interface DisconnectInternals {
|
|
1171
|
+
_isClosed: boolean;
|
|
1172
|
+
reconnectAttempts: number;
|
|
1173
|
+
handleDisconnect: (connection: string, reason?: number) => void;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function scheduledDelay(
|
|
1177
|
+
nextRetryDelayInMs: ReconnectPolicy['nextRetryDelayInMs'],
|
|
1178
|
+
attempts = 0,
|
|
1179
|
+
) {
|
|
1180
|
+
const engine = new RTCEngine({
|
|
1181
|
+
...roomOptionDefaults,
|
|
1182
|
+
reconnectPolicy: { nextRetryDelayInMs },
|
|
1183
|
+
});
|
|
1184
|
+
const internals = engine as unknown as DisconnectInternals;
|
|
1185
|
+
internals._isClosed = false;
|
|
1186
|
+
internals.reconnectAttempts = attempts;
|
|
1187
|
+
const setTimeoutSpy = vi
|
|
1188
|
+
.spyOn(CriticalTimers, 'setTimeout')
|
|
1189
|
+
.mockReturnValue(0 as unknown as ReturnType<typeof setTimeout>);
|
|
1190
|
+
internals.handleDisconnect('leave-reconnect');
|
|
1191
|
+
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
1192
|
+
return setTimeoutSpy.mock.calls[0][1];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
afterEach(() => {
|
|
1196
|
+
vi.restoreAllMocks();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('uses the first retry delay of the policy instead of reconnecting immediately', () => {
|
|
1200
|
+
const nextRetryDelayInMs = vi.fn((context: ReconnectContext) =>
|
|
1201
|
+
context.retryCount === 0 ? 321 : 5000,
|
|
1202
|
+
);
|
|
1203
|
+
expect(scheduledDelay(nextRetryDelayInMs)).toBe(321);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('skips the remaining backoff when the request arrives during a later retry', () => {
|
|
1207
|
+
const nextRetryDelayInMs = vi.fn((context: ReconnectContext) =>
|
|
1208
|
+
context.retryCount === 0 ? 321 : 5000,
|
|
1209
|
+
);
|
|
1210
|
+
expect(scheduledDelay(nextRetryDelayInMs, 3)).toBe(321);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('gives up when the policy stops on the first retry delay', () => {
|
|
1214
|
+
const engine = new RTCEngine({
|
|
1215
|
+
...roomOptionDefaults,
|
|
1216
|
+
reconnectPolicy: {
|
|
1217
|
+
nextRetryDelayInMs: (context: ReconnectContext) =>
|
|
1218
|
+
context.retryCount === 0 ? null : 5000,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
const internals = engine as unknown as DisconnectInternals;
|
|
1222
|
+
internals._isClosed = false;
|
|
1223
|
+
internals.reconnectAttempts = 3;
|
|
1224
|
+
const setTimeoutSpy = vi.spyOn(CriticalTimers, 'setTimeout');
|
|
1225
|
+
const disconnected = vi.fn();
|
|
1226
|
+
engine.on(EngineEvent.Disconnected, disconnected);
|
|
1227
|
+
internals.handleDisconnect('leave-reconnect');
|
|
1228
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
1229
|
+
expect(disconnected).toHaveBeenCalledTimes(1);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
it('spreads out clients that use the default policy', () => {
|
|
1233
|
+
const defaultPolicy = new DefaultReconnectPolicy();
|
|
1234
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
1235
|
+
expect(scheduledDelay((context) => defaultPolicy.nextRetryDelayInMs(context))).toBe(250);
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1006
1238
|
});
|