livekit-client 2.20.2 → 2.22.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 +10 -10
- 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 +6 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2588 -1056
- 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 +6 -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/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +48 -65
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
- package/dist/ts4.2/room/Room.d.ts +6 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +28 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +14 -13
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +397 -101
- package/src/room/RTCEngine.ts +244 -545
- package/src/room/Room.test.ts +138 -4
- package/src/room/Room.ts +92 -25
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +9 -3
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/errors.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +119 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/version.ts +5 -1
package/src/room/Room.test.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ClientInfo_Capability,
|
|
3
|
+
JoinResponse,
|
|
4
|
+
StreamState as ProtoStreamState,
|
|
5
|
+
StreamStateUpdate,
|
|
6
|
+
SubscriptionError,
|
|
7
|
+
SubscriptionResponse,
|
|
8
|
+
TrackInfo,
|
|
9
|
+
TrackType,
|
|
10
|
+
} from '@livekit/protocol';
|
|
2
11
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import
|
|
12
|
+
import MockMediaStreamTrack from '../test/MockMediaStreamTrack';
|
|
13
|
+
import Room, { ConnectionState } from './Room';
|
|
4
14
|
import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
|
|
5
|
-
import { RoomEvent } from './events';
|
|
15
|
+
import { EngineEvent, ParticipantEvent, RoomEvent } from './events';
|
|
16
|
+
import RemoteParticipant from './participant/RemoteParticipant';
|
|
17
|
+
import RemoteTrackPublication from './track/RemoteTrackPublication';
|
|
18
|
+
import RemoteVideoTrack from './track/RemoteVideoTrack';
|
|
19
|
+
import { Track } from './track/Track';
|
|
6
20
|
|
|
7
21
|
describe('Active device switch', () => {
|
|
8
22
|
it('updates devices correctly', async () => {
|
|
@@ -81,7 +95,10 @@ describe('Room signaling options', () => {
|
|
|
81
95
|
'wss://test.livekit.io',
|
|
82
96
|
'test-token',
|
|
83
97
|
expect.objectContaining({
|
|
84
|
-
clientInfoCapabilities: [
|
|
98
|
+
clientInfoCapabilities: [
|
|
99
|
+
ClientInfo_Capability.CAP_PACKET_TRAILER,
|
|
100
|
+
ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW,
|
|
101
|
+
],
|
|
85
102
|
e2eeEnabled: true,
|
|
86
103
|
}),
|
|
87
104
|
expect.any(AbortSignal),
|
|
@@ -187,3 +204,120 @@ describe('Room lifecycle', () => {
|
|
|
187
204
|
}
|
|
188
205
|
});
|
|
189
206
|
});
|
|
207
|
+
|
|
208
|
+
describe('remote track subscription', () => {
|
|
209
|
+
it('does not replay a deferred ontrack after the subscription failed', () => {
|
|
210
|
+
const room = new Room();
|
|
211
|
+
room.state = ConnectionState.Connecting;
|
|
212
|
+
|
|
213
|
+
const trackSid = 'TR_h264';
|
|
214
|
+
const mediaTrack = { id: trackSid } as MediaStreamTrack;
|
|
215
|
+
const stream = {
|
|
216
|
+
id: `PA_remote|${trackSid}`,
|
|
217
|
+
getTracks: () => [mediaTrack],
|
|
218
|
+
} as unknown as MediaStream;
|
|
219
|
+
const receiver = {} as RTCRtpReceiver;
|
|
220
|
+
|
|
221
|
+
room.engine.emit(EngineEvent.MediaTrackAdded, mediaTrack, stream, receiver);
|
|
222
|
+
|
|
223
|
+
const replay = vi.spyOn(
|
|
224
|
+
room as unknown as {
|
|
225
|
+
onTrackAdded(
|
|
226
|
+
mediaTrack: MediaStreamTrack,
|
|
227
|
+
stream: MediaStream,
|
|
228
|
+
receiver: RTCRtpReceiver,
|
|
229
|
+
): void;
|
|
230
|
+
},
|
|
231
|
+
'onTrackAdded',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
room.engine.emit(
|
|
235
|
+
EngineEvent.SubscriptionError,
|
|
236
|
+
new SubscriptionResponse({
|
|
237
|
+
trackSid,
|
|
238
|
+
err: SubscriptionError.SE_CODEC_UNSUPPORTED,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
room.state = ConnectionState.Connected;
|
|
243
|
+
room.emit(RoomEvent.Connected);
|
|
244
|
+
|
|
245
|
+
expect(replay).not.toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('stream state updates', () => {
|
|
250
|
+
const participantSid = 'PA_remote';
|
|
251
|
+
const participantIdentity = 'remote-user';
|
|
252
|
+
const trackSid = 'TR_video';
|
|
253
|
+
|
|
254
|
+
function setupSubscribedTrack() {
|
|
255
|
+
const room = new Room();
|
|
256
|
+
room.state = ConnectionState.Connected;
|
|
257
|
+
|
|
258
|
+
const participant = new RemoteParticipant(
|
|
259
|
+
room.engine.client,
|
|
260
|
+
participantSid,
|
|
261
|
+
participantIdentity,
|
|
262
|
+
);
|
|
263
|
+
const publication = new RemoteTrackPublication(
|
|
264
|
+
Track.Kind.Video,
|
|
265
|
+
new TrackInfo({ sid: trackSid, type: TrackType.VIDEO, name: 'camera' }),
|
|
266
|
+
true,
|
|
267
|
+
);
|
|
268
|
+
publication.setTrack(
|
|
269
|
+
new RemoteVideoTrack(new MockMediaStreamTrack(), trackSid, undefined as never, {}),
|
|
270
|
+
);
|
|
271
|
+
participant.trackPublications.set(trackSid, publication);
|
|
272
|
+
|
|
273
|
+
(
|
|
274
|
+
room as unknown as { remoteParticipants: Map<string, RemoteParticipant> }
|
|
275
|
+
).remoteParticipants.set(participantIdentity, participant);
|
|
276
|
+
(room as unknown as { sidToIdentity: Map<string, string> }).sidToIdentity.set(
|
|
277
|
+
participantSid,
|
|
278
|
+
participantIdentity,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return { room, participant, publication };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function pushStreamState(room: Room, state: ProtoStreamState) {
|
|
285
|
+
(
|
|
286
|
+
room as unknown as { handleStreamStateUpdate: (update: StreamStateUpdate) => void }
|
|
287
|
+
).handleStreamStateUpdate(
|
|
288
|
+
new StreamStateUpdate({
|
|
289
|
+
streamStates: [{ participantSid, trackSid, state }],
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
it('emits TrackStreamStateChanged when the SFU pauses a subscribed track', () => {
|
|
295
|
+
const { room, participant, publication } = setupSubscribedTrack();
|
|
296
|
+
|
|
297
|
+
const roomEvents = vi.fn();
|
|
298
|
+
const participantEvents = vi.fn();
|
|
299
|
+
room.on(RoomEvent.TrackStreamStateChanged, roomEvents);
|
|
300
|
+
participant.on(ParticipantEvent.TrackStreamStateChanged, participantEvents);
|
|
301
|
+
|
|
302
|
+
pushStreamState(room, ProtoStreamState.PAUSED);
|
|
303
|
+
|
|
304
|
+
expect(publication.track?.streamState).toBe(Track.StreamState.Paused);
|
|
305
|
+
expect(roomEvents).toHaveBeenCalledWith(publication, Track.StreamState.Paused, participant);
|
|
306
|
+
expect(participantEvents).toHaveBeenCalledWith(publication, Track.StreamState.Paused);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('does not emit TrackStreamStateChanged when the state is unchanged', () => {
|
|
310
|
+
const { room, participant } = setupSubscribedTrack();
|
|
311
|
+
|
|
312
|
+
const roomEvents = vi.fn();
|
|
313
|
+
const participantEvents = vi.fn();
|
|
314
|
+
room.on(RoomEvent.TrackStreamStateChanged, roomEvents);
|
|
315
|
+
participant.on(ParticipantEvent.TrackStreamStateChanged, participantEvents);
|
|
316
|
+
|
|
317
|
+
// The track starts Active; a redundant ACTIVE update must stay silent.
|
|
318
|
+
pushStreamState(room, ProtoStreamState.ACTIVE);
|
|
319
|
+
|
|
320
|
+
expect(roomEvents).not.toHaveBeenCalled();
|
|
321
|
+
expect(participantEvents).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
});
|
package/src/room/Room.ts
CHANGED
|
@@ -113,11 +113,13 @@ import {
|
|
|
113
113
|
Future,
|
|
114
114
|
createDummyVideoStreamTrack,
|
|
115
115
|
extractChatMessage,
|
|
116
|
+
extractTrackSid,
|
|
116
117
|
extractTranscriptionSegments,
|
|
117
118
|
getDisconnectReasonFromConnectionError,
|
|
118
119
|
getEmptyAudioStreamTrack,
|
|
119
120
|
isBrowserSupported,
|
|
120
121
|
isCloud,
|
|
122
|
+
isCompressionStreamSupported,
|
|
121
123
|
isLocalAudioTrack,
|
|
122
124
|
isLocalParticipant,
|
|
123
125
|
isReactNative,
|
|
@@ -217,6 +219,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
217
219
|
|
|
218
220
|
private isResuming: boolean = false;
|
|
219
221
|
|
|
222
|
+
private pendingTrackAddedCallbacks = new Map<Track.SID, Set<() => void>>();
|
|
223
|
+
|
|
220
224
|
/**
|
|
221
225
|
* map to store first point in time when a particular transcription segment was received
|
|
222
226
|
*/
|
|
@@ -268,7 +272,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
268
272
|
this.maybeCreateEngine();
|
|
269
273
|
|
|
270
274
|
this.incomingDataStreamManager = new IncomingDataStreamManager();
|
|
271
|
-
this.outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
275
|
+
this.outgoingDataStreamManager = new OutgoingDataStreamManager(
|
|
276
|
+
this.engine,
|
|
277
|
+
this.log,
|
|
278
|
+
this.getRemoteParticipantClientProtocol,
|
|
279
|
+
this.getRemoteParticipantCapabilities,
|
|
280
|
+
this.getAllRemoteParticipantIdentities,
|
|
281
|
+
);
|
|
272
282
|
|
|
273
283
|
this.incomingDataTrackManager = new IncomingDataTrackManager({ e2eeManager: this.e2eeManager });
|
|
274
284
|
this.incomingDataTrackManager
|
|
@@ -308,7 +318,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
308
318
|
})
|
|
309
319
|
.on('packetAvailable', ({ handle, bytes }) => {
|
|
310
320
|
this.engine
|
|
311
|
-
.
|
|
321
|
+
.sendDataTrackFrame(bytes)
|
|
312
322
|
.finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
|
|
313
323
|
});
|
|
314
324
|
|
|
@@ -623,16 +633,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
623
633
|
this.isResuming = false;
|
|
624
634
|
this.log.debug('Resumed signal connection');
|
|
625
635
|
this.updateSubscriptions();
|
|
626
|
-
this.emitBufferedEvents();
|
|
627
636
|
if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
|
|
628
637
|
this.emit(RoomEvent.Reconnected);
|
|
629
638
|
}
|
|
639
|
+
this.emitBufferedEvents();
|
|
630
640
|
})
|
|
631
641
|
.on(EngineEvent.SignalResumed, () => {
|
|
632
|
-
this.bufferedEvents = [];
|
|
633
642
|
if (this.state === ConnectionState.Reconnecting || this.isResuming) {
|
|
634
643
|
this.sendSyncState();
|
|
635
644
|
}
|
|
645
|
+
this.emitBufferedEvents();
|
|
636
646
|
})
|
|
637
647
|
.on(EngineEvent.Restarting, this.handleRestarting)
|
|
638
648
|
.on(EngineEvent.Restarted, this.handleRestarted)
|
|
@@ -971,11 +981,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
971
981
|
autoSubscribe: connectOptions.autoSubscribe,
|
|
972
982
|
adaptiveStream:
|
|
973
983
|
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
|
974
|
-
clientInfoCapabilities:
|
|
975
|
-
isFrameMetadataSupported(roomOptions.frameMetadata ?? roomOptions.packetTrailer) ||
|
|
976
|
-
!!this.e2eeManager
|
|
977
|
-
? [ClientInfo_Capability.CAP_PACKET_TRAILER]
|
|
978
|
-
: undefined,
|
|
984
|
+
clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
|
|
979
985
|
maxRetries: connectOptions.maxRetries,
|
|
980
986
|
e2eeEnabled: !!this.e2eeManager,
|
|
981
987
|
websocketTimeout: connectOptions.websocketTimeout,
|
|
@@ -1585,24 +1591,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1585
1591
|
// at that time, ICE connectivity has not been established so the track is not
|
|
1586
1592
|
// technically subscribed.
|
|
1587
1593
|
// We'll defer these events until when the room is connected or eventually disconnected.
|
|
1588
|
-
if (
|
|
1594
|
+
if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
|
|
1595
|
+
const pendingTrackSid = extractTrackSid(mediaTrack, stream);
|
|
1596
|
+
this.log.debug('deferring on track for later', {
|
|
1597
|
+
mediaTrackId: mediaTrack.id,
|
|
1598
|
+
mediaStreamId: stream.id,
|
|
1599
|
+
tracksInStream: stream.getTracks().map((track) => track.id),
|
|
1600
|
+
});
|
|
1589
1601
|
const reconnectedHandler = () => {
|
|
1590
|
-
this.log.debug('deferring on track for later', {
|
|
1591
|
-
mediaTrackId: mediaTrack.id,
|
|
1592
|
-
mediaStreamId: stream.id,
|
|
1593
|
-
tracksInStream: stream.getTracks().map((track) => track.id),
|
|
1594
|
-
});
|
|
1595
|
-
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
1596
1602
|
cleanup();
|
|
1603
|
+
this.onTrackAdded(mediaTrack, stream, receiver);
|
|
1597
1604
|
};
|
|
1598
1605
|
const cleanup = () => {
|
|
1599
1606
|
this.off(RoomEvent.Reconnected, reconnectedHandler);
|
|
1600
1607
|
this.off(RoomEvent.Connected, reconnectedHandler);
|
|
1601
1608
|
this.off(RoomEvent.Disconnected, cleanup);
|
|
1609
|
+
if (pendingTrackSid) {
|
|
1610
|
+
const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
|
|
1611
|
+
pendingCallbacks?.delete(cleanup);
|
|
1612
|
+
if (pendingCallbacks?.size === 0) {
|
|
1613
|
+
this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1602
1616
|
};
|
|
1603
1617
|
this.once(RoomEvent.Reconnected, reconnectedHandler);
|
|
1604
1618
|
this.once(RoomEvent.Connected, reconnectedHandler);
|
|
1605
1619
|
this.once(RoomEvent.Disconnected, cleanup);
|
|
1620
|
+
if (pendingTrackSid) {
|
|
1621
|
+
const pendingCallbacks =
|
|
1622
|
+
this.pendingTrackAddedCallbacks.get(pendingTrackSid) ?? new Set<() => void>();
|
|
1623
|
+
pendingCallbacks.add(cleanup);
|
|
1624
|
+
this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
|
|
1625
|
+
}
|
|
1606
1626
|
return;
|
|
1607
1627
|
}
|
|
1608
1628
|
if (this.state === ConnectionState.Disconnected) {
|
|
@@ -1615,11 +1635,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1615
1635
|
}
|
|
1616
1636
|
const parts = unpackStreamId(stream.id);
|
|
1617
1637
|
const participantSid = parts[0];
|
|
1618
|
-
|
|
1619
|
-
let trackId = mediaTrack.id;
|
|
1638
|
+
const streamId = parts[1];
|
|
1620
1639
|
// firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
|
|
1621
1640
|
// and generates its own track id instead of infer from sdp track id.
|
|
1622
|
-
|
|
1641
|
+
let trackId = extractTrackSid(mediaTrack, stream) ?? mediaTrack.id;
|
|
1623
1642
|
|
|
1624
1643
|
if (participantSid === this.localParticipant.sid) {
|
|
1625
1644
|
this.log.warn('tried to create RemoteParticipant for local participant');
|
|
@@ -1688,6 +1707,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
1688
1707
|
}
|
|
1689
1708
|
}
|
|
1690
1709
|
|
|
1710
|
+
private cancelPendingTrackAdded(trackSid: Track.SID) {
|
|
1711
|
+
this.pendingTrackAddedCallbacks.get(trackSid)?.forEach((cleanup) => cleanup());
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1691
1714
|
private handleLocalTrackSubscribed(subscribedSid: string) {
|
|
1692
1715
|
const findPublication = () =>
|
|
1693
1716
|
this.localParticipant
|
|
@@ -2005,8 +2028,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2005
2028
|
return;
|
|
2006
2029
|
}
|
|
2007
2030
|
const newStreamState = Track.streamStateFromProto(streamState.state);
|
|
2031
|
+
const prevStreamState = pub.track.streamState;
|
|
2008
2032
|
pub.track.setStreamState(newStreamState);
|
|
2009
|
-
if (newStreamState !==
|
|
2033
|
+
if (newStreamState !== prevStreamState) {
|
|
2010
2034
|
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
|
2011
2035
|
this.emitWhenConnected(
|
|
2012
2036
|
RoomEvent.TrackStreamStateChanged,
|
|
@@ -2032,6 +2056,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2032
2056
|
};
|
|
2033
2057
|
|
|
2034
2058
|
private handleSubscriptionError = (update: SubscriptionResponse) => {
|
|
2059
|
+
this.cancelPendingTrackAdded(update.trackSid);
|
|
2060
|
+
|
|
2035
2061
|
const participant = Array.from(this.remoteParticipants.values()).find((p) =>
|
|
2036
2062
|
p.trackPublications.has(update.trackSid),
|
|
2037
2063
|
);
|
|
@@ -2406,6 +2432,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2406
2432
|
},
|
|
2407
2433
|
)
|
|
2408
2434
|
.on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
|
|
2435
|
+
this.cancelPendingTrackAdded(publication.trackSid);
|
|
2409
2436
|
this.emit(RoomEvent.TrackUnpublished, publication, participant);
|
|
2410
2437
|
})
|
|
2411
2438
|
.on(
|
|
@@ -2505,10 +2532,39 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2505
2532
|
}
|
|
2506
2533
|
}
|
|
2507
2534
|
|
|
2535
|
+
/** The client capabilities this SDK advertises to other participants in its `ClientInfo`. */
|
|
2536
|
+
private getClientInfoCapabilities(
|
|
2537
|
+
roomOptions: InternalRoomOptions,
|
|
2538
|
+
): Array<ClientInfo_Capability> {
|
|
2539
|
+
const capabilities: Array<ClientInfo_Capability> = [];
|
|
2540
|
+
if (
|
|
2541
|
+
isFrameMetadataSupported(roomOptions.frameMetadata ?? roomOptions.packetTrailer) ||
|
|
2542
|
+
!!this.e2eeManager
|
|
2543
|
+
) {
|
|
2544
|
+
capabilities.push(ClientInfo_Capability.CAP_PACKET_TRAILER);
|
|
2545
|
+
}
|
|
2546
|
+
// Advertise deflate-raw decompression support so peers know they can send us compressed data
|
|
2547
|
+
// streams (gated separately from clientProtocol — see the data streams v2 spec).
|
|
2548
|
+
if (isCompressionStreamSupported()) {
|
|
2549
|
+
capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
|
|
2550
|
+
}
|
|
2551
|
+
return capabilities;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2508
2554
|
private getRemoteParticipantClientProtocol = (identity: Participant['identity']) => {
|
|
2509
2555
|
return this.remoteParticipants.get(identity)?.clientProtocol ?? CLIENT_PROTOCOL_DEFAULT;
|
|
2510
2556
|
};
|
|
2511
2557
|
|
|
2558
|
+
private getRemoteParticipantCapabilities = (
|
|
2559
|
+
identity: Participant['identity'],
|
|
2560
|
+
): Array<ClientInfo_Capability> => {
|
|
2561
|
+
return this.remoteParticipants.get(identity)?.capabilities ?? [];
|
|
2562
|
+
};
|
|
2563
|
+
|
|
2564
|
+
private getAllRemoteParticipantIdentities = () => {
|
|
2565
|
+
return Array.from(this.remoteParticipants.keys());
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2512
2568
|
private registerRpcDataStreamHandler() {
|
|
2513
2569
|
this.incomingDataStreamManager.registerTextStreamHandler(
|
|
2514
2570
|
RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
@@ -2549,11 +2605,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2549
2605
|
: undefined,
|
|
2550
2606
|
});
|
|
2551
2607
|
if (consecutiveFailures >= 3) {
|
|
2552
|
-
this.
|
|
2553
|
-
this.
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2608
|
+
this.clearConnectionReconcile();
|
|
2609
|
+
if (this.engine && !this.engine.isClosed) {
|
|
2610
|
+
// The transport silently died while we still looked connected. Try a full reconnect
|
|
2611
|
+
// (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
|
|
2612
|
+
this.log.warn('detected connection state mismatch, attempting full reconnect');
|
|
2613
|
+
this.engine.reconnect();
|
|
2614
|
+
} else {
|
|
2615
|
+
// No usable engine to reconnect with; tear down.
|
|
2616
|
+
this.recreateEngine();
|
|
2617
|
+
this.handleDisconnect(
|
|
2618
|
+
this.options.stopLocalTrackOnUnpublish,
|
|
2619
|
+
DisconnectReason.STATE_MISMATCH,
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2557
2622
|
}
|
|
2558
2623
|
} else {
|
|
2559
2624
|
consecutiveFailures = 0;
|
|
@@ -2575,7 +2640,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
2575
2640
|
this.log.info(`connection state changed: ${this.state} -> ${state}`);
|
|
2576
2641
|
this.state = state;
|
|
2577
2642
|
this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
|
|
2643
|
+
|
|
2578
2644
|
this.emit(RoomEvent.ConnectionStateChanged, this.state);
|
|
2645
|
+
|
|
2579
2646
|
return true;
|
|
2580
2647
|
}
|
|
2581
2648
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { PCTransportManager } from '../PCTransportManager';
|
|
3
|
+
import { UnexpectedConnectionState } from '../errors';
|
|
4
|
+
import { DataChannelManager, type DataChannelManagerOptions } from './DataChannelManager';
|
|
5
|
+
import { DataChannelKind } from './types';
|
|
6
|
+
|
|
7
|
+
class FakeDataChannel extends EventTarget {
|
|
8
|
+
bufferedAmount = 0;
|
|
9
|
+
|
|
10
|
+
bufferedAmountLowThreshold = 0;
|
|
11
|
+
|
|
12
|
+
onmessage: ((message: MessageEvent) => void) | null = null;
|
|
13
|
+
|
|
14
|
+
onerror: ((event: Event) => void) | null = null;
|
|
15
|
+
|
|
16
|
+
onclose: (() => void) | null = null;
|
|
17
|
+
|
|
18
|
+
onbufferedamountlow: (() => void) | null = null;
|
|
19
|
+
|
|
20
|
+
send = vi.fn();
|
|
21
|
+
|
|
22
|
+
close = vi.fn();
|
|
23
|
+
|
|
24
|
+
constructor(public label: string) {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
30
|
+
|
|
31
|
+
function makeManager(overrides?: Partial<DataChannelManagerOptions>) {
|
|
32
|
+
const opts = {
|
|
33
|
+
isEngineClosed: () => false,
|
|
34
|
+
isReconnecting: () => false,
|
|
35
|
+
onDataMessage: vi.fn(),
|
|
36
|
+
onDataTrackMessage: vi.fn(),
|
|
37
|
+
onDataError: vi.fn(),
|
|
38
|
+
onChannelClose: vi.fn(),
|
|
39
|
+
onBufferStatusChanged: vi.fn(),
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
const manager = new DataChannelManager(opts);
|
|
43
|
+
const created: Record<string, FakeDataChannel> = {};
|
|
44
|
+
const pcManager = {
|
|
45
|
+
createPublisherDataChannel: vi.fn((label: string) => {
|
|
46
|
+
const dc = new FakeDataChannel(label);
|
|
47
|
+
created[label] = dc;
|
|
48
|
+
return dc as unknown as RTCDataChannel;
|
|
49
|
+
}),
|
|
50
|
+
} as unknown as PCTransportManager;
|
|
51
|
+
return { manager, opts, pcManager, created };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('DataChannelManager', () => {
|
|
55
|
+
it('creates and wires the three publisher channels', () => {
|
|
56
|
+
const { manager, opts, pcManager, created } = makeManager();
|
|
57
|
+
|
|
58
|
+
manager.createPublisherChannels(pcManager);
|
|
59
|
+
|
|
60
|
+
expect(Object.keys(created).sort()).toEqual(['_data_track', '_lossy', '_reliable']);
|
|
61
|
+
expect(manager.getHandle(DataChannelKind.RELIABLE)).toBe(created._reliable);
|
|
62
|
+
expect(manager.hasPublisherChannels).toBe(true);
|
|
63
|
+
for (const dc of Object.values(created)) {
|
|
64
|
+
expect(dc.onmessage).toBeTruthy();
|
|
65
|
+
expect(dc.onerror).toBeTruthy();
|
|
66
|
+
expect(dc.onclose).toBeTruthy();
|
|
67
|
+
expect(dc.onbufferedamountlow).toBeTruthy();
|
|
68
|
+
expect(dc.bufferedAmountLowThreshold).toBeGreaterThan(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Close handler reports the right kind.
|
|
72
|
+
created._reliable.onclose!();
|
|
73
|
+
expect(opts.onChannelClose).toHaveBeenCalledWith(DataChannelKind.RELIABLE);
|
|
74
|
+
|
|
75
|
+
// A drain event on the data-track channel refreshes its status; since the buffer starts empty
|
|
76
|
+
// (below the low mark) and the wrapper's initial status is already "low", no change fires —
|
|
77
|
+
// fill it first so the drain is an observable not-low → low transition.
|
|
78
|
+
const dataTrackDc = created._data_track;
|
|
79
|
+
dataTrackDc.bufferedAmount = 10 * 1024 * 1024;
|
|
80
|
+
dataTrackDc.onbufferedamountlow!(); // refresh: now "not low"
|
|
81
|
+
dataTrackDc.bufferedAmount = 0;
|
|
82
|
+
dataTrackDc.onbufferedamountlow!(); // refresh: back to "low"
|
|
83
|
+
expect(opts.onBufferStatusChanged).toHaveBeenCalledWith(
|
|
84
|
+
DataChannelKind.DATA_TRACK_LOSSY,
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
expect(opts.onBufferStatusChanged).toHaveBeenCalledWith(DataChannelKind.DATA_TRACK_LOSSY, true);
|
|
88
|
+
|
|
89
|
+
manager.lossy.stopThresholdTuning();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('recreating channels rejects waiters parked on the replaced handles', async () => {
|
|
93
|
+
const { manager, pcManager, created } = makeManager();
|
|
94
|
+
manager.createPublisherChannels(pcManager);
|
|
95
|
+
|
|
96
|
+
// Park a reliable sender on the first-generation channel.
|
|
97
|
+
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
98
|
+
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
99
|
+
parked.catch(() => {});
|
|
100
|
+
await tick();
|
|
101
|
+
|
|
102
|
+
// Safari null-id path: channels recreated without closing the old ones.
|
|
103
|
+
manager.createPublisherChannels(pcManager);
|
|
104
|
+
|
|
105
|
+
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
106
|
+
// The gate recovers against the fresh (empty) channel.
|
|
107
|
+
await expect(manager.reliable.waitForHeadroomWithLock()).resolves.toBeUndefined();
|
|
108
|
+
|
|
109
|
+
manager.lossy.stopThresholdTuning();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('adopts subscriber channels by label and rejects unknown labels', () => {
|
|
113
|
+
const { manager, opts } = makeManager();
|
|
114
|
+
|
|
115
|
+
const sub = new FakeDataChannel('_reliable');
|
|
116
|
+
expect(manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel)).toBe(true);
|
|
117
|
+
expect(manager.getHandle(DataChannelKind.RELIABLE, true)).toBe(sub);
|
|
118
|
+
expect(sub.onmessage).toBe(opts.onDataMessage);
|
|
119
|
+
|
|
120
|
+
const dataTrackSub = new FakeDataChannel('_data_track');
|
|
121
|
+
expect(manager.adoptSubscriberChannel(dataTrackSub as unknown as RTCDataChannel)).toBe(true);
|
|
122
|
+
expect(dataTrackSub.onmessage).toBe(opts.onDataTrackMessage);
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
manager.adoptSubscriberChannel(new FakeDataChannel('_other') as unknown as RTCDataChannel),
|
|
126
|
+
).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('teardown rejects parked waiters, closes all handles, and detaches', async () => {
|
|
130
|
+
const { manager, pcManager, created } = makeManager();
|
|
131
|
+
manager.createPublisherChannels(pcManager);
|
|
132
|
+
const sub = new FakeDataChannel('_lossy');
|
|
133
|
+
manager.adoptSubscriberChannel(sub as unknown as RTCDataChannel);
|
|
134
|
+
|
|
135
|
+
created._reliable.bufferedAmount = 2 * 1024 * 1024;
|
|
136
|
+
const parked = manager.reliable.waitForHeadroomWithLock();
|
|
137
|
+
parked.catch(() => {});
|
|
138
|
+
await tick();
|
|
139
|
+
|
|
140
|
+
manager.teardown();
|
|
141
|
+
|
|
142
|
+
await expect(parked).rejects.toBeInstanceOf(UnexpectedConnectionState);
|
|
143
|
+
for (const dc of [...Object.values(created), sub]) {
|
|
144
|
+
expect(dc.close).toHaveBeenCalled();
|
|
145
|
+
expect(dc.onmessage).toBeNull();
|
|
146
|
+
}
|
|
147
|
+
expect(manager.hasPublisherChannels).toBe(false);
|
|
148
|
+
expect(manager.getHandle(DataChannelKind.LOSSY, true)).toBeUndefined();
|
|
149
|
+
|
|
150
|
+
manager.lossy.stopThresholdTuning();
|
|
151
|
+
});
|
|
152
|
+
});
|