livekit-client 2.22.3 → 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/README.md +35 -0
- 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 +71 -8
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1746 -547
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +2 -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 +4 -2
- 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/options.d.ts +14 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +100 -7
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +16 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +54 -1
- 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/defaults.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +18 -0
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +3 -2
- package/dist/src/room/events.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/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.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 +9 -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 +13 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +60 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/compressionStreamStubs.d.ts +12 -0
- package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +187 -185
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
- package/dist/ts4.2/api/WebSocketStream.d.ts +34 -33
- package/dist/ts4.2/api/utils.d.ts +9 -9
- package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
- package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
- package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
- package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
- package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
- package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
- package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
- package/dist/ts4.2/e2ee/constants.d.ts +21 -21
- package/dist/ts4.2/e2ee/errors.d.ts +12 -12
- package/dist/ts4.2/e2ee/events.d.ts +39 -39
- package/dist/ts4.2/e2ee/index.d.ts +6 -6
- package/dist/ts4.2/e2ee/types.d.ts +231 -231
- package/dist/ts4.2/e2ee/utils.d.ts +32 -32
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
- package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
- package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
- package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +39 -28
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
- package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
- package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
- package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
- package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
- package/dist/ts4.2/index.d.ts +76 -76
- package/dist/ts4.2/logger.d.ts +66 -66
- package/dist/ts4.2/options.d.ts +163 -149
- package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
- package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
- package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
- package/dist/ts4.2/room/PCTransport.d.ts +268 -175
- package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
- package/dist/ts4.2/room/RTCEngine.d.ts +277 -262
- package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
- package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
- package/dist/ts4.2/room/Room.d.ts +407 -365
- package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -95
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
- package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
- package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
- package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
- package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
- package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
- package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
- package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
- package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
- package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
- package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
- package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
- package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
- package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
- package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
- package/dist/ts4.2/room/data-track/types.d.ts +22 -22
- package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
- package/dist/ts4.2/room/debounce.d.ts +11 -11
- package/dist/ts4.2/room/defaults.d.ts +9 -9
- package/dist/ts4.2/room/errors.d.ts +180 -162
- package/dist/ts4.2/room/events.d.ts +638 -637
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +291 -287
- package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
- package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
- package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/index.d.ts +7 -7
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
- package/dist/ts4.2/room/stats.d.ts +71 -71
- package/dist/ts4.2/room/statsSummary.d.ts +13 -13
- package/dist/ts4.2/room/timers.d.ts +12 -12
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
- package/dist/ts4.2/room/token-source/types.d.ts +70 -70
- package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
- package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +62 -42
- package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
- package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -55
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
- package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
- package/dist/ts4.2/room/track/Track.d.ts +156 -166
- package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
- package/dist/ts4.2/room/track/create.d.ts +25 -25
- package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
- package/dist/ts4.2/room/track/options.d.ts +403 -403
- package/dist/ts4.2/room/track/processor/types.d.ts +42 -31
- package/dist/ts4.2/room/track/record.d.ts +13 -13
- package/dist/ts4.2/room/track/types.d.ts +28 -28
- package/dist/ts4.2/room/track/utils.d.ts +60 -47
- package/dist/ts4.2/room/types.d.ts +127 -127
- package/dist/ts4.2/room/utils.d.ts +273 -214
- package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
- package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
- package/dist/ts4.2/test/mocks.d.ts +12 -12
- package/dist/ts4.2/test/promiseState.d.ts +12 -12
- package/dist/ts4.2/test/signalToken.d.ts +26 -26
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
- package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
- package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
- package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
- package/dist/ts4.2/utils/browserParser.d.ts +13 -13
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
- package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
- package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
- package/dist/ts4.2/utils/serializer.d.ts +48 -48
- package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
- package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
- package/dist/ts4.2/version.d.ts +15 -15
- package/package.json +5 -4
- package/src/api/SignalClient.e2e.test.ts +1 -3
- package/src/api/SignalClient.test.ts +394 -10
- package/src/api/SignalClient.ts +132 -76
- 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/options.ts +16 -0
- package/src/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +327 -9
- package/src/room/PCTransport.ts +339 -36
- package/src/room/PCTransportManager.ts +3 -1
- package/src/room/RTCEngine.test.ts +318 -2
- package/src/room/RTCEngine.ts +126 -12
- package/src/room/Room.test.ts +148 -6
- package/src/room/Room.ts +312 -80
- 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 +47 -20
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -5
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.test.ts +36 -0
- package/src/room/errors.ts +27 -0
- package/src/room/events.ts +2 -1
- 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/publishUtils.ts +0 -19
- package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
- package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
- package/src/room/rpc/client/RpcClientManager.ts +32 -15
- 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 +87 -19
- package/src/room/track/LocalVideoTrack.ts +17 -3
- package/src/room/track/RemoteAudioTrack.ts +59 -14
- package/src/room/track/RemoteVideoTrack.test.ts +210 -0
- package/src/room/track/RemoteVideoTrack.ts +29 -3
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +206 -2
- package/src/room/track/utils.ts +117 -5
- package/src/room/utils.test.ts +250 -0
- package/src/room/utils.ts +152 -19
- package/src/test/compressionStreamStubs.ts +53 -0
|
@@ -1,15 +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';
|
|
17
|
+
import { EngineEvent } from './events';
|
|
18
|
+
import CriticalTimers from './timers';
|
|
19
|
+
import { Future } from './utils';
|
|
13
20
|
|
|
14
21
|
describe('RTCEngine', () => {
|
|
15
22
|
const originalRTCRtpSender = window.RTCRtpSender;
|
|
@@ -309,7 +316,13 @@ describe('RTCEngine', () => {
|
|
|
309
316
|
|
|
310
317
|
bufferedAmountLowThreshold = 64 * 1024;
|
|
311
318
|
|
|
319
|
+
readyState: RTCDataChannelState = 'open';
|
|
320
|
+
|
|
312
321
|
send = vi.fn();
|
|
322
|
+
|
|
323
|
+
close = vi.fn(() => {
|
|
324
|
+
this.readyState = 'closing';
|
|
325
|
+
});
|
|
313
326
|
}
|
|
314
327
|
|
|
315
328
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
@@ -536,6 +549,37 @@ describe('RTCEngine', () => {
|
|
|
536
549
|
});
|
|
537
550
|
});
|
|
538
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
|
+
|
|
539
583
|
describe('handleDataChannelClose', () => {
|
|
540
584
|
function stubCloseEnv(
|
|
541
585
|
engine: RTCEngine,
|
|
@@ -700,6 +744,123 @@ describe('RTCEngine', () => {
|
|
|
700
744
|
});
|
|
701
745
|
});
|
|
702
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
|
+
|
|
703
864
|
describe('reconnect requested mid-attempt', () => {
|
|
704
865
|
// A full reconnect requested while a resume is already in flight (e.g. a server
|
|
705
866
|
// RECONNECT leave racing the resume) sets `fullReconnectOnNext` mid-attempt. A
|
|
@@ -710,7 +871,7 @@ describe('RTCEngine', () => {
|
|
|
710
871
|
clientConfiguration: unknown;
|
|
711
872
|
pcManager: unknown;
|
|
712
873
|
resumeConnection: (reason?: number) => Promise<void>;
|
|
713
|
-
restartConnection: (regionUrl?: string) => Promise<void>;
|
|
874
|
+
restartConnection: (abortSignal: AbortSignal, regionUrl?: string) => Promise<void>;
|
|
714
875
|
clearPendingReconnect: () => void;
|
|
715
876
|
handleDisconnect: (connection: string, reason?: number) => void;
|
|
716
877
|
attemptReconnect: (reason?: number) => Promise<void>;
|
|
@@ -919,4 +1080,159 @@ describe('RTCEngine', () => {
|
|
|
919
1080
|
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
920
1081
|
});
|
|
921
1082
|
});
|
|
1083
|
+
|
|
1084
|
+
describe('negotiate', () => {
|
|
1085
|
+
/**
|
|
1086
|
+
* An engine whose `pcManager` parks in `negotiate()` until its abort controller fires, so
|
|
1087
|
+
* several negotiations can be held in flight at once. The controllers are captured so tests
|
|
1088
|
+
* can check the abort actually reached each in-flight call.
|
|
1089
|
+
*/
|
|
1090
|
+
function stubNegotiableEngine(engine: RTCEngine) {
|
|
1091
|
+
const controllers: AbortController[] = [];
|
|
1092
|
+
const negotiate = vi.fn((abortController: AbortController) => {
|
|
1093
|
+
controllers.push(abortController);
|
|
1094
|
+
return new Promise<void>((_resolve, reject) => {
|
|
1095
|
+
abortController.signal.addEventListener(
|
|
1096
|
+
'abort',
|
|
1097
|
+
() => reject(new Error('negotiation aborted')),
|
|
1098
|
+
{ once: true },
|
|
1099
|
+
);
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
1103
|
+
_isClosed: false,
|
|
1104
|
+
pcManager: {
|
|
1105
|
+
requirePublisher: vi.fn(),
|
|
1106
|
+
// a non-empty transceiver list keeps negotiate() off the createDataChannels path
|
|
1107
|
+
publisher: { getTransceivers: () => [{}], off: vi.fn(), once: vi.fn() },
|
|
1108
|
+
negotiate,
|
|
1109
|
+
},
|
|
1110
|
+
handleDisconnect: vi.fn(),
|
|
1111
|
+
});
|
|
1112
|
+
return { negotiate, controllers };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const pendingAborts = (engine: RTCEngine) =>
|
|
1116
|
+
(engine as unknown as { pendingNegotiationAborts: Set<() => void> }).pendingNegotiationAborts;
|
|
1117
|
+
|
|
1118
|
+
it('does not add a Closing/Restarting listener per in-flight negotiate call', async () => {
|
|
1119
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
1120
|
+
const { negotiate, controllers } = stubNegotiableEngine(engine);
|
|
1121
|
+
|
|
1122
|
+
// A burst of server-initiated renegotiations used to stack a listener pair per call, which
|
|
1123
|
+
// trips the emitter's max-listener warning at 11.
|
|
1124
|
+
const closingBaseline = engine.listenerCount(EngineEvent.Closing);
|
|
1125
|
+
const restartingBaseline = engine.listenerCount(EngineEvent.Restarting);
|
|
1126
|
+
|
|
1127
|
+
const pending = Array.from({ length: 20 }, () => engine.negotiate());
|
|
1128
|
+
await tick();
|
|
1129
|
+
|
|
1130
|
+
expect(negotiate).toHaveBeenCalledTimes(20);
|
|
1131
|
+
expect(engine.listenerCount(EngineEvent.Closing)).toBe(closingBaseline);
|
|
1132
|
+
expect(engine.listenerCount(EngineEvent.Restarting)).toBe(restartingBaseline);
|
|
1133
|
+
|
|
1134
|
+
// The single central listener still has to reach every one of them.
|
|
1135
|
+
engine.emit(EngineEvent.Closing);
|
|
1136
|
+
await expect(Promise.all(pending)).resolves.toHaveLength(20);
|
|
1137
|
+
expect(controllers).toHaveLength(20);
|
|
1138
|
+
expect(controllers.every((c) => c.signal.aborted)).toBe(true);
|
|
1139
|
+
expect(pendingAborts(engine).size).toBe(0);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('aborts in-flight negotiations on restart without disarming later ones', async () => {
|
|
1143
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
1144
|
+
const { controllers } = stubNegotiableEngine(engine);
|
|
1145
|
+
|
|
1146
|
+
const firstBatch = [engine.negotiate(), engine.negotiate()];
|
|
1147
|
+
await tick();
|
|
1148
|
+
engine.emit(EngineEvent.Restarting);
|
|
1149
|
+
await expect(Promise.all(firstBatch)).resolves.toHaveLength(2);
|
|
1150
|
+
expect(controllers.every((c) => c.signal.aborted)).toBe(true);
|
|
1151
|
+
|
|
1152
|
+
// Restarting fires on every reconnect attempt, so the fan-out point has to re-arm: a
|
|
1153
|
+
// negotiation started after a restart must not be aborted on arrival.
|
|
1154
|
+
const afterRestart = engine.negotiate();
|
|
1155
|
+
await tick();
|
|
1156
|
+
expect(controllers).toHaveLength(3);
|
|
1157
|
+
expect(controllers[2].signal.aborted).toBe(false);
|
|
1158
|
+
expect(pendingAborts(engine).size).toBe(1);
|
|
1159
|
+
|
|
1160
|
+
engine.emit(EngineEvent.Restarting);
|
|
1161
|
+
await expect(afterRestart).resolves.toBeUndefined();
|
|
1162
|
+
expect(controllers[2].signal.aborted).toBe(true);
|
|
1163
|
+
expect(pendingAborts(engine).size).toBe(0);
|
|
1164
|
+
});
|
|
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
|
+
});
|
|
922
1238
|
});
|
package/src/room/RTCEngine.ts
CHANGED
|
@@ -227,6 +227,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
227
227
|
|
|
228
228
|
private attemptingReconnect: boolean = false;
|
|
229
229
|
|
|
230
|
+
private reconnectAbortController?: AbortController;
|
|
231
|
+
|
|
230
232
|
private reconnectPolicy: ReconnectPolicy;
|
|
231
233
|
|
|
232
234
|
private reconnectTimeout?: ReturnType<typeof setTimeout>;
|
|
@@ -236,6 +238,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
236
238
|
/** keeps track of how often an initial join connection has been tried */
|
|
237
239
|
private joinAttempts: number = 0;
|
|
238
240
|
|
|
241
|
+
/**
|
|
242
|
+
* When the current join attempt began (`performance.now()`), taken at the top of `join` before
|
|
243
|
+
* the signal connection or the peer connections exist. Every attempt (a join retry, or the join
|
|
244
|
+
* inside a full restart) times itself from scratch. Consumed when the primary transport first
|
|
245
|
+
* connects, to hand the publisher its connection setup time; resumes never set it.
|
|
246
|
+
*/
|
|
247
|
+
private joinStartedAt?: number;
|
|
248
|
+
|
|
239
249
|
/** specifies how often an initial join connection is allowed to retry */
|
|
240
250
|
private maxJoinAttempts: number = 1;
|
|
241
251
|
|
|
@@ -268,6 +278,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
268
278
|
/** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
|
|
269
279
|
private transportConnectingSince?: number;
|
|
270
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Abort handlers for the in-flight `negotiate()` calls, which a single central
|
|
283
|
+
* Closing/Restarting listener pair fans out to. Registering a listener pair per call instead
|
|
284
|
+
* accumulates them on the engine emitter and trips its max-listener warning under
|
|
285
|
+
* renegotiation bursts.
|
|
286
|
+
*/
|
|
287
|
+
private pendingNegotiationAborts = new Set<() => void>();
|
|
288
|
+
|
|
271
289
|
constructor(private options: InternalRoomOptions) {
|
|
272
290
|
super();
|
|
273
291
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
|
|
@@ -308,6 +326,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
308
326
|
this.client.onParticipantUpdate = (updates) =>
|
|
309
327
|
this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
310
328
|
this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
|
|
329
|
+
|
|
330
|
+
const abortPendingNegotiations = () => {
|
|
331
|
+
// Iterate a copy: each handler removes itself from the set as its negotiation settles.
|
|
332
|
+
for (const abort of Array.from(this.pendingNegotiationAborts)) {
|
|
333
|
+
abort();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
this.on(EngineEvent.Closing, abortPendingNegotiations);
|
|
337
|
+
this.on(EngineEvent.Restarting, abortPendingNegotiations);
|
|
311
338
|
}
|
|
312
339
|
|
|
313
340
|
/** @internal */
|
|
@@ -335,6 +362,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
335
362
|
this.maxJoinAttempts = opts.maxRetries;
|
|
336
363
|
try {
|
|
337
364
|
this.joinAttempts += 1;
|
|
365
|
+
this.joinStartedAt = performance.now();
|
|
338
366
|
|
|
339
367
|
this.setupSignalClientCallbacks();
|
|
340
368
|
// Whether the initial publisher offer is bundled with the join request. Computed once and
|
|
@@ -375,6 +403,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
375
403
|
useV0Path,
|
|
376
404
|
offerProto,
|
|
377
405
|
);
|
|
406
|
+
if (abortSignal?.aborted) {
|
|
407
|
+
this.log.info('join cancelled after the signal join response');
|
|
408
|
+
throw ConnectionError.cancelled('Connection aborted');
|
|
409
|
+
}
|
|
378
410
|
this._isClosed = false;
|
|
379
411
|
this.latestJoinResponse = joinResponse;
|
|
380
412
|
this.participantSid = joinResponse.participant?.sid;
|
|
@@ -388,6 +420,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
388
420
|
// the server's ICE servers and topology, then negotiate separately rather than bundling
|
|
389
421
|
// the offer with the join.
|
|
390
422
|
await this.configure(joinResponse, !useV0Path);
|
|
423
|
+
if (abortSignal?.aborted) {
|
|
424
|
+
this.log.info('join cancelled after configuring peer connections');
|
|
425
|
+
throw ConnectionError.cancelled('Connection aborted');
|
|
426
|
+
}
|
|
391
427
|
if (!useV0Path) {
|
|
392
428
|
// The V1 first offer must carry the media layout so Firefox binds receive decoders for
|
|
393
429
|
// subscribed tracks — without it, subscribed audio/video arrive as RTP but
|
|
@@ -403,6 +439,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
403
439
|
}
|
|
404
440
|
}
|
|
405
441
|
|
|
442
|
+
if (this.joinStartedAt !== undefined) {
|
|
443
|
+
// Lets a video published before the transports connect be seeded from the time elapsed
|
|
444
|
+
// so far, rather than delaying its offer until the setup time is measured.
|
|
445
|
+
this.pcManager?.publisher.setConnectionStartedAt(this.joinStartedAt);
|
|
446
|
+
}
|
|
447
|
+
|
|
406
448
|
this.registerOnLineListener();
|
|
407
449
|
this.clientConfiguration = joinResponse.clientConfiguration;
|
|
408
450
|
this.emit(EngineEvent.SignalConnected, joinResponse);
|
|
@@ -446,6 +488,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
446
488
|
* having given up on reconnecting.
|
|
447
489
|
*/
|
|
448
490
|
async close(reason?: string) {
|
|
491
|
+
this.reconnectAbortController?.abort();
|
|
492
|
+
this.reconnectAbortController = undefined;
|
|
449
493
|
const unlock = await this.closingLock.lock();
|
|
450
494
|
if (this.isClosed) {
|
|
451
495
|
unlock();
|
|
@@ -454,6 +498,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
454
498
|
try {
|
|
455
499
|
this._isClosed = true;
|
|
456
500
|
this.joinAttempts = 0;
|
|
501
|
+
this.joinStartedAt = undefined;
|
|
457
502
|
this.emit(EngineEvent.Closing);
|
|
458
503
|
this.removeAllListeners();
|
|
459
504
|
this.deregisterOnLineListener();
|
|
@@ -612,6 +657,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
612
657
|
this.publisherConnectionPromise = undefined;
|
|
613
658
|
}
|
|
614
659
|
if (connectionState === PCTransportState.CONNECTED) {
|
|
660
|
+
if (this.joinStartedAt !== undefined) {
|
|
661
|
+
// Connection setup time is the only network signal there is before the first video
|
|
662
|
+
// offer. Set synchronously, so it is in place before `room.connect()` resolves and the
|
|
663
|
+
// app can publish. A full restart joins again and builds new transports, so it is
|
|
664
|
+
// measured again; a resume keeps its estimator and never restarts the timer.
|
|
665
|
+
const setupTimeMs = Math.round(performance.now() - this.joinStartedAt);
|
|
666
|
+
this.joinStartedAt = undefined;
|
|
667
|
+
this.log.info(`connection setup took ${setupTimeMs} ms`);
|
|
668
|
+
this.pcManager?.publisher.setConnectionSetupTime(setupTimeMs);
|
|
669
|
+
}
|
|
615
670
|
const shouldEmit = this.pcState === PCState.New;
|
|
616
671
|
this.pcState = PCState.Connected;
|
|
617
672
|
if (shouldEmit) {
|
|
@@ -669,6 +724,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
669
724
|
this.midToTrackId = midToTrackId;
|
|
670
725
|
}
|
|
671
726
|
await this.pcManager.setPublisherAnswer(sd, offerId);
|
|
727
|
+
this.emit(EngineEvent.RequestSubscribedCodecRefresh);
|
|
672
728
|
};
|
|
673
729
|
|
|
674
730
|
// add candidate on trickle
|
|
@@ -738,7 +794,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
738
794
|
|
|
739
795
|
this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
|
|
740
796
|
this.addMediaSections(requirement.numAudios, requirement.numVideos);
|
|
741
|
-
this.negotiate()
|
|
797
|
+
this.negotiate().catch((err) => {
|
|
798
|
+
this.log.error(err);
|
|
799
|
+
});
|
|
742
800
|
};
|
|
743
801
|
|
|
744
802
|
this.client.onPublishDataTrackResponse = (event: PublishDataTrackResponse) => {
|
|
@@ -879,6 +937,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
879
937
|
return;
|
|
880
938
|
}
|
|
881
939
|
|
|
940
|
+
if (this.dataChannels.hasPublisherChannels) {
|
|
941
|
+
// Replacing live channels (the Safari null-id path in resumeConnection): the new ones start
|
|
942
|
+
// out connecting, so drop the memoized readiness check and make the next sender wait for
|
|
943
|
+
// them to open.
|
|
944
|
+
this.publisherConnectionPromise = undefined;
|
|
945
|
+
}
|
|
882
946
|
this.dataChannels.createPublisherChannels(this.pcManager);
|
|
883
947
|
}
|
|
884
948
|
|
|
@@ -1201,7 +1265,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1201
1265
|
return;
|
|
1202
1266
|
}
|
|
1203
1267
|
if (connection === leaveReconnect) {
|
|
1204
|
-
|
|
1268
|
+
// The server asked for a reconnect, so skip any remaining backoff. Use the policy's first
|
|
1269
|
+
// retry delay instead of 0 so that clients told to reconnect together don't all reconnect
|
|
1270
|
+
// at the same instant.
|
|
1271
|
+
delay = this.getNextRetryDelay({ elapsedMs: duration, retryCount: 0 });
|
|
1272
|
+
if (delay === null) {
|
|
1273
|
+
disconnect(duration);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1205
1276
|
}
|
|
1206
1277
|
|
|
1207
1278
|
this.log.debug(`reconnecting in ${delay}ms`);
|
|
@@ -1318,16 +1389,26 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1318
1389
|
this.fullReconnectOnNext = false;
|
|
1319
1390
|
|
|
1320
1391
|
let succeeded = false;
|
|
1392
|
+
const abortController = new AbortController();
|
|
1393
|
+
this.reconnectAbortController = abortController;
|
|
1321
1394
|
try {
|
|
1322
1395
|
this.attemptingReconnect = true;
|
|
1323
1396
|
if (fullReconnect) {
|
|
1324
|
-
await this.restartConnection();
|
|
1397
|
+
await this.restartConnection(abortController.signal);
|
|
1325
1398
|
} else {
|
|
1326
1399
|
await this.resumeConnection(reason);
|
|
1327
1400
|
}
|
|
1401
|
+
if (abortController.signal.aborted) {
|
|
1402
|
+
this.log.info('reconnect attempt cancelled, not marking it as succeeded');
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1328
1405
|
this.clearPendingReconnect();
|
|
1329
1406
|
succeeded = true;
|
|
1330
1407
|
} catch (e) {
|
|
1408
|
+
if (abortController.signal.aborted) {
|
|
1409
|
+
this.log.info('reconnect attempt cancelled, not retrying', { error: e });
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1331
1412
|
this.reconnectAttempts += 1;
|
|
1332
1413
|
let recoverable = true;
|
|
1333
1414
|
if (e instanceof UnexpectedConnectionState) {
|
|
@@ -1356,6 +1437,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1356
1437
|
);
|
|
1357
1438
|
}
|
|
1358
1439
|
} finally {
|
|
1440
|
+
// close() may already have cleared this (or a later attempt replaced it); only release our own
|
|
1441
|
+
if (this.reconnectAbortController === abortController) {
|
|
1442
|
+
this.reconnectAbortController = undefined;
|
|
1443
|
+
}
|
|
1359
1444
|
this.attemptingReconnect = false;
|
|
1360
1445
|
|
|
1361
1446
|
// A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
|
|
@@ -1379,8 +1464,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1379
1464
|
return null;
|
|
1380
1465
|
}
|
|
1381
1466
|
|
|
1382
|
-
private async restartConnection(regionUrl?: string) {
|
|
1467
|
+
private async restartConnection(abortSignal: AbortSignal, regionUrl?: string) {
|
|
1383
1468
|
try {
|
|
1469
|
+
if (abortSignal.aborted) {
|
|
1470
|
+
this.log.info('full reconnect cancelled before starting', { regionUrl });
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1384
1473
|
if (!this.url || !this.token) {
|
|
1385
1474
|
// permanent failure, don't attempt reconnection
|
|
1386
1475
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
|
@@ -1394,6 +1483,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1394
1483
|
}
|
|
1395
1484
|
await this.cleanupPeerConnections();
|
|
1396
1485
|
await this.cleanupClient();
|
|
1486
|
+
if (abortSignal.aborted) {
|
|
1487
|
+
this.log.info('full reconnect cancelled after cleaning up the previous session', {
|
|
1488
|
+
regionUrl,
|
|
1489
|
+
});
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1397
1492
|
|
|
1398
1493
|
let joinResponse: JoinResponse;
|
|
1399
1494
|
try {
|
|
@@ -1407,7 +1502,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1407
1502
|
regionUrl ?? this.url,
|
|
1408
1503
|
this.token,
|
|
1409
1504
|
this.signalOpts,
|
|
1410
|
-
|
|
1505
|
+
abortSignal,
|
|
1411
1506
|
!this.options.singlePeerConnection,
|
|
1412
1507
|
)
|
|
1413
1508
|
).joinResponse;
|
|
@@ -1417,6 +1512,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1417
1512
|
}
|
|
1418
1513
|
throw new SignalReconnectError();
|
|
1419
1514
|
}
|
|
1515
|
+
if (abortSignal.aborted) {
|
|
1516
|
+
this.log.info('full reconnect cancelled after the signal join response', { regionUrl });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1420
1519
|
|
|
1421
1520
|
if (this.shouldFailNext) {
|
|
1422
1521
|
this.shouldFailNext = false;
|
|
@@ -1427,6 +1526,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1427
1526
|
this.emit(EngineEvent.SignalRestarted, joinResponse);
|
|
1428
1527
|
|
|
1429
1528
|
await this.waitForPCReconnected();
|
|
1529
|
+
if (abortSignal.aborted) {
|
|
1530
|
+
this.log.info('full reconnect cancelled after peer connections reconnected', { regionUrl });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1430
1533
|
|
|
1431
1534
|
// re-check signal connection state before setting engine as resumed
|
|
1432
1535
|
if (this.client.currentState !== SignalConnectionState.CONNECTED) {
|
|
@@ -1437,9 +1540,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1437
1540
|
// reconnect success
|
|
1438
1541
|
this.emit(EngineEvent.Restarted);
|
|
1439
1542
|
} catch (error) {
|
|
1440
|
-
|
|
1543
|
+
if (abortSignal.aborted) {
|
|
1544
|
+
this.log.info('full reconnect cancelled after a failed attempt', { regionUrl });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const nextRegionUrl = await this.regionStrategy?.getNextUrl(abortSignal);
|
|
1548
|
+
if (abortSignal.aborted) {
|
|
1549
|
+
this.log.info('full reconnect cancelled during region selection', { regionUrl });
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1441
1552
|
if (nextRegionUrl) {
|
|
1442
|
-
await this.restartConnection(nextRegionUrl);
|
|
1553
|
+
await this.restartConnection(abortSignal, nextRegionUrl);
|
|
1443
1554
|
return;
|
|
1444
1555
|
} else {
|
|
1445
1556
|
// no more regions to try (or we're not on cloud)
|
|
@@ -1673,7 +1784,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1673
1784
|
* {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
|
|
1674
1785
|
*/
|
|
1675
1786
|
async waitForBufferHeadroom(kind: DataChannelKind) {
|
|
1676
|
-
|
|
1787
|
+
const dc = this.dataChannelForKind(kind);
|
|
1788
|
+
if (!dc) {
|
|
1789
|
+
throw new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`);
|
|
1790
|
+
}
|
|
1791
|
+
return this.flowControlFor(kind).waitForHeadroomWithLock(dc);
|
|
1677
1792
|
}
|
|
1678
1793
|
|
|
1679
1794
|
/**
|
|
@@ -1807,8 +1922,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1807
1922
|
if (this.isClosed) {
|
|
1808
1923
|
reject(new NegotiationError('cannot negotiate on closed engine'));
|
|
1809
1924
|
}
|
|
1810
|
-
this.
|
|
1811
|
-
this.on(EngineEvent.Restarting, handleClosed);
|
|
1925
|
+
this.pendingNegotiationAborts.add(handleClosed);
|
|
1812
1926
|
this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
|
|
1813
1927
|
this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
|
|
1814
1928
|
|
|
@@ -1832,8 +1946,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1832
1946
|
reject(new Error(String(e)));
|
|
1833
1947
|
}
|
|
1834
1948
|
} finally {
|
|
1835
|
-
this.
|
|
1836
|
-
this.off(EngineEvent.Restarting, handleClosed);
|
|
1949
|
+
this.pendingNegotiationAborts.delete(handleClosed);
|
|
1837
1950
|
}
|
|
1838
1951
|
});
|
|
1839
1952
|
}
|
|
@@ -2095,6 +2208,7 @@ export type EngineEventCallbacks = {
|
|
|
2095
2208
|
joined: (joinResponse: JoinResponse) => void;
|
|
2096
2209
|
tokenRefreshed: (token: string) => void;
|
|
2097
2210
|
serverRegionsReported: (regions: RegionSettings) => void;
|
|
2211
|
+
requestSubscribedCodecRefresh: () => void;
|
|
2098
2212
|
};
|
|
2099
2213
|
|
|
2100
2214
|
export interface RegionStrategy {
|