livekit-client 2.22.2 → 2.22.4
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 +488 -438
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +954 -301
- 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 +10 -2
- 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.map +1 -1
- package/dist/src/api/utils.d.ts +1 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +26 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +15 -1
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
- package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/logger.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/PCTransport.d.ts +78 -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 +9 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.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.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +16 -0
- 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/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/options.d.ts +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 +66 -0
- 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 +33 -33
- package/dist/ts4.2/api/utils.d.ts +9 -8
- 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 -85
- 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 -217
- 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 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -226
- 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 +28 -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 -62
- 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 +246 -175
- package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
- package/dist/ts4.2/room/RTCEngine.d.ts +269 -261
- 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 +354 -365
- package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -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 +287 -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 -37
- 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 +39 -42
- package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
- package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -93
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -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 +31 -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 +246 -180
- 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 +160 -10
- package/src/api/SignalClient.ts +75 -54
- package/src/api/WebSocketStream.ts +3 -8
- package/src/api/utils.ts +10 -0
- package/src/e2ee/E2eeManager.test.ts +196 -0
- package/src/e2ee/E2eeManager.ts +114 -19
- package/src/e2ee/types.ts +19 -1
- package/src/e2ee/worker/DataCryptor.ts +2 -1
- package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
- package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
- package/src/e2ee/worker/FrameCryptor.ts +20 -70
- package/src/e2ee/worker/e2ee.worker.ts +34 -11
- package/src/logger.ts +22 -0
- package/src/options.ts +16 -0
- package/src/room/PCTransport.test.ts +183 -7
- package/src/room/PCTransport.ts +197 -36
- package/src/room/PCTransportManager.ts +3 -1
- package/src/room/RTCEngine.test.ts +84 -0
- package/src/room/RTCEngine.ts +52 -12
- package/src/room/Room.test.ts +32 -6
- package/src/room/Room.ts +27 -7
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -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.ts +30 -14
- package/src/room/participant/publishUtils.test.ts +133 -0
- package/src/room/participant/publishUtils.ts +54 -38
- package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
- package/src/room/rpc/client/RpcClientManager.ts +32 -15
- package/src/room/track/LocalTrack.ts +18 -7
- package/src/room/track/LocalVideoTrack.ts +25 -8
- package/src/room/track/RemoteAudioTrack.ts +3 -3
- package/src/room/track/RemoteVideoTrack.test.ts +210 -0
- package/src/room/track/RemoteVideoTrack.ts +29 -3
- package/src/room/track/options.ts +1 -1
- package/src/room/track/utils.test.ts +201 -2
- package/src/room/track/utils.ts +88 -0
- package/src/room/utils.test.ts +211 -0
- package/src/room/utils.ts +129 -0
- package/src/test/compressionStreamStubs.ts +53 -0
package/src/room/PCTransport.ts
CHANGED
|
@@ -34,51 +34,130 @@ const startBitrateMultiplier = 0.9;
|
|
|
34
34
|
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
35
35
|
const maxStartBitrateKbps = 1000;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Minimum target bitrate in kbps for the start bitrate hint. Below this, seeding above the
|
|
39
|
+
* real capacity costs more than the ramp it saves, so libwebrtc's default is left in place.
|
|
40
|
+
*/
|
|
41
|
+
const minTargetBitrateKbps = 300;
|
|
42
|
+
|
|
37
43
|
const debounceInterval = 20;
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
|
-
*
|
|
41
|
-
* This SDP munging is used for a bitrate setting that cannot be applied through
|
|
42
|
-
* `RTCRtpEncodingParameters`.
|
|
46
|
+
* Codec payload for `codec` in this media section, when the section carries `cid`.
|
|
43
47
|
*
|
|
44
|
-
* Returns `undefined` when the section does not belong to the track, `0` when
|
|
45
|
-
*
|
|
46
|
-
* requested codec is present (whether the bitrate was added or already set).
|
|
48
|
+
* Returns `undefined` when the section does not belong to the track, `0` when it does but
|
|
49
|
+
* does not offer the requested codec, and the codec payload otherwise.
|
|
47
50
|
*
|
|
48
51
|
* @internal
|
|
49
52
|
*/
|
|
50
|
-
export function
|
|
53
|
+
export function findTrackCodecPayload(
|
|
51
54
|
media: MediaDescription,
|
|
52
55
|
cid: string,
|
|
53
56
|
codec: string,
|
|
54
|
-
maxbr: number,
|
|
55
|
-
isScreenShare = false,
|
|
56
57
|
): number | undefined {
|
|
57
58
|
if (!media.msid?.includes(cid)) {
|
|
58
59
|
return undefined;
|
|
59
60
|
}
|
|
61
|
+
return media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
|
|
62
|
+
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Start bitrate hinted for a single track, or `undefined` when its target is too low to
|
|
66
|
+
* be worth seeding.
|
|
67
|
+
*
|
|
68
|
+
* 90% of the target leaves ~10% headroom for the estimator to settle. The same multiplier
|
|
69
|
+
* is used for every codec because the target already reflects the codec's efficiency.
|
|
70
|
+
* Camera is capped at 1 Mbps so the estimator does not open too aggressively on a
|
|
71
|
+
* high-bitrate track; screen share is exempt, because its content needs the bitrate
|
|
72
|
+
* immediately to stay legible.
|
|
73
|
+
*
|
|
74
|
+
* TODO: adjust dynamically from network conditions (e.g. a previous BWE estimate) rather
|
|
75
|
+
* than a fixed cap.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
export function computeTrackStartBitrate(trackbr: TrackBitrateInfo): number | undefined {
|
|
80
|
+
if (trackbr.maxbr < minTargetBitrateKbps) {
|
|
81
|
+
return undefined;
|
|
65
82
|
}
|
|
83
|
+
const calculated = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
84
|
+
return trackbr.isScreenShare ? calculated : Math.min(calculated, maxStartBitrateKbps);
|
|
85
|
+
}
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The single start bitrate for this peer connection: the largest hint among the video
|
|
89
|
+
* m-sections of `media` that map to a published track.
|
|
90
|
+
*
|
|
91
|
+
* libwebrtc reads `x-google-start-bitrate` per m-section but applies it to the shared
|
|
92
|
+
* `Call` (`WebRtcVideoSendChannel::ApplyChangedParams` -> `SetSdpBitrateParameters`), where
|
|
93
|
+
* `RtpBitrateConfigurator` holds one config for the whole connection. Differing per-section
|
|
94
|
+
* values are therefore last-writer-wins, decided by m-section order, so every video section
|
|
95
|
+
* gets the same number instead.
|
|
96
|
+
*
|
|
97
|
+
* Only sections that can send local media are considered. `trackBitrates` is append-only and an
|
|
98
|
+
* unpublished section keeps its `a=msid`, so matching on msid alone would still pair a stale
|
|
99
|
+
* entry with its old section — letting an uncapped screen-share target seed a connection that
|
|
100
|
+
* now carries only a camera, or consuming the one-shot hint on a section that sends nothing.
|
|
101
|
+
*
|
|
102
|
+
* The direction is the discriminator, as an exclusion rather than a match: `recvonly` and
|
|
103
|
+
* `inactive` are the only directions that cannot carry local media, and they are exactly the
|
|
104
|
+
* two a dead section lands on — unpublish sets `inactive` (explicitly in
|
|
105
|
+
* `LocalParticipant.unpublishTrack`, and via `removeTrack`'s sendonly -> inactive transition for
|
|
106
|
+
* the simulcast senders), `removeTrack` on a `sendrecv` transceiver leaves `recvonly`, and the
|
|
107
|
+
* pre-populated placeholders are `recvonly`. Everything else sends: `sendonly` from the
|
|
108
|
+
* `addTransceiver` path, `sendrecv` from the legacy `addTrack` fallback (which reuses a
|
|
109
|
+
* transceiver rather than creating a sendonly one), and a section with no direction attribute,
|
|
110
|
+
* which SDP defaults to `sendrecv`.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function computeConnectionStartBitrate(
|
|
115
|
+
media: MediaDescription[],
|
|
116
|
+
trackBitrates: TrackBitrateInfo[],
|
|
117
|
+
): number | undefined {
|
|
118
|
+
let connectionStartBitrate: number | undefined;
|
|
119
|
+
for (const m of media) {
|
|
120
|
+
if (m.type !== 'video' || m.direction === 'recvonly' || m.direction === 'inactive') {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const trackbr of trackBitrates) {
|
|
124
|
+
if (!trackbr.cid) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const codecPayload = findTrackCodecPayload(m, trackbr.cid, trackbr.codec);
|
|
128
|
+
if (codecPayload === undefined) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const startBitrate = codecPayload > 0 ? computeTrackStartBitrate(trackbr) : undefined;
|
|
132
|
+
if (
|
|
133
|
+
startBitrate !== undefined &&
|
|
134
|
+
(connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)
|
|
135
|
+
) {
|
|
136
|
+
connectionStartBitrate = startBitrate;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return connectionStartBitrate;
|
|
142
|
+
}
|
|
75
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Declares `x-google-start-bitrate` on `codecPayload`'s fmtp. This SDP munging is used for
|
|
146
|
+
* a bitrate setting that cannot be applied through `RTCRtpEncodingParameters`.
|
|
147
|
+
*
|
|
148
|
+
* Returns whether the section now carries the hint.
|
|
149
|
+
*
|
|
150
|
+
* @internal
|
|
151
|
+
*/
|
|
152
|
+
export function applyVideoStartBitrate(
|
|
153
|
+
media: MediaDescription,
|
|
154
|
+
codecPayload: number,
|
|
155
|
+
startBitrate: number,
|
|
156
|
+
): boolean {
|
|
76
157
|
const fmtp = media.fmtp.find((entry) => entry.payload === codecPayload);
|
|
77
158
|
if (fmtp) {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// This forces every track sharing that payload to use the initial track's
|
|
81
|
-
// start bitrate.
|
|
159
|
+
// A payload type is shared across the bundle, so a value written for one section is
|
|
160
|
+
// already the connection-level one; leave it rather than rewrite it.
|
|
82
161
|
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
83
162
|
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
|
|
84
163
|
}
|
|
@@ -90,7 +169,7 @@ export function applyVideoStartBitrate(
|
|
|
90
169
|
});
|
|
91
170
|
}
|
|
92
171
|
|
|
93
|
-
return
|
|
172
|
+
return true;
|
|
94
173
|
}
|
|
95
174
|
|
|
96
175
|
export const PCEvents = {
|
|
@@ -127,6 +206,11 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
127
206
|
|
|
128
207
|
latestOfferId: number = 0;
|
|
129
208
|
|
|
209
|
+
private answeredOfferId: number = 0;
|
|
210
|
+
|
|
211
|
+
/** offerId (0 if unnumbered) of the answer being applied right now */
|
|
212
|
+
private answerInFlight: number | undefined;
|
|
213
|
+
|
|
130
214
|
latestAcknowledgedOfferId: number = 0;
|
|
131
215
|
|
|
132
216
|
private offerLock: Mutex;
|
|
@@ -141,6 +225,15 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
141
225
|
|
|
142
226
|
trackBitrates: TrackBitrateInfo[] = [];
|
|
143
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Whether an offer carrying the connection-level `x-google-start-bitrate` has been
|
|
230
|
+
* accepted locally. The hint is written once per peer connection: libwebrtc retains
|
|
231
|
+
* `start_bitrate_bps` in `RtpBitrateConfigurator` and re-applies it on network route
|
|
232
|
+
* changes, so a later rewrite is at best a no-op and at worst restarts a converged
|
|
233
|
+
* bandwidth estimator. A new peer connection (full reconnect) seeds a new estimator.
|
|
234
|
+
*/
|
|
235
|
+
private hasAppliedVideoStartBitrate = false;
|
|
236
|
+
|
|
144
237
|
remoteStereoMids: string[] = [];
|
|
145
238
|
|
|
146
239
|
remoteNackMids: string[] = [];
|
|
@@ -250,6 +343,36 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
250
343
|
});
|
|
251
344
|
return false;
|
|
252
345
|
}
|
|
346
|
+
if (sd.type === 'answer') {
|
|
347
|
+
if (
|
|
348
|
+
isDuplicateAnswer(
|
|
349
|
+
offerId,
|
|
350
|
+
this.answerInFlight,
|
|
351
|
+
this.answeredOfferId,
|
|
352
|
+
this._pc?.signalingState,
|
|
353
|
+
this.pendingInitialOffer !== undefined,
|
|
354
|
+
)
|
|
355
|
+
) {
|
|
356
|
+
// a repeat of an answer already applied or being applied (e.g.
|
|
357
|
+
// duplicated by a proxy): there is no local offer left to answer
|
|
358
|
+
this.log.warn('ignoring duplicate answer', { offerId });
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
// claimed before any await, so a copy arriving mid-apply is caught too;
|
|
362
|
+
// released however the apply ends, so a genuine retry is still possible
|
|
363
|
+
this.answerInFlight = offerId;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
return await this.applyRemoteDescription(sd, offerId);
|
|
367
|
+
} finally {
|
|
368
|
+
if (sd.type === 'answer') this.answerInFlight = undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async applyRemoteDescription(
|
|
373
|
+
sd: RTCSessionDescriptionInit,
|
|
374
|
+
offerId: number,
|
|
375
|
+
): Promise<boolean> {
|
|
253
376
|
let mungedSDP: string | undefined = undefined;
|
|
254
377
|
if (sd.type === 'offer') {
|
|
255
378
|
let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
|
|
@@ -331,6 +454,8 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
331
454
|
mungedSDP = write(sdpParsed);
|
|
332
455
|
}
|
|
333
456
|
await this.setMungedSDP(sd, mungedSDP, true);
|
|
457
|
+
// only a successfully applied answer makes later copies duplicates
|
|
458
|
+
if (sd.type === 'answer' && offerId > 0) this.answeredOfferId = offerId;
|
|
334
459
|
|
|
335
460
|
if (this.pendingCandidates.length > 0) {
|
|
336
461
|
this.iceLog.debug('flushing queued ICE candidates', {
|
|
@@ -453,6 +578,14 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
453
578
|
this.log.debug('original offer', { sdp: offer.sdp });
|
|
454
579
|
|
|
455
580
|
const sdpParsed = parse(offer.sdp ?? '');
|
|
581
|
+
// One value for every video m-section, written only on the first offer that carries
|
|
582
|
+
// local video: the hint is connection-level in libwebrtc, so differing per-section
|
|
583
|
+
// values would be last-writer-wins on m-section order. Offers before any video is
|
|
584
|
+
// published (data channel or audio only) find no target and leave the latch unset.
|
|
585
|
+
const connectionStartBitrate = this.hasAppliedVideoStartBitrate
|
|
586
|
+
? undefined
|
|
587
|
+
: computeConnectionStartBitrate(sdpParsed.media, this.trackBitrates);
|
|
588
|
+
let appliedVideoStartBitrate = false;
|
|
456
589
|
sdpParsed.media.forEach((media) => {
|
|
457
590
|
ensureIPAddrMatchVersion(media);
|
|
458
591
|
if (media.type === 'audio') {
|
|
@@ -463,19 +596,21 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
463
596
|
return false;
|
|
464
597
|
}
|
|
465
598
|
|
|
466
|
-
const codecPayload =
|
|
467
|
-
media,
|
|
468
|
-
trackbr.cid,
|
|
469
|
-
trackbr.codec,
|
|
470
|
-
trackbr.maxbr,
|
|
471
|
-
trackbr.isScreenShare,
|
|
472
|
-
);
|
|
599
|
+
const codecPayload = findTrackCodecPayload(media, trackbr.cid, trackbr.codec);
|
|
473
600
|
if (codecPayload === undefined) {
|
|
474
601
|
return false;
|
|
475
602
|
}
|
|
476
603
|
|
|
477
|
-
if (codecPayload > 0
|
|
478
|
-
|
|
604
|
+
if (codecPayload > 0) {
|
|
605
|
+
if (connectionStartBitrate !== undefined) {
|
|
606
|
+
appliedVideoStartBitrate =
|
|
607
|
+
applyVideoStartBitrate(media, codecPayload, connectionStartBitrate) ||
|
|
608
|
+
appliedVideoStartBitrate;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
612
|
+
this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
|
|
613
|
+
}
|
|
479
614
|
}
|
|
480
615
|
|
|
481
616
|
return true;
|
|
@@ -500,7 +635,13 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
500
635
|
});
|
|
501
636
|
return;
|
|
502
637
|
}
|
|
503
|
-
|
|
638
|
+
const mungedSdp = write(sdpParsed);
|
|
639
|
+
await this.setMungedSDP(offer, mungedSdp);
|
|
640
|
+
// setMungedSDP falls back to the unmunged SDP on rejection. Only consume the
|
|
641
|
+
// one-shot hint once the SDP carrying it has been accepted locally.
|
|
642
|
+
if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
|
|
643
|
+
this.hasAppliedVideoStartBitrate = true;
|
|
644
|
+
}
|
|
504
645
|
this.onOffer(offer, this.latestOfferId);
|
|
505
646
|
} finally {
|
|
506
647
|
unlock();
|
|
@@ -952,6 +1093,26 @@ export function conformBundledCodecFmtp(
|
|
|
952
1093
|
}
|
|
953
1094
|
|
|
954
1095
|
/** @internal */
|
|
1096
|
+
/**
|
|
1097
|
+
* Whether an incoming answer repeats one already applied or being applied.
|
|
1098
|
+
* Any answer for the offer whose answer is in flight is a duplicate, numbered
|
|
1099
|
+
* or not. With an offerId (current servers) an answer is also a duplicate if
|
|
1100
|
+
* that offer's answer was already applied. Without one, fall back to the
|
|
1101
|
+
* signaling state: in `stable` no local offer is pending, unless the initial
|
|
1102
|
+
* offer is still deferred (it is set locally only once its answer arrives).
|
|
1103
|
+
*/
|
|
1104
|
+
export function isDuplicateAnswer(
|
|
1105
|
+
offerId: number,
|
|
1106
|
+
answerInFlight: number | undefined,
|
|
1107
|
+
answeredOfferId: number,
|
|
1108
|
+
signalingState: RTCSignalingState | undefined,
|
|
1109
|
+
hasPendingInitialOffer: boolean,
|
|
1110
|
+
): boolean {
|
|
1111
|
+
if (answerInFlight === offerId) return true;
|
|
1112
|
+
if (offerId > 0) return offerId === answeredOfferId;
|
|
1113
|
+
return signalingState === 'stable' && !hasPendingInitialOffer;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
955
1116
|
export function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
956
1117
|
stereoMids: string[];
|
|
957
1118
|
nackMids: string[];
|
|
@@ -229,7 +229,9 @@ export class PCTransportManager {
|
|
|
229
229
|
this.publisher.getConnectionState() !== 'connecting'
|
|
230
230
|
) {
|
|
231
231
|
this.log.debug('negotiation required, start negotiating');
|
|
232
|
-
this.publisher.negotiate()
|
|
232
|
+
this.publisher.negotiate().catch((err) => {
|
|
233
|
+
this.log.error(err);
|
|
234
|
+
});
|
|
233
235
|
}
|
|
234
236
|
await Promise.all(
|
|
235
237
|
this.requiredTransports?.map((transport) =>
|
|
@@ -10,6 +10,7 @@ import { PCTransportState } from './PCTransportManager';
|
|
|
10
10
|
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
11
11
|
import { roomOptionDefaults } from './defaults';
|
|
12
12
|
import { PublishDataError, UnexpectedConnectionState } from './errors';
|
|
13
|
+
import { EngineEvent } from './events';
|
|
13
14
|
|
|
14
15
|
describe('RTCEngine', () => {
|
|
15
16
|
const originalRTCRtpSender = window.RTCRtpSender;
|
|
@@ -919,4 +920,87 @@ describe('RTCEngine', () => {
|
|
|
919
920
|
expect(handleDisconnect).not.toHaveBeenCalled();
|
|
920
921
|
});
|
|
921
922
|
});
|
|
923
|
+
|
|
924
|
+
describe('negotiate', () => {
|
|
925
|
+
/**
|
|
926
|
+
* An engine whose `pcManager` parks in `negotiate()` until its abort controller fires, so
|
|
927
|
+
* several negotiations can be held in flight at once. The controllers are captured so tests
|
|
928
|
+
* can check the abort actually reached each in-flight call.
|
|
929
|
+
*/
|
|
930
|
+
function stubNegotiableEngine(engine: RTCEngine) {
|
|
931
|
+
const controllers: AbortController[] = [];
|
|
932
|
+
const negotiate = vi.fn((abortController: AbortController) => {
|
|
933
|
+
controllers.push(abortController);
|
|
934
|
+
return new Promise<void>((_resolve, reject) => {
|
|
935
|
+
abortController.signal.addEventListener(
|
|
936
|
+
'abort',
|
|
937
|
+
() => reject(new Error('negotiation aborted')),
|
|
938
|
+
{ once: true },
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
943
|
+
_isClosed: false,
|
|
944
|
+
pcManager: {
|
|
945
|
+
requirePublisher: vi.fn(),
|
|
946
|
+
// a non-empty transceiver list keeps negotiate() off the createDataChannels path
|
|
947
|
+
publisher: { getTransceivers: () => [{}], off: vi.fn(), once: vi.fn() },
|
|
948
|
+
negotiate,
|
|
949
|
+
},
|
|
950
|
+
handleDisconnect: vi.fn(),
|
|
951
|
+
});
|
|
952
|
+
return { negotiate, controllers };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const pendingAborts = (engine: RTCEngine) =>
|
|
956
|
+
(engine as unknown as { pendingNegotiationAborts: Set<() => void> }).pendingNegotiationAborts;
|
|
957
|
+
|
|
958
|
+
it('does not add a Closing/Restarting listener per in-flight negotiate call', async () => {
|
|
959
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
960
|
+
const { negotiate, controllers } = stubNegotiableEngine(engine);
|
|
961
|
+
|
|
962
|
+
// A burst of server-initiated renegotiations used to stack a listener pair per call, which
|
|
963
|
+
// trips the emitter's max-listener warning at 11.
|
|
964
|
+
const closingBaseline = engine.listenerCount(EngineEvent.Closing);
|
|
965
|
+
const restartingBaseline = engine.listenerCount(EngineEvent.Restarting);
|
|
966
|
+
|
|
967
|
+
const pending = Array.from({ length: 20 }, () => engine.negotiate());
|
|
968
|
+
await tick();
|
|
969
|
+
|
|
970
|
+
expect(negotiate).toHaveBeenCalledTimes(20);
|
|
971
|
+
expect(engine.listenerCount(EngineEvent.Closing)).toBe(closingBaseline);
|
|
972
|
+
expect(engine.listenerCount(EngineEvent.Restarting)).toBe(restartingBaseline);
|
|
973
|
+
|
|
974
|
+
// The single central listener still has to reach every one of them.
|
|
975
|
+
engine.emit(EngineEvent.Closing);
|
|
976
|
+
await expect(Promise.all(pending)).resolves.toHaveLength(20);
|
|
977
|
+
expect(controllers).toHaveLength(20);
|
|
978
|
+
expect(controllers.every((c) => c.signal.aborted)).toBe(true);
|
|
979
|
+
expect(pendingAborts(engine).size).toBe(0);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('aborts in-flight negotiations on restart without disarming later ones', async () => {
|
|
983
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
984
|
+
const { controllers } = stubNegotiableEngine(engine);
|
|
985
|
+
|
|
986
|
+
const firstBatch = [engine.negotiate(), engine.negotiate()];
|
|
987
|
+
await tick();
|
|
988
|
+
engine.emit(EngineEvent.Restarting);
|
|
989
|
+
await expect(Promise.all(firstBatch)).resolves.toHaveLength(2);
|
|
990
|
+
expect(controllers.every((c) => c.signal.aborted)).toBe(true);
|
|
991
|
+
|
|
992
|
+
// Restarting fires on every reconnect attempt, so the fan-out point has to re-arm: a
|
|
993
|
+
// negotiation started after a restart must not be aborted on arrival.
|
|
994
|
+
const afterRestart = engine.negotiate();
|
|
995
|
+
await tick();
|
|
996
|
+
expect(controllers).toHaveLength(3);
|
|
997
|
+
expect(controllers[2].signal.aborted).toBe(false);
|
|
998
|
+
expect(pendingAborts(engine).size).toBe(1);
|
|
999
|
+
|
|
1000
|
+
engine.emit(EngineEvent.Restarting);
|
|
1001
|
+
await expect(afterRestart).resolves.toBeUndefined();
|
|
1002
|
+
expect(controllers[2].signal.aborted).toBe(true);
|
|
1003
|
+
expect(pendingAborts(engine).size).toBe(0);
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
922
1006
|
});
|
package/src/room/RTCEngine.ts
CHANGED
|
@@ -173,6 +173,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
173
173
|
return !!this.reconnectTimeout;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
get serverVersion(): string | undefined {
|
|
177
|
+
return (
|
|
178
|
+
this.latestJoinResponse?.serverInfo?.version ||
|
|
179
|
+
this.latestJoinResponse?.serverVersion ||
|
|
180
|
+
undefined
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
176
184
|
/**
|
|
177
185
|
* Owns the data channels: the three flow-controlled publisher wrappers (engine-lifetime; the
|
|
178
186
|
* RTCDataChannel handles underneath are attached/detached as peer connections come and go, with
|
|
@@ -260,6 +268,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
260
268
|
/** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
|
|
261
269
|
private transportConnectingSince?: number;
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Abort handlers for the in-flight `negotiate()` calls, which a single central
|
|
273
|
+
* Closing/Restarting listener pair fans out to. Registering a listener pair per call instead
|
|
274
|
+
* accumulates them on the engine emitter and trips its max-listener warning under
|
|
275
|
+
* renegotiation bursts.
|
|
276
|
+
*/
|
|
277
|
+
private pendingNegotiationAborts = new Set<() => void>();
|
|
278
|
+
|
|
263
279
|
constructor(private options: InternalRoomOptions) {
|
|
264
280
|
super();
|
|
265
281
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
|
|
@@ -300,6 +316,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
300
316
|
this.client.onParticipantUpdate = (updates) =>
|
|
301
317
|
this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
302
318
|
this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
|
|
319
|
+
|
|
320
|
+
const abortPendingNegotiations = () => {
|
|
321
|
+
// Iterate a copy: each handler removes itself from the set as its negotiation settles.
|
|
322
|
+
for (const abort of Array.from(this.pendingNegotiationAborts)) {
|
|
323
|
+
abort();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
this.on(EngineEvent.Closing, abortPendingNegotiations);
|
|
327
|
+
this.on(EngineEvent.Restarting, abortPendingNegotiations);
|
|
303
328
|
}
|
|
304
329
|
|
|
305
330
|
/** @internal */
|
|
@@ -655,8 +680,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
655
680
|
sdp: sd.sdp,
|
|
656
681
|
midToTrackId,
|
|
657
682
|
});
|
|
658
|
-
|
|
683
|
+
// in dual PC mode the publisher answer carries no mapping (the server's publisher
|
|
684
|
+
// transport has no sending tracks) and must not clobber the subscriber offer mapping
|
|
685
|
+
if (this.pcManager.mode === 'publisher-only') {
|
|
686
|
+
this.midToTrackId = midToTrackId;
|
|
687
|
+
}
|
|
659
688
|
await this.pcManager.setPublisherAnswer(sd, offerId);
|
|
689
|
+
this.emit(EngineEvent.RequestSubscribedCodecRefresh);
|
|
660
690
|
};
|
|
661
691
|
|
|
662
692
|
// add candidate on trickle
|
|
@@ -726,7 +756,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
726
756
|
|
|
727
757
|
this.client.onMediaSectionsRequirement = (requirement: MediaSectionsRequirement) => {
|
|
728
758
|
this.addMediaSections(requirement.numAudios, requirement.numVideos);
|
|
729
|
-
this.negotiate()
|
|
759
|
+
this.negotiate().catch((err) => {
|
|
760
|
+
this.log.error(err);
|
|
761
|
+
});
|
|
730
762
|
};
|
|
731
763
|
|
|
732
764
|
this.client.onPublishDataTrackResponse = (event: PublishDataTrackResponse) => {
|
|
@@ -918,12 +950,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
918
950
|
this.log.error('Received encrypted packet but E2EE not set up');
|
|
919
951
|
return;
|
|
920
952
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
953
|
+
let decryptedData;
|
|
954
|
+
try {
|
|
955
|
+
decryptedData = await this.e2eeManager.handleEncryptedData(
|
|
956
|
+
dp.value.value.encryptedValue as NonSharedUint8Array,
|
|
957
|
+
dp.value.value.iv as NonSharedUint8Array,
|
|
958
|
+
dp.participantIdentity,
|
|
959
|
+
dp.value.value.keyIndex,
|
|
960
|
+
);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
this.log.debug('failed to decrypt data packet', {
|
|
963
|
+
error: err,
|
|
964
|
+
participantIdentity: dp.participantIdentity,
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
927
968
|
const decryptedPacket = EncryptedPacketPayload.fromBinary(decryptedData.payload);
|
|
928
969
|
const newDp = new DataPacket({
|
|
929
970
|
value: decryptedPacket.value,
|
|
@@ -1786,8 +1827,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1786
1827
|
if (this.isClosed) {
|
|
1787
1828
|
reject(new NegotiationError('cannot negotiate on closed engine'));
|
|
1788
1829
|
}
|
|
1789
|
-
this.
|
|
1790
|
-
this.on(EngineEvent.Restarting, handleClosed);
|
|
1830
|
+
this.pendingNegotiationAborts.add(handleClosed);
|
|
1791
1831
|
this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
|
|
1792
1832
|
this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
|
|
1793
1833
|
|
|
@@ -1811,8 +1851,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1811
1851
|
reject(new Error(String(e)));
|
|
1812
1852
|
}
|
|
1813
1853
|
} finally {
|
|
1814
|
-
this.
|
|
1815
|
-
this.off(EngineEvent.Restarting, handleClosed);
|
|
1854
|
+
this.pendingNegotiationAborts.delete(handleClosed);
|
|
1816
1855
|
}
|
|
1817
1856
|
});
|
|
1818
1857
|
}
|
|
@@ -2074,6 +2113,7 @@ export type EngineEventCallbacks = {
|
|
|
2074
2113
|
joined: (joinResponse: JoinResponse) => void;
|
|
2075
2114
|
tokenRefreshed: (token: string) => void;
|
|
2076
2115
|
serverRegionsReported: (regions: RegionSettings) => void;
|
|
2116
|
+
requestSubscribedCodecRefresh: () => void;
|
|
2077
2117
|
};
|
|
2078
2118
|
|
|
2079
2119
|
export interface RegionStrategy {
|
package/src/room/Room.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@livekit/protocol';
|
|
11
11
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import MockMediaStreamTrack from '../test/MockMediaStreamTrack';
|
|
13
|
+
import { stubMissingDeflateRawSupport } from '../test/compressionStreamStubs';
|
|
13
14
|
import Room, { ConnectionState } from './Room';
|
|
14
15
|
import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
|
|
15
16
|
import { EngineEvent, ParticipantEvent, RoomEvent } from './events';
|
|
@@ -46,8 +47,8 @@ describe('Active device switch', () => {
|
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
describe('Room signaling options', () => {
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
/** Drives `connectSignal` with E2EE enabled against a stub engine, returning the `join` mock. */
|
|
51
|
+
async function connectSignalWithE2EE(room: Room) {
|
|
51
52
|
const join = vi.fn().mockResolvedValue({
|
|
52
53
|
joinResponse: new JoinResponse({
|
|
53
54
|
room: { name: 'test-room', sid: 'room-sid' },
|
|
@@ -91,19 +92,44 @@ describe('Room signaling options', () => {
|
|
|
91
92
|
new AbortController(),
|
|
92
93
|
);
|
|
93
94
|
|
|
95
|
+
return join;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function expectCapabilities(
|
|
99
|
+
join: ReturnType<typeof vi.fn>,
|
|
100
|
+
clientInfoCapabilities: Array<ClientInfo_Capability>,
|
|
101
|
+
) {
|
|
94
102
|
expect(join).toHaveBeenCalledWith(
|
|
95
103
|
'wss://test.livekit.io',
|
|
96
104
|
'test-token',
|
|
97
105
|
expect.objectContaining({
|
|
98
|
-
clientInfoCapabilities
|
|
99
|
-
ClientInfo_Capability.CAP_PACKET_TRAILER,
|
|
100
|
-
ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
|
|
101
|
-
],
|
|
106
|
+
clientInfoCapabilities,
|
|
102
107
|
e2eeEnabled: true,
|
|
103
108
|
}),
|
|
104
109
|
expect.any(AbortSignal),
|
|
105
110
|
false,
|
|
106
111
|
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it('advertises packet trailer capability when E2EE can handle trailers', async () => {
|
|
115
|
+
const join = await connectSignalWithE2EE(new Room());
|
|
116
|
+
|
|
117
|
+
expectCapabilities(join, [
|
|
118
|
+
ClientInfo_Capability.CAP_PACKET_TRAILER,
|
|
119
|
+
ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not advertise deflate-raw compression when the runtime cannot decompress it', async () => {
|
|
124
|
+
const restoreCompressionStreams = stubMissingDeflateRawSupport();
|
|
125
|
+
try {
|
|
126
|
+
const join = await connectSignalWithE2EE(new Room());
|
|
127
|
+
|
|
128
|
+
// Otherwise peers would compress what they send us and we'd have to drop it.
|
|
129
|
+
expectCapabilities(join, [ClientInfo_Capability.CAP_PACKET_TRAILER]);
|
|
130
|
+
} finally {
|
|
131
|
+
restoreCompressionStreams();
|
|
132
|
+
}
|
|
107
133
|
});
|
|
108
134
|
});
|
|
109
135
|
|