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
package/src/room/Room.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
ConnectionErrorReason,
|
|
78
78
|
UnexpectedConnectionState,
|
|
79
79
|
UnsupportedServer,
|
|
80
|
+
canFailOverToAnotherRegion,
|
|
80
81
|
} from './errors';
|
|
81
82
|
import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
|
|
82
83
|
import LocalParticipant from './participant/LocalParticipant';
|
|
@@ -120,14 +121,16 @@ import {
|
|
|
120
121
|
getEmptyAudioStreamTrack,
|
|
121
122
|
isBrowserSupported,
|
|
122
123
|
isCloud,
|
|
123
|
-
|
|
124
|
+
isDeflateRawCompressionSupported,
|
|
124
125
|
isLocalAudioTrack,
|
|
125
126
|
isLocalParticipant,
|
|
127
|
+
isLocalVideoTrack,
|
|
126
128
|
isReactNative,
|
|
127
129
|
isRemotePub,
|
|
128
130
|
isSafariBased,
|
|
129
131
|
isWeb,
|
|
130
132
|
numberToBigInt,
|
|
133
|
+
releaseEmptyAudioStreamTrack,
|
|
131
134
|
sleep,
|
|
132
135
|
supportsSetSinkId,
|
|
133
136
|
toHttpUrl,
|
|
@@ -146,6 +149,30 @@ export enum ConnectionState {
|
|
|
146
149
|
const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
|
|
147
150
|
const STATS_LOG_FREQUENCY_MS = 30 * 1000;
|
|
148
151
|
|
|
152
|
+
const DUMMY_AUDIO_ELEMENT_ID = 'livekit-dummy-audio-el';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* One silent audio element serves every room on the page, so it is reference counted: a room
|
|
156
|
+
* disconnecting must not pull it out from under the rooms still playing through it. The element
|
|
157
|
+
* and its track go away once the last room hands its reference back.
|
|
158
|
+
*/
|
|
159
|
+
let sharedDummyAudioElement:
|
|
160
|
+
| { element: HTMLAudioElement; stream: MediaStream; track: MediaStreamTrack; refCount: number }
|
|
161
|
+
| undefined;
|
|
162
|
+
|
|
163
|
+
function createSharedDummyAudioElement() {
|
|
164
|
+
const element = document.createElement('audio');
|
|
165
|
+
element.id = DUMMY_AUDIO_ELEMENT_ID;
|
|
166
|
+
element.autoplay = true;
|
|
167
|
+
element.hidden = true;
|
|
168
|
+
const track = getEmptyAudioStreamTrack();
|
|
169
|
+
track.enabled = true;
|
|
170
|
+
const stream = new MediaStream([track]);
|
|
171
|
+
element.srcObject = stream;
|
|
172
|
+
document.body.append(element);
|
|
173
|
+
return { element, stream, track, refCount: 0 };
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
/**
|
|
150
177
|
* In LiveKit, a room is the logical grouping for a list of participants.
|
|
151
178
|
* Participants in a room can publish tracks, and subscribe to others' tracks.
|
|
@@ -193,6 +220,31 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
193
220
|
|
|
194
221
|
private audioContext?: AudioContext;
|
|
195
222
|
|
|
223
|
+
/**
|
|
224
|
+
* whether `audioContext` was created by the SDK and therefore has to be closed by it.
|
|
225
|
+
* A context handed in through `webAudioMix` is owned by the application instead.
|
|
226
|
+
*/
|
|
227
|
+
private ownsAudioContext = false;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* set once the audio context has been released as part of a disconnect. Guards against
|
|
231
|
+
* creating a new context outside of a connection lifecycle, where nothing would close it again.
|
|
232
|
+
*/
|
|
233
|
+
private audioContextReleased = false;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* serialises `acquireAudioContext` against `releaseAudioContext`. Both await per-track graph
|
|
237
|
+
* rebuilds, so without it a reconnect landing inside a disconnect's teardown leaves the two
|
|
238
|
+
* racing to set the participants' context, and the loser's value is the one that sticks.
|
|
239
|
+
*/
|
|
240
|
+
private audioContextMutex: Mutex = new Mutex();
|
|
241
|
+
|
|
242
|
+
/** tears down the iOS dummy audio element, set while one is in place. See `startAudio` */
|
|
243
|
+
private releaseDummyAudioElement?: () => void;
|
|
244
|
+
|
|
245
|
+
/** silent audio tracks this room acquired, handed back to the shared source on disconnect */
|
|
246
|
+
private acquiredEmptyAudioTracks = new Set<MediaStreamTrack>();
|
|
247
|
+
|
|
196
248
|
/** used for aborting pending connections to a LiveKit server */
|
|
197
249
|
private abortController?: AbortController;
|
|
198
250
|
|
|
@@ -755,6 +807,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
755
807
|
updatedAtInMs: Date.now(),
|
|
756
808
|
maxAgeInMs: DEFAULT_MAX_AGE_MS,
|
|
757
809
|
});
|
|
810
|
+
})
|
|
811
|
+
.on(EngineEvent.RequestSubscribedCodecRefresh, () => {
|
|
812
|
+
for (const videoPub of this.localParticipant.videoTrackPublications.values()) {
|
|
813
|
+
if (isLocalVideoTrack(videoPub.track)) {
|
|
814
|
+
videoPub.track.refreshSubscribedCodecs();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
758
817
|
});
|
|
759
818
|
|
|
760
819
|
if (this.localParticipant) {
|
|
@@ -908,8 +967,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
908
967
|
if (
|
|
909
968
|
this.regionUrlProvider &&
|
|
910
969
|
error instanceof ConnectionError &&
|
|
911
|
-
error
|
|
912
|
-
error.reason !== ConnectionErrorReason.NotAllowed
|
|
970
|
+
canFailOverToAnotherRegion(error)
|
|
913
971
|
) {
|
|
914
972
|
let nextUrl: string | null = null;
|
|
915
973
|
try {
|
|
@@ -992,6 +1050,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
992
1050
|
autoSubscribe: connectOptions.autoSubscribe,
|
|
993
1051
|
adaptiveStream:
|
|
994
1052
|
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
|
1053
|
+
disableIceLite: connectOptions.disableIceLite,
|
|
1054
|
+
subscriberAllowPause: connectOptions.subscriberAllowPause,
|
|
995
1055
|
clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
|
|
996
1056
|
maxRetries: connectOptions.maxRetries,
|
|
997
1057
|
e2eeEnabled: !!this.e2eeManager,
|
|
@@ -1063,7 +1123,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1063
1123
|
this.engine.setRegionStrategy(this.createRegionStrategy());
|
|
1064
1124
|
}
|
|
1065
1125
|
|
|
1066
|
-
this.acquireAudioContext()
|
|
1126
|
+
this.acquireAudioContext(true).catch((error) =>
|
|
1127
|
+
this.log.warn('Could not acquire audio context', { ...this.logContext, error }),
|
|
1128
|
+
);
|
|
1067
1129
|
|
|
1068
1130
|
this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
|
|
1069
1131
|
|
|
@@ -1211,8 +1273,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1211
1273
|
let req: SimulateScenario | undefined;
|
|
1212
1274
|
switch (scenario) {
|
|
1213
1275
|
case 'signal-reconnect':
|
|
1276
|
+
const reconnectDelay = typeof arg === 'number' ? arg : 0;
|
|
1214
1277
|
// @ts-expect-error function is private
|
|
1215
|
-
await this.engine.client.handleOnClose('simulate disconnect');
|
|
1278
|
+
await this.engine.client.handleOnClose('simulate disconnect', undefined, reconnectDelay);
|
|
1216
1279
|
break;
|
|
1217
1280
|
case 'fail-on-v1-path':
|
|
1218
1281
|
this.engine.failNextV1Path();
|
|
@@ -1334,6 +1397,93 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1334
1397
|
await this.disconnect();
|
|
1335
1398
|
};
|
|
1336
1399
|
|
|
1400
|
+
/**
|
|
1401
|
+
* iOS blocks audio element playback if
|
|
1402
|
+
* - user is not publishing audio themselves and
|
|
1403
|
+
* - no other audio source is playing
|
|
1404
|
+
*
|
|
1405
|
+
* as a workaround, we create an audio element with an empty track, so that
|
|
1406
|
+
* silent audio is always playing
|
|
1407
|
+
*
|
|
1408
|
+
* The element is shared with any other room on the page and handed back on disconnect, so it
|
|
1409
|
+
* outlives whichever room happened to create it.
|
|
1410
|
+
*/
|
|
1411
|
+
private acquireDummyAudioElement(): HTMLAudioElement {
|
|
1412
|
+
if (!sharedDummyAudioElement) {
|
|
1413
|
+
sharedDummyAudioElement = createSharedDummyAudioElement();
|
|
1414
|
+
}
|
|
1415
|
+
const shared = sharedDummyAudioElement;
|
|
1416
|
+
if (this.releaseDummyAudioElement) {
|
|
1417
|
+
// this room already holds a reference, a second one would never be handed back
|
|
1418
|
+
return shared.element;
|
|
1419
|
+
}
|
|
1420
|
+
shared.refCount += 1;
|
|
1421
|
+
|
|
1422
|
+
// the listener is per room rather than shared, so that the room it resumes playback for is
|
|
1423
|
+
// still connected, and so that it goes away with the room that installed it
|
|
1424
|
+
const handleVisibilityChange = () => {
|
|
1425
|
+
// set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
|
|
1426
|
+
shared.element.srcObject = document.hidden ? null : shared.stream;
|
|
1427
|
+
if (!document.hidden) {
|
|
1428
|
+
this.log.debug(
|
|
1429
|
+
'page visible again, triggering startAudio to resume playback and update playback status',
|
|
1430
|
+
);
|
|
1431
|
+
this.startAudio().catch((error) =>
|
|
1432
|
+
this.log.warn('Could not restart audio on page becoming visible', {
|
|
1433
|
+
...this.logContext,
|
|
1434
|
+
error,
|
|
1435
|
+
}),
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
1440
|
+
|
|
1441
|
+
this.releaseDummyAudioElement = () => {
|
|
1442
|
+
this.releaseDummyAudioElement = undefined;
|
|
1443
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
1444
|
+
shared.refCount -= 1;
|
|
1445
|
+
if (shared.refCount > 0) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
shared.element.srcObject = null;
|
|
1449
|
+
shared.element.remove();
|
|
1450
|
+
sharedDummyAudioElement = undefined;
|
|
1451
|
+
releaseEmptyAudioStreamTrack(shared.track).catch((error) =>
|
|
1452
|
+
this.log.warn('Could not release the dummy audio element track', {
|
|
1453
|
+
...this.logContext,
|
|
1454
|
+
error,
|
|
1455
|
+
}),
|
|
1456
|
+
);
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
return shared.element;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Obtains a silent audio track from the shared source and keeps track of it, so that it can be
|
|
1464
|
+
* handed back on disconnect and the shared `AudioContext` behind it can be closed again.
|
|
1465
|
+
*/
|
|
1466
|
+
private acquireEmptyAudioStreamTrack(): MediaStreamTrack {
|
|
1467
|
+
const track = getEmptyAudioStreamTrack();
|
|
1468
|
+
this.acquiredEmptyAudioTracks.add(track);
|
|
1469
|
+
return track;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private releaseAcquiredEmptyAudioTrack(track: MediaStreamTrack) {
|
|
1473
|
+
if (!this.acquiredEmptyAudioTracks.delete(track)) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
releaseEmptyAudioStreamTrack(track).catch((error) =>
|
|
1477
|
+
this.log.warn('Could not release empty audio stream track', { ...this.logContext, error }),
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
private releaseAcquiredEmptyAudioTracks() {
|
|
1482
|
+
for (const track of this.acquiredEmptyAudioTracks) {
|
|
1483
|
+
this.releaseAcquiredEmptyAudioTrack(track);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1337
1487
|
/**
|
|
1338
1488
|
* Browsers have different policies regarding audio playback. Most requiring
|
|
1339
1489
|
* some form of user interaction (click/tap/etc).
|
|
@@ -1344,46 +1494,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1344
1494
|
startAudio = async () => {
|
|
1345
1495
|
const elements: Array<HTMLMediaElement> = [];
|
|
1346
1496
|
const browser = getBrowser();
|
|
1347
|
-
if (browser && browser.os === 'iOS') {
|
|
1348
|
-
|
|
1349
|
-
* iOS blocks audio element playback if
|
|
1350
|
-
* - user is not publishing audio themselves and
|
|
1351
|
-
* - no other audio source is playing
|
|
1352
|
-
*
|
|
1353
|
-
* as a workaround, we create an audio element with an empty track, so that
|
|
1354
|
-
* silent audio is always playing
|
|
1355
|
-
*/
|
|
1356
|
-
const audioId = 'livekit-dummy-audio-el';
|
|
1357
|
-
let dummyAudioEl = document.getElementById(audioId) as HTMLAudioElement | null;
|
|
1358
|
-
if (!dummyAudioEl) {
|
|
1359
|
-
dummyAudioEl = document.createElement('audio');
|
|
1360
|
-
dummyAudioEl.id = audioId;
|
|
1361
|
-
dummyAudioEl.autoplay = true;
|
|
1362
|
-
dummyAudioEl.hidden = true;
|
|
1363
|
-
const track = getEmptyAudioStreamTrack();
|
|
1364
|
-
track.enabled = true;
|
|
1365
|
-
const stream = new MediaStream([track]);
|
|
1366
|
-
dummyAudioEl.srcObject = stream;
|
|
1367
|
-
document.addEventListener('visibilitychange', () => {
|
|
1368
|
-
if (!dummyAudioEl) {
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
// set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
|
|
1372
|
-
dummyAudioEl.srcObject = document.hidden ? null : stream;
|
|
1373
|
-
if (!document.hidden) {
|
|
1374
|
-
this.log.debug(
|
|
1375
|
-
'page visible again, triggering startAudio to resume playback and update playback status',
|
|
1376
|
-
);
|
|
1377
|
-
this.startAudio();
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
document.body.append(dummyAudioEl);
|
|
1381
|
-
this.once(RoomEvent.Disconnected, () => {
|
|
1382
|
-
dummyAudioEl?.remove();
|
|
1383
|
-
dummyAudioEl = null;
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
elements.push(dummyAudioEl);
|
|
1497
|
+
if (browser && browser.os === 'iOS' && !this.audioContextReleased) {
|
|
1498
|
+
elements.push(this.acquireDummyAudioElement());
|
|
1387
1499
|
}
|
|
1388
1500
|
|
|
1389
1501
|
this.remoteParticipants.forEach((p) => {
|
|
@@ -1398,7 +1510,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1398
1510
|
|
|
1399
1511
|
try {
|
|
1400
1512
|
await Promise.all([
|
|
1401
|
-
|
|
1513
|
+
// once the audio context has been released as part of a disconnect there is nothing left
|
|
1514
|
+
// to unlock, and creating a new context here would leak it as no disconnect will follow
|
|
1515
|
+
...(this.audioContextReleased ? [] : [this.acquireAudioContext()]),
|
|
1402
1516
|
...elements.map((e) => {
|
|
1403
1517
|
// when webAudioMix is enabled, attached elements are deliberately kept muted by
|
|
1404
1518
|
// RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
|
|
@@ -1848,6 +1962,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1848
1962
|
}
|
|
1849
1963
|
|
|
1850
1964
|
try {
|
|
1965
|
+
// audio tracks that outlive this room are about to become unreachable through the local
|
|
1966
|
+
// participant, collect them while we can so they can be detached from the context below
|
|
1967
|
+
const retainedAudioTracks = shouldStopTracks
|
|
1968
|
+
? []
|
|
1969
|
+
: Array.from(this.localParticipant.audioTrackPublications.values())
|
|
1970
|
+
.map((pub) => pub.track)
|
|
1971
|
+
.filter(isLocalAudioTrack);
|
|
1972
|
+
|
|
1851
1973
|
this.remoteParticipants.forEach((p) => {
|
|
1852
1974
|
p.trackPublications.forEach((pub) => {
|
|
1853
1975
|
p.unpublishTrack(pub.trackSid);
|
|
@@ -1890,10 +2012,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1890
2012
|
this.remoteParticipants.clear();
|
|
1891
2013
|
this.sidToIdentity.clear();
|
|
1892
2014
|
this.activeSpeakers = [];
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2015
|
+
this.releaseDummyAudioElement?.();
|
|
2016
|
+
this.releaseAcquiredEmptyAudioTracks();
|
|
2017
|
+
// teardown is asynchronous (processors need to release their web audio nodes), but
|
|
2018
|
+
// `handleDisconnect` is not, so let it finish in the background
|
|
2019
|
+
this.releaseAudioContext(retainedAudioTracks).catch((error) =>
|
|
2020
|
+
this.log.warn('Could not release audio context', { ...this.logContext, error }),
|
|
2021
|
+
);
|
|
1897
2022
|
if (isWeb()) {
|
|
1898
2023
|
window.removeEventListener('beforeunload', this.onPageLeave);
|
|
1899
2024
|
window.removeEventListener('pagehide', this.onPageLeave);
|
|
@@ -2097,7 +2222,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2097
2222
|
// find the participant
|
|
2098
2223
|
const participant = this.remoteParticipants.get(packet.participantIdentity);
|
|
2099
2224
|
if (packet.value.case === 'user') {
|
|
2100
|
-
this.handleUserPacket(
|
|
2225
|
+
this.handleUserPacket(
|
|
2226
|
+
participant,
|
|
2227
|
+
packet.value.value,
|
|
2228
|
+
packet.kind,
|
|
2229
|
+
encryptionType,
|
|
2230
|
+
packet.participantIdentity,
|
|
2231
|
+
);
|
|
2101
2232
|
} else if (packet.value.case === 'transcription') {
|
|
2102
2233
|
this.handleTranscription(participant, packet.value.value);
|
|
2103
2234
|
} else if (packet.value.case === 'sipDtmf') {
|
|
@@ -2147,6 +2278,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2147
2278
|
userPacket: UserPacket,
|
|
2148
2279
|
kind: DataPacket_Kind,
|
|
2149
2280
|
encryptionType: Encryption_Type,
|
|
2281
|
+
participantIdentity: string,
|
|
2150
2282
|
) => {
|
|
2151
2283
|
this.emit(
|
|
2152
2284
|
RoomEvent.DataReceived,
|
|
@@ -2155,6 +2287,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2155
2287
|
kind,
|
|
2156
2288
|
userPacket.topic,
|
|
2157
2289
|
encryptionType,
|
|
2290
|
+
participantIdentity,
|
|
2158
2291
|
);
|
|
2159
2292
|
|
|
2160
2293
|
// also emit on the participant
|
|
@@ -2340,38 +2473,132 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2340
2473
|
});
|
|
2341
2474
|
};
|
|
2342
2475
|
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
this.
|
|
2351
|
-
|
|
2476
|
+
/**
|
|
2477
|
+
* @param forConnect whether this acquisition belongs to a connect, which is the only caller
|
|
2478
|
+
* allowed to put a released room back on an audio context.
|
|
2479
|
+
*/
|
|
2480
|
+
private async acquireAudioContext(forConnect = false) {
|
|
2481
|
+
const unlock = await this.audioContextMutex.lock();
|
|
2482
|
+
try {
|
|
2483
|
+
if (this.audioContextReleased && !forConnect) {
|
|
2484
|
+
// a disconnect set the flag and took the lock while this call was queued behind it.
|
|
2485
|
+
// Creating a context now would leave one open on a room nothing tears down again
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
this.audioContextReleased = false;
|
|
2489
|
+
const providedAudioContext =
|
|
2490
|
+
typeof this.options.webAudioMix !== 'boolean'
|
|
2491
|
+
? this.options.webAudioMix.audioContext
|
|
2492
|
+
: undefined;
|
|
2493
|
+
if (providedAudioContext) {
|
|
2494
|
+
// override audio context with custom audio context if supplied by user
|
|
2495
|
+
await this.setRoomAudioContext(providedAudioContext, false);
|
|
2496
|
+
} else if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
2497
|
+
// by using an AudioContext, it reduces lag on audio elements
|
|
2498
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
|
2499
|
+
await this.setRoomAudioContext(getNewAudioContext() ?? undefined, true);
|
|
2500
|
+
}
|
|
2352
2501
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2502
|
+
if (this.audioContext && this.audioContext.state === 'suspended') {
|
|
2503
|
+
// for iOS a newly created AudioContext is always in `suspended` state.
|
|
2504
|
+
// we try our best to resume the context here, if that doesn't work, we just continue with regular processing
|
|
2505
|
+
try {
|
|
2506
|
+
await Promise.race([this.audioContext.resume(), sleep(200)]);
|
|
2507
|
+
} catch (e: any) {
|
|
2508
|
+
this.log.warn('Could not resume audio context', { error: e });
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
const newContextIsRunning = this.audioContext?.state === 'running';
|
|
2513
|
+
if (newContextIsRunning !== this.canPlaybackAudio) {
|
|
2514
|
+
this.audioEnabled = newContextIsRunning;
|
|
2515
|
+
this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
|
|
2516
|
+
}
|
|
2517
|
+
} finally {
|
|
2518
|
+
unlock();
|
|
2357
2519
|
}
|
|
2520
|
+
}
|
|
2358
2521
|
|
|
2359
|
-
|
|
2522
|
+
/**
|
|
2523
|
+
* Points the room and its participants at `audioContext`, rebuilding the web audio graph of
|
|
2524
|
+
* every audio track in it. Closes the previous context if the SDK `owned` it.
|
|
2525
|
+
*/
|
|
2526
|
+
private async setRoomAudioContext(audioContext: AudioContext | undefined, owned: boolean) {
|
|
2527
|
+
const ownsAudioContext = !!audioContext && owned;
|
|
2528
|
+
if (this.audioContext === audioContext) {
|
|
2529
|
+
this.ownsAudioContext = ownsAudioContext;
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
const contextToClose = this.ownsAudioContext ? this.audioContext : undefined;
|
|
2533
|
+
this.audioContext = audioContext;
|
|
2534
|
+
this.ownsAudioContext = ownsAudioContext;
|
|
2360
2535
|
|
|
2361
|
-
|
|
2362
|
-
//
|
|
2363
|
-
//
|
|
2364
|
-
|
|
2365
|
-
await Promise.
|
|
2366
|
-
|
|
2367
|
-
|
|
2536
|
+
try {
|
|
2537
|
+
// every audio track rebuilds its nodes in the new context, which has to happen before the
|
|
2538
|
+
// previous one is closed so that processors can move off it while it is still alive
|
|
2539
|
+
if (this.options.webAudioMix) {
|
|
2540
|
+
await Promise.all(
|
|
2541
|
+
Array.from(this.remoteParticipants.values()).map((participant) =>
|
|
2542
|
+
participant.setAudioContext(audioContext),
|
|
2543
|
+
),
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
await this.localParticipant.setAudioContext(audioContext);
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
// the context is being abandoned either way, so a track that failed to move off it must
|
|
2549
|
+
// not stop us closing it — that would leak the context this whole path exists to release
|
|
2550
|
+
this.log.warn('Could not move audio tracks to the new audio context', {
|
|
2551
|
+
...this.logContext,
|
|
2552
|
+
error,
|
|
2553
|
+
});
|
|
2554
|
+
} finally {
|
|
2555
|
+
if (contextToClose && contextToClose.state !== 'closed') {
|
|
2556
|
+
try {
|
|
2557
|
+
await contextToClose.close();
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
this.log.warn('Could not close audio context', { ...this.logContext, error });
|
|
2560
|
+
}
|
|
2368
2561
|
}
|
|
2369
2562
|
}
|
|
2563
|
+
}
|
|
2370
2564
|
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2565
|
+
/**
|
|
2566
|
+
* Detaches the web audio graph the SDK built from the current audio context and closes that
|
|
2567
|
+
* context if the SDK created it. A context provided through `webAudioMix` is left untouched.
|
|
2568
|
+
*/
|
|
2569
|
+
private async releaseAudioContext(retainedAudioTracks: LocalAudioTrack[] = []) {
|
|
2570
|
+
// both reads happen before the first await, so they describe the room as it was when the
|
|
2571
|
+
// disconnect started rather than after a reconnect has had a chance to run
|
|
2572
|
+
this.audioContextReleased = true;
|
|
2573
|
+
const contextToRelease = this.audioContext;
|
|
2574
|
+
const unlock = await this.audioContextMutex.lock();
|
|
2575
|
+
try {
|
|
2576
|
+
if (!this.audioContextReleased || this.audioContext !== contextToRelease) {
|
|
2577
|
+
// a reconnect acquired a new context while we were waiting, closing it would leave the
|
|
2578
|
+
// reconnected room without one
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (!this.ownsAudioContext) {
|
|
2582
|
+
// a provided context stays open, so tracks that outlive the room stay functional on it.
|
|
2583
|
+
// The tracks this room detached have released their nodes during track teardown already.
|
|
2584
|
+
// read under the lock: an acquire queued behind an in-flight release flips this flag
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
// tracks that outlive the room aren't reachable through the local participant anymore,
|
|
2589
|
+
// but still hold web audio nodes in the context we're about to close
|
|
2590
|
+
await Promise.all(retainedAudioTracks.map((track) => track.setAudioContext(undefined)));
|
|
2591
|
+
} catch (error) {
|
|
2592
|
+
this.log.warn('Could not detach retained audio tracks from the audio context', {
|
|
2593
|
+
...this.logContext,
|
|
2594
|
+
error,
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
// the participant maps have already been cleared by `handleDisconnect`, so this reaches no
|
|
2598
|
+
// tracks of its own — `retainedAudioTracks` above is what detaches them
|
|
2599
|
+
await this.setRoomAudioContext(undefined, false);
|
|
2600
|
+
} finally {
|
|
2601
|
+
unlock();
|
|
2375
2602
|
}
|
|
2376
2603
|
}
|
|
2377
2604
|
|
|
@@ -2402,7 +2629,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2402
2629
|
);
|
|
2403
2630
|
}
|
|
2404
2631
|
if (this.options.webAudioMix) {
|
|
2405
|
-
participant
|
|
2632
|
+
participant
|
|
2633
|
+
.setAudioContext(this.audioContext)
|
|
2634
|
+
.catch((error) =>
|
|
2635
|
+
this.log.warn('Could not set audio context', { ...this.logContext, error }),
|
|
2636
|
+
);
|
|
2406
2637
|
}
|
|
2407
2638
|
if (this.options.audioOutput?.deviceId) {
|
|
2408
2639
|
participant
|
|
@@ -2566,7 +2797,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2566
2797
|
}
|
|
2567
2798
|
// Advertise deflate-raw decompression support so peers know they can send us compressed data
|
|
2568
2799
|
// streams (gated separately from clientProtocol — see the data streams v2 spec).
|
|
2569
|
-
if (
|
|
2800
|
+
if (isDeflateRawCompressionSupported()) {
|
|
2570
2801
|
capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
|
|
2571
2802
|
}
|
|
2572
2803
|
return capabilities;
|
|
@@ -2903,7 +3134,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2903
3134
|
new LocalAudioTrack(
|
|
2904
3135
|
publishOptions.useRealTracks && navigator.mediaDevices?.getUserMedia
|
|
2905
3136
|
? (await navigator.mediaDevices.getUserMedia({ audio: true })).getAudioTracks()[0]
|
|
2906
|
-
:
|
|
3137
|
+
: this.acquireEmptyAudioStreamTrack(),
|
|
2907
3138
|
undefined,
|
|
2908
3139
|
false,
|
|
2909
3140
|
this.audioContext,
|
|
@@ -2946,7 +3177,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2946
3177
|
info.tracks = [...info.tracks, videoTrack];
|
|
2947
3178
|
}
|
|
2948
3179
|
if (participantOptions.audio) {
|
|
2949
|
-
const dummyTrack =
|
|
3180
|
+
const dummyTrack = this.acquireEmptyAudioStreamTrack();
|
|
2950
3181
|
const audioTrack = new TrackInfo({
|
|
2951
3182
|
source: TrackSource.MICROPHONE,
|
|
2952
3183
|
sid: Math.floor(Math.random() * 10_000).toString(),
|
|
@@ -3060,6 +3291,7 @@ export type RoomEventCallbacks = {
|
|
|
3060
3291
|
kind?: DataPacket_Kind,
|
|
3061
3292
|
topic?: string,
|
|
3062
3293
|
encryptionType?: Encryption_Type,
|
|
3294
|
+
participantIdentity?: string,
|
|
3063
3295
|
) => void;
|
|
3064
3296
|
sipDTMFReceived: (dtmf: SipDTMF, participant?: RemoteParticipant) => void;
|
|
3065
3297
|
transcriptionReceived: (
|
|
@@ -17,15 +17,22 @@ class FakeDataChannel extends EventTarget {
|
|
|
17
17
|
|
|
18
18
|
onbufferedamountlow: (() => void) | null = null;
|
|
19
19
|
|
|
20
|
+
readyState: RTCDataChannelState = 'open';
|
|
21
|
+
|
|
20
22
|
send = vi.fn();
|
|
21
23
|
|
|
22
|
-
close = vi.fn()
|
|
24
|
+
close = vi.fn(() => {
|
|
25
|
+
this.readyState = 'closing';
|
|
26
|
+
});
|
|
23
27
|
|
|
24
28
|
constructor(public label: string) {
|
|
25
29
|
super();
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
const reliableHandle = (manager: DataChannelManager) =>
|
|
34
|
+
manager.getHandle(DataChannelKind.RELIABLE) as RTCDataChannel;
|
|
35
|
+
|
|
29
36
|
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
30
37
|
|
|
31
38
|
function makeManager(overrides?: Partial<DataChannelManagerOptions>) {
|
|
@@ -89,22 +96,32 @@ describe('DataChannelManager', () => {
|
|
|
89
96
|
manager.lossy.stopThresholdTuning();
|
|
90
97
|
});
|
|
91
98
|
|
|
92
|
-
it('recreating channels rejects waiters parked on
|
|
93
|
-
const { manager, pcManager, created } = makeManager();
|
|
99
|
+
it('recreating channels closes the replaced handles and rejects waiters parked on them', async () => {
|
|
100
|
+
const { manager, opts, pcManager, created } = makeManager();
|
|
94
101
|
manager.createPublisherChannels(pcManager);
|
|
102
|
+
const firstGeneration = { ...created };
|
|
95
103
|
|
|
96
104
|
// Park a reliable sender on the first-generation channel.
|
|
97
105
|
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
98
|
-
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
106
|
+
const parked = manager.reliable.waitForHeadroomWithLock(reliableHandle(manager));
|
|
99
107
|
parked.catch(() => {});
|
|
100
108
|
await tick();
|
|
101
109
|
|
|
102
|
-
// Safari null-id path: channels recreated
|
|
110
|
+
// Safari null-id path: channels recreated while the old ones are still open.
|
|
103
111
|
manager.createPublisherChannels(pcManager);
|
|
104
112
|
|
|
105
113
|
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
114
|
+
for (const dc of Object.values(firstGeneration)) {
|
|
115
|
+
expect(dc.close).toHaveBeenCalled();
|
|
116
|
+
// Unhooked before closing, so the deliberate close isn't reported as an unexpected one.
|
|
117
|
+
expect(dc.onclose).toBeNull();
|
|
118
|
+
expect(dc.onbufferedamountlow).toBeNull();
|
|
119
|
+
}
|
|
120
|
+
expect(opts.onChannelClose).not.toHaveBeenCalled();
|
|
106
121
|
// The gate recovers against the fresh (empty) channel.
|
|
107
|
-
await expect(
|
|
122
|
+
await expect(
|
|
123
|
+
manager.reliable.waitForHeadroomWithLock(reliableHandle(manager)),
|
|
124
|
+
).resolves.toBeUndefined();
|
|
108
125
|
|
|
109
126
|
manager.lossy.stopThresholdTuning();
|
|
110
127
|
});
|
|
@@ -133,7 +150,7 @@ describe('DataChannelManager', () => {
|
|
|
133
150
|
manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel);
|
|
134
151
|
|
|
135
152
|
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
136
|
-
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
153
|
+
const parked = manager.reliable.waitForHeadroomWithLock(reliableHandle(manager));
|
|
137
154
|
parked.catch(() => {});
|
|
138
155
|
await tick();
|
|
139
156
|
|