livekit-client 2.19.2 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +23 -23
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +179 -111
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +2 -0
- package/dist/{livekit-client.pt.worker.js.map → livekit-client.fm.worker.js.map} +1 -1
- package/dist/{livekit-client.pt.worker.mjs → livekit-client.fm.worker.mjs} +4 -4
- package/dist/livekit-client.fm.worker.mjs.map +1 -0
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +5 -5
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +8 -8
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
- package/dist/src/{packetTrailer/PacketTrailerManager.d.ts.map → frameMetadata/FrameMetadataManager.d.ts.map} +1 -1
- package/dist/src/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +7 -7
- package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -0
- package/dist/src/{packetTrailer → frameMetadata}/types.d.ts +10 -6
- package/dist/src/frameMetadata/types.d.ts.map +1 -0
- package/dist/src/frameMetadata/utils.d.ts +9 -0
- package/dist/src/{packetTrailer → frameMetadata}/utils.d.ts.map +1 -1
- package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
- package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +8 -4
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +3 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +2 -2
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +2 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/token-source/types.d.ts +1 -0
- package/dist/src/room/token-source/types.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/FrameMetadataExtractor.d.ts +19 -0
- package/dist/src/room/track/FrameMetadataExtractor.d.ts.map +1 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts +7 -7
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +17 -5
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -1
- package/dist/ts4.2/e2ee/types.d.ts +5 -5
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +8 -8
- package/dist/ts4.2/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
- package/dist/ts4.2/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +7 -7
- package/dist/ts4.2/{packetTrailer → frameMetadata}/types.d.ts +10 -6
- package/dist/ts4.2/frameMetadata/utils.d.ts +9 -0
- package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
- package/dist/ts4.2/index.d.ts +9 -3
- package/dist/ts4.2/options.d.ts +8 -4
- package/dist/ts4.2/room/PCTransport.d.ts +1 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +3 -1
- package/dist/ts4.2/room/Room.d.ts +2 -2
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +2 -2
- package/dist/ts4.2/room/token-source/types.d.ts +1 -0
- package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -0
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +7 -7
- package/dist/ts4.2/room/track/options.d.ts +17 -5
- package/package.json +23 -18
- package/src/api/SignalClient.test.ts +4 -4
- package/src/api/SignalClient.ts +7 -3
- package/src/api/WebSocketStream.test.ts +7 -7
- package/src/e2ee/E2eeManager.ts +13 -11
- package/src/e2ee/types.ts +5 -5
- package/src/e2ee/worker/FrameCryptor.test.ts +5 -5
- package/src/e2ee/worker/FrameCryptor.ts +27 -23
- package/src/e2ee/worker/e2ee.worker.ts +5 -5
- package/src/e2ee/worker/tsconfig.json +4 -1
- package/src/{packetTrailer/PacketTrailerManager.test.ts → frameMetadata/FrameMetadataManager.test.ts} +10 -8
- package/src/{packetTrailer/PacketTrailerManager.ts → frameMetadata/FrameMetadataManager.ts} +21 -15
- package/src/{packetTrailer/packetTrailer.test.ts → frameMetadata/frameMetadata.test.ts} +1 -1
- package/src/{packetTrailer/packetTrailer.ts → frameMetadata/frameMetadata.ts} +9 -9
- package/src/{packetTrailer → frameMetadata}/types.ts +12 -6
- package/src/{packetTrailer → frameMetadata}/utils.test.ts +19 -19
- package/src/{packetTrailer → frameMetadata}/utils.ts +11 -11
- package/src/{packetTrailer/worker/packetTrailer.worker.ts → frameMetadata/worker/frameMetadata.worker.ts} +5 -5
- package/src/{packetTrailer → frameMetadata}/worker/tsconfig.json +4 -1
- package/src/index.ts +13 -2
- package/src/options.ts +9 -4
- package/src/room/PCTransport.ts +4 -0
- package/src/room/PCTransportManager.ts +4 -0
- package/src/room/RTCEngine.test.ts +143 -9
- package/src/room/RTCEngine.ts +111 -31
- package/src/room/Room.ts +10 -10
- package/src/room/participant/LocalParticipant.test.ts +19 -19
- package/src/room/participant/LocalParticipant.ts +21 -16
- package/src/room/participant/publishUtils.test.ts +6 -0
- package/src/room/participant/publishUtils.ts +4 -1
- package/src/room/rpc/client/RpcClientManager.ts +1 -1
- package/src/room/rpc/server/RpcServerManager.ts +1 -1
- package/src/room/token-source/TokenSource.ts +9 -23
- package/src/room/token-source/types.ts +1 -0
- package/src/room/token-source/utils.ts +1 -0
- package/src/room/track/{PacketTrailerExtractor.ts → FrameMetadataExtractor.ts} +8 -8
- package/src/room/track/RemoteVideoTrack.ts +7 -11
- package/src/room/track/options.ts +19 -5
- package/dist/livekit-client.pt.worker.js +0 -2
- package/dist/livekit-client.pt.worker.mjs.map +0 -1
- package/dist/src/packetTrailer/packetTrailer.d.ts.map +0 -1
- package/dist/src/packetTrailer/types.d.ts.map +0 -1
- package/dist/src/packetTrailer/utils.d.ts +0 -9
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +0 -1
- package/dist/src/room/track/PacketTrailerExtractor.d.ts +0 -19
- package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +0 -1
- package/dist/ts4.2/packetTrailer/utils.d.ts +0 -9
- package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
- package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +0 -19
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { DataPacket, DataPacket_Kind, UserPacket } from '@livekit/protocol';
|
|
1
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import RTCEngine from './RTCEngine';
|
|
3
|
+
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
3
4
|
import { roomOptionDefaults } from './defaults';
|
|
5
|
+
import { PublishDataError } from './errors';
|
|
4
6
|
|
|
5
7
|
describe('RTCEngine', () => {
|
|
6
8
|
const originalRTCRtpSender = window.RTCRtpSender;
|
|
@@ -56,12 +58,12 @@ describe('RTCEngine', () => {
|
|
|
56
58
|
).makeRTCConfiguration();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function
|
|
61
|
+
function setupFrameMetadataSender(engine: RTCEngine, sender: RTCRtpSender, opts = {}) {
|
|
60
62
|
(
|
|
61
63
|
engine as unknown as {
|
|
62
|
-
|
|
64
|
+
setupFrameMetadataSender: (sender: RTCRtpSender, opts?: unknown) => void;
|
|
63
65
|
}
|
|
64
|
-
).
|
|
66
|
+
).setupFrameMetadataSender(sender, opts);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
it('does not enable encoded insertable streams without E2EE or a packet trailer worker', () => {
|
|
@@ -128,7 +130,7 @@ describe('RTCEngine', () => {
|
|
|
128
130
|
createEncodedStreams,
|
|
129
131
|
} as unknown as RTCRtpSender;
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
setupFrameMetadataSender(engine, sender);
|
|
132
134
|
|
|
133
135
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
134
136
|
});
|
|
@@ -145,7 +147,7 @@ describe('RTCEngine', () => {
|
|
|
145
147
|
createEncodedStreams,
|
|
146
148
|
} as unknown as RTCRtpSender;
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
setupFrameMetadataSender(engine, sender);
|
|
149
151
|
|
|
150
152
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
151
153
|
});
|
|
@@ -165,7 +167,7 @@ describe('RTCEngine', () => {
|
|
|
165
167
|
createEncodedStreams,
|
|
166
168
|
} as unknown as RTCRtpSender;
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
setupFrameMetadataSender(engine, sender, { packetTrailer: { timestamp: true, frameId: true } });
|
|
169
171
|
|
|
170
172
|
expect(createEncodedStreams).toHaveBeenCalledTimes(1);
|
|
171
173
|
expect(worker.postMessage).toHaveBeenCalledWith(
|
|
@@ -185,7 +187,9 @@ describe('RTCEngine', () => {
|
|
|
185
187
|
stubScriptTransformSupport();
|
|
186
188
|
|
|
187
189
|
const transform = {};
|
|
188
|
-
const RTCRtpScriptTransform = vi.fn(()
|
|
190
|
+
const RTCRtpScriptTransform = vi.fn(function () {
|
|
191
|
+
return transform;
|
|
192
|
+
});
|
|
189
193
|
Object.defineProperty(window, 'RTCRtpScriptTransform', {
|
|
190
194
|
configurable: true,
|
|
191
195
|
value: RTCRtpScriptTransform,
|
|
@@ -207,7 +211,7 @@ describe('RTCEngine', () => {
|
|
|
207
211
|
createEncodedStreams,
|
|
208
212
|
} as unknown as RTCRtpSender;
|
|
209
213
|
|
|
210
|
-
|
|
214
|
+
setupFrameMetadataSender(engine, sender, { packetTrailer: { timestamp: true } });
|
|
211
215
|
|
|
212
216
|
expect(RTCRtpScriptTransform).toHaveBeenCalledWith(worker, {
|
|
213
217
|
kind: 'encode',
|
|
@@ -216,4 +220,134 @@ describe('RTCEngine', () => {
|
|
|
216
220
|
expect((sender as unknown as { transform: unknown }).transform).toBe(transform);
|
|
217
221
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
218
222
|
});
|
|
223
|
+
|
|
224
|
+
describe('sendDataPacket', () => {
|
|
225
|
+
const MAX_DATA_PACKET_SIZE = 64 * 1024 - 1; // 65535 bytes (64 KB - 1)
|
|
226
|
+
function stubConnectedEngine(
|
|
227
|
+
engine: RTCEngine,
|
|
228
|
+
maxDataPacketSize: number = MAX_DATA_PACKET_SIZE,
|
|
229
|
+
) {
|
|
230
|
+
const send = vi.fn();
|
|
231
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
232
|
+
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
233
|
+
waitForBufferStatusLow: vi.fn().mockResolvedValue(undefined),
|
|
234
|
+
updateAndEmitDCBufferStatus: vi.fn(),
|
|
235
|
+
dataChannelForKind: vi.fn(() => ({ send })),
|
|
236
|
+
pcManager: {
|
|
237
|
+
getMaxPublisherMessageSize: vi.fn(() => maxDataPacketSize),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
return send;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it('rejects packets larger than the max data packet size', async () => {
|
|
244
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
245
|
+
const send = stubConnectedEngine(engine);
|
|
246
|
+
|
|
247
|
+
// The serialized packet includes protobuf framing on top of the payload, so a payload at the
|
|
248
|
+
// limit is already guaranteed to exceed it once serialized.
|
|
249
|
+
const packet = new DataPacket({
|
|
250
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
251
|
+
value: {
|
|
252
|
+
case: 'user',
|
|
253
|
+
value: new UserPacket({ payload: new Uint8Array(MAX_DATA_PACKET_SIZE) }),
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await expect(engine.sendDataPacket(packet, DataChannelKind.RELIABLE)).rejects.toBeInstanceOf(
|
|
258
|
+
PublishDataError,
|
|
259
|
+
);
|
|
260
|
+
expect(send).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('does not reject packets if the max data packet size is 0', async () => {
|
|
264
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
265
|
+
const send = stubConnectedEngine(engine, 0);
|
|
266
|
+
|
|
267
|
+
const packet = new DataPacket({
|
|
268
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
269
|
+
value: {
|
|
270
|
+
case: 'user',
|
|
271
|
+
value: new UserPacket({ payload: new Uint8Array(100) }),
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Sending the packet should succeed, there isn't a size limit
|
|
276
|
+
await expect(
|
|
277
|
+
engine.sendDataPacket(packet, DataChannelKind.RELIABLE),
|
|
278
|
+
).resolves.toBeUndefined();
|
|
279
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('sends packets within the max data packet size', async () => {
|
|
283
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
284
|
+
const send = stubConnectedEngine(engine);
|
|
285
|
+
|
|
286
|
+
const packet = new DataPacket({
|
|
287
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
288
|
+
value: {
|
|
289
|
+
case: 'user',
|
|
290
|
+
value: new UserPacket({ payload: new Uint8Array(1024) }),
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await expect(
|
|
295
|
+
engine.sendDataPacket(packet, DataChannelKind.RELIABLE),
|
|
296
|
+
).resolves.toBeUndefined();
|
|
297
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('handleDataChannelClose', () => {
|
|
302
|
+
function stubCloseEnv(
|
|
303
|
+
engine: RTCEngine,
|
|
304
|
+
{ closed, publisherState }: { closed: boolean; publisherState: RTCPeerConnectionState },
|
|
305
|
+
) {
|
|
306
|
+
const error = vi.fn();
|
|
307
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
308
|
+
_isClosed: closed,
|
|
309
|
+
log: { error },
|
|
310
|
+
pcManager: {
|
|
311
|
+
publisher: { getConnectionState: () => publisherState },
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
return error;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function fireClose(engine: RTCEngine, kind: DataChannelKind) {
|
|
318
|
+
(
|
|
319
|
+
engine as unknown as {
|
|
320
|
+
handleDataChannelClose: (kind: DataChannelKind) => () => void;
|
|
321
|
+
}
|
|
322
|
+
).handleDataChannelClose(kind)();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
it('logs an error when a publisher channel closes while connected', () => {
|
|
326
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
327
|
+
const error = stubCloseEnv(engine, { closed: false, publisherState: 'connected' });
|
|
328
|
+
|
|
329
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
330
|
+
|
|
331
|
+
expect(error).toHaveBeenCalledOnce();
|
|
332
|
+
expect(error.mock.calls[0][0]).toContain('RELIABLE');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('stays quiet when the engine is already closed', () => {
|
|
336
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
337
|
+
const error = stubCloseEnv(engine, { closed: true, publisherState: 'connected' });
|
|
338
|
+
|
|
339
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
340
|
+
|
|
341
|
+
expect(error).not.toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('stays quiet when the publisher PC is no longer connected', () => {
|
|
345
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
346
|
+
const error = stubCloseEnv(engine, { closed: false, publisherState: 'closed' });
|
|
347
|
+
|
|
348
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
349
|
+
|
|
350
|
+
expect(error).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
219
353
|
});
|
package/src/room/RTCEngine.ts
CHANGED
|
@@ -54,13 +54,13 @@ import {
|
|
|
54
54
|
} from '../api/SignalClient';
|
|
55
55
|
import type { BaseE2EEManager } from '../e2ee/E2eeManager';
|
|
56
56
|
import { asEncryptablePacket, isInsertableStreamSupported } from '../e2ee/utils';
|
|
57
|
+
import {
|
|
58
|
+
hasFrameMetadataPublishOptions,
|
|
59
|
+
isFrameMetadataSupported,
|
|
60
|
+
shouldUseFrameMetadataScriptTransform,
|
|
61
|
+
} from '../frameMetadata/utils';
|
|
57
62
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
58
63
|
import type { InternalRoomOptions } from '../options';
|
|
59
|
-
import {
|
|
60
|
-
hasPacketTrailerPublishOptions,
|
|
61
|
-
isPacketTrailerSupported,
|
|
62
|
-
shouldUsePacketTrailerScriptTransform,
|
|
63
|
-
} from '../packetTrailer/utils';
|
|
64
64
|
import TypedPromise from '../utils/TypedPromise';
|
|
65
65
|
import { DataPacketBuffer } from '../utils/dataPacketBuffer';
|
|
66
66
|
import { TTLMap } from '../utils/ttlmap';
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
ConnectionError,
|
|
74
74
|
ConnectionErrorReason,
|
|
75
75
|
NegotiationError,
|
|
76
|
+
PublishDataError,
|
|
76
77
|
SignalReconnectError,
|
|
77
78
|
TrackInvalidError,
|
|
78
79
|
UnexpectedConnectionState,
|
|
@@ -91,6 +92,7 @@ import type { LoggerOptions } from './types';
|
|
|
91
92
|
import {
|
|
92
93
|
Future,
|
|
93
94
|
isPublisherOfferWithJoinSupported,
|
|
95
|
+
isReactNative,
|
|
94
96
|
isVideoCodec,
|
|
95
97
|
isVideoTrack,
|
|
96
98
|
isWeb,
|
|
@@ -108,6 +110,7 @@ const leaveReconnect = 'leave-reconnect';
|
|
|
108
110
|
const reliabeReceiveStateTTL = 30_000;
|
|
109
111
|
const lossyDataChannelBufferThresholdMin = 8 * 1024;
|
|
110
112
|
const lossyDataChannelBufferThresholdMax = 256 * 1024;
|
|
113
|
+
|
|
111
114
|
const initialMediaSectionsAudio = 3;
|
|
112
115
|
const initialMediaSectionsVideo = 3;
|
|
113
116
|
|
|
@@ -125,6 +128,11 @@ export enum DataChannelKind {
|
|
|
125
128
|
DATA_TRACK_LOSSY = 2,
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
// Default data-channel max message size (bytes), used when the remote SDP
|
|
132
|
+
// answer does not advertise an `a=max-message-size` attribute (RFC 8841).
|
|
133
|
+
// `0` means "no limit".
|
|
134
|
+
const DEFAULT_MAX_MESSAGE_SIZE = 64_000;
|
|
135
|
+
|
|
128
136
|
/** @internal */
|
|
129
137
|
export default class RTCEngine extends (EventEmitter as new () => TypedEventEmitter<EngineEventCallbacks>) {
|
|
130
138
|
client: SignalClient;
|
|
@@ -449,18 +457,25 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
449
457
|
}
|
|
450
458
|
|
|
451
459
|
async cleanupPeerConnections() {
|
|
452
|
-
await this.pcManager?.close();
|
|
453
|
-
this.pcManager = undefined;
|
|
454
|
-
|
|
455
460
|
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
|
456
|
-
if (!dc)
|
|
457
|
-
|
|
461
|
+
if (!dc) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
466
|
+
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
467
|
+
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
468
|
+
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
469
|
+
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
470
|
+
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
458
471
|
dc.onbufferedamountlow = null;
|
|
459
472
|
dc.onclose = null;
|
|
460
473
|
dc.onclosing = null;
|
|
461
474
|
dc.onerror = null;
|
|
462
475
|
dc.onmessage = null;
|
|
463
476
|
dc.onopen = null;
|
|
477
|
+
|
|
478
|
+
dc.close();
|
|
464
479
|
};
|
|
465
480
|
dcCleanup(this.lossyDC);
|
|
466
481
|
dcCleanup(this.lossyDCSub);
|
|
@@ -469,6 +484,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
469
484
|
dcCleanup(this.dataTrackDC);
|
|
470
485
|
dcCleanup(this.dataTrackDCSub);
|
|
471
486
|
|
|
487
|
+
await this.pcManager?.close();
|
|
488
|
+
this.pcManager = undefined;
|
|
489
|
+
|
|
472
490
|
this.lossyDC = undefined;
|
|
473
491
|
this.lossyDCSub = undefined;
|
|
474
492
|
this.reliableDC = undefined;
|
|
@@ -788,7 +806,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
788
806
|
// used; RTCRtpScriptTransform does not need the PeerConnection flag.
|
|
789
807
|
const needsInsertableStreams =
|
|
790
808
|
this.signalOpts?.e2eeEnabled ||
|
|
791
|
-
(this.
|
|
809
|
+
(this.frameMetadataWorker && !shouldUseFrameMetadataScriptTransform());
|
|
792
810
|
|
|
793
811
|
if (needsInsertableStreams && isInsertableStreamSupported()) {
|
|
794
812
|
this.log.debug('E2EE - setting up transports with insertable streams');
|
|
@@ -840,7 +858,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
840
858
|
*/
|
|
841
859
|
private applyInitialPublisherLayout() {
|
|
842
860
|
this.createDataChannels();
|
|
843
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Native libwebrtc does not support pre-populating the media sections,
|
|
863
|
+
* so we skip it for React Native.
|
|
864
|
+
*
|
|
865
|
+
* Related: https://github.com/livekit/rust-sdks/pull/1151
|
|
866
|
+
*/
|
|
867
|
+
if (!isReactNative()) {
|
|
868
|
+
this.addMediaSections(initialMediaSectionsAudio, initialMediaSectionsVideo);
|
|
869
|
+
}
|
|
844
870
|
}
|
|
845
871
|
|
|
846
872
|
private addMediaSections(numAudios: number, numVideos: number) {
|
|
@@ -862,14 +888,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
862
888
|
if (this.lossyDC) {
|
|
863
889
|
this.lossyDC.onmessage = null;
|
|
864
890
|
this.lossyDC.onerror = null;
|
|
891
|
+
this.lossyDC.onclose = null;
|
|
865
892
|
}
|
|
866
893
|
if (this.reliableDC) {
|
|
867
894
|
this.reliableDC.onmessage = null;
|
|
868
895
|
this.reliableDC.onerror = null;
|
|
896
|
+
this.reliableDC.onclose = null;
|
|
869
897
|
}
|
|
870
898
|
if (this.dataTrackDC) {
|
|
871
899
|
this.dataTrackDC.onmessage = null;
|
|
872
900
|
this.dataTrackDC.onerror = null;
|
|
901
|
+
this.dataTrackDC.onclose = null;
|
|
873
902
|
}
|
|
874
903
|
|
|
875
904
|
// create data channels
|
|
@@ -895,6 +924,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
895
924
|
this.reliableDC.onerror = this.handleDataError;
|
|
896
925
|
this.dataTrackDC.onerror = this.handleDataError;
|
|
897
926
|
|
|
927
|
+
// detect unexpected publisher data channel closes
|
|
928
|
+
this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
|
|
929
|
+
this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
|
|
930
|
+
this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
|
|
931
|
+
|
|
898
932
|
// set up dc buffer threshold, set to 64kB (otherwise 0 by default)
|
|
899
933
|
this.lossyDC.bufferedAmountLowThreshold = 65535;
|
|
900
934
|
this.reliableDC.bufferedAmountLowThreshold = 65535;
|
|
@@ -1023,17 +1057,41 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1023
1057
|
};
|
|
1024
1058
|
|
|
1025
1059
|
private handleDataError = (event: Event) => {
|
|
1060
|
+
// Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as
|
|
1061
|
+
// the peer connection closes) carry no actionable information — the channel is going away
|
|
1062
|
+
// regardless. Suppress them so a graceful disconnect doesn't surface spurious errors.
|
|
1063
|
+
// See livekit/client-sdk-js#1953.
|
|
1064
|
+
if (this._isClosed) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1026
1068
|
const channel = event.currentTarget as RTCDataChannel;
|
|
1027
1069
|
const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable';
|
|
1028
1070
|
|
|
1029
|
-
if (event instanceof
|
|
1030
|
-
const { error } = event
|
|
1031
|
-
this.log.error(`DataChannel error on ${channelKind}: ${
|
|
1071
|
+
if (typeof RTCErrorEvent !== 'undefined' && event instanceof RTCErrorEvent && event.error) {
|
|
1072
|
+
const { error } = event;
|
|
1073
|
+
this.log.error(`DataChannel error on ${channelKind}: ${error.message}`, {
|
|
1074
|
+
error,
|
|
1075
|
+
errorDetail: error.errorDetail,
|
|
1076
|
+
sctpCauseCode: error.sctpCauseCode,
|
|
1077
|
+
});
|
|
1032
1078
|
} else {
|
|
1033
1079
|
this.log.error(`Unknown DataChannel error on ${channelKind}`, { event });
|
|
1034
1080
|
}
|
|
1035
1081
|
};
|
|
1036
1082
|
|
|
1083
|
+
private handleDataChannelClose = (kind: DataChannelKind) => () => {
|
|
1084
|
+
// A publisher DC closing while the session is up and the publisher PC is still
|
|
1085
|
+
// connected is the signature of an oversized message having aborted the channel
|
|
1086
|
+
// (see livekit/rust-sdks#1137). Surface it; do not attempt renegotiation.
|
|
1087
|
+
if (!this._isClosed && this.pcManager?.publisher.getConnectionState() === 'connected') {
|
|
1088
|
+
this.log.error(
|
|
1089
|
+
`publisher data channel '${DataChannelKind[kind]}' closed unexpectedly`,
|
|
1090
|
+
this.logContext,
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1037
1095
|
private handleBufferedAmountLow = (channelKind: DataChannelKind) => {
|
|
1038
1096
|
this.updateAndEmitDCBufferStatus(channelKind);
|
|
1039
1097
|
};
|
|
@@ -1052,7 +1110,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1052
1110
|
} else {
|
|
1053
1111
|
throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
|
|
1054
1112
|
}
|
|
1055
|
-
this.
|
|
1113
|
+
this.setupFrameMetadataSender(sender, opts);
|
|
1056
1114
|
return sender;
|
|
1057
1115
|
}
|
|
1058
1116
|
|
|
@@ -1072,50 +1130,55 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1072
1130
|
throw new UnexpectedConnectionState('Cannot stream on this device');
|
|
1073
1131
|
}
|
|
1074
1132
|
if (sender) {
|
|
1075
|
-
this.
|
|
1133
|
+
this.setupFrameMetadataSender(sender, opts);
|
|
1076
1134
|
}
|
|
1077
1135
|
return sender;
|
|
1078
1136
|
}
|
|
1079
1137
|
|
|
1080
|
-
private
|
|
1081
|
-
|
|
1138
|
+
private get frameMetadataWorker(): Worker | undefined {
|
|
1139
|
+
return (this.options.frameMetadata ?? this.options.packetTrailer)?.worker;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
private setupFrameMetadataSender(sender: RTCRtpSender, opts: TrackPublishOptions = {}) {
|
|
1143
|
+
const worker = this.frameMetadataWorker;
|
|
1144
|
+
if (!worker || this.signalOpts?.e2eeEnabled) {
|
|
1082
1145
|
return;
|
|
1083
1146
|
}
|
|
1084
1147
|
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1148
|
+
const frameMetadata = opts.frameMetadata ?? opts.packetTrailer;
|
|
1149
|
+
const hasMetadata = hasFrameMetadataPublishOptions(frameMetadata);
|
|
1087
1150
|
|
|
1088
|
-
if (
|
|
1089
|
-
if (
|
|
1151
|
+
if (shouldUseFrameMetadataScriptTransform()) {
|
|
1152
|
+
if (hasMetadata) {
|
|
1090
1153
|
// @ts-ignore
|
|
1091
|
-
sender.transform = new RTCRtpScriptTransform(
|
|
1154
|
+
sender.transform = new RTCRtpScriptTransform(worker, {
|
|
1092
1155
|
kind: 'encode',
|
|
1093
|
-
packetTrailer,
|
|
1156
|
+
packetTrailer: frameMetadata,
|
|
1094
1157
|
});
|
|
1095
1158
|
}
|
|
1096
1159
|
return;
|
|
1097
1160
|
}
|
|
1098
1161
|
|
|
1099
1162
|
if (
|
|
1100
|
-
!
|
|
1163
|
+
!isFrameMetadataSupported(this.options.frameMetadata ?? this.options.packetTrailer) ||
|
|
1101
1164
|
!('createEncodedStreams' in sender)
|
|
1102
1165
|
) {
|
|
1103
|
-
if (
|
|
1104
|
-
this.log.warn('
|
|
1166
|
+
if (hasMetadata) {
|
|
1167
|
+
this.log.warn('frame metadata transform not supported; skipping write', this.logContext);
|
|
1105
1168
|
}
|
|
1106
1169
|
return;
|
|
1107
1170
|
}
|
|
1108
1171
|
|
|
1109
1172
|
// @ts-ignore
|
|
1110
1173
|
const { readable, writable } = sender.createEncodedStreams();
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1174
|
+
if (hasMetadata) {
|
|
1175
|
+
worker.postMessage(
|
|
1113
1176
|
{
|
|
1114
1177
|
kind: 'encode',
|
|
1115
1178
|
data: {
|
|
1116
1179
|
readableStream: readable,
|
|
1117
1180
|
writableStream: writable,
|
|
1118
|
-
packetTrailer,
|
|
1181
|
+
packetTrailer: frameMetadata,
|
|
1119
1182
|
},
|
|
1120
1183
|
},
|
|
1121
1184
|
[readable, writable],
|
|
@@ -1535,6 +1598,23 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1535
1598
|
|
|
1536
1599
|
const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
|
|
1537
1600
|
|
|
1601
|
+
// Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
|
|
1602
|
+
// than LiveKit/pion can deliver end-to-end (~64 KiB), so we trust
|
|
1603
|
+
// the answer up untilthe built in ceiling.
|
|
1604
|
+
const maxPublisherMessageSizeBytes = Math.min(
|
|
1605
|
+
this.pcManager?.getMaxPublisherMessageSize() ?? DEFAULT_MAX_MESSAGE_SIZE,
|
|
1606
|
+
DEFAULT_MAX_MESSAGE_SIZE,
|
|
1607
|
+
);
|
|
1608
|
+
if (
|
|
1609
|
+
typeof maxPublisherMessageSizeBytes !== 'undefined' &&
|
|
1610
|
+
maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ &&
|
|
1611
|
+
msg.byteLength > maxPublisherMessageSizeBytes
|
|
1612
|
+
) {
|
|
1613
|
+
throw new PublishDataError(
|
|
1614
|
+
`cannot publish data packet larger than ${maxPublisherMessageSizeBytes} bytes (got ${msg.byteLength})`,
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1538
1618
|
switch (kind) {
|
|
1539
1619
|
case DataChannelKind.LOSSY:
|
|
1540
1620
|
case DataChannelKind.DATA_TRACK_LOSSY:
|
package/src/room/Room.ts
CHANGED
|
@@ -37,6 +37,8 @@ import type TypedEmitter from 'typed-emitter';
|
|
|
37
37
|
import { ensureTrailingSlash } from '../api/utils';
|
|
38
38
|
import { EncryptionEvent } from '../e2ee';
|
|
39
39
|
import { type BaseE2EEManager, E2EEManager } from '../e2ee/E2eeManager';
|
|
40
|
+
import { FrameMetadataManager } from '../frameMetadata/FrameMetadataManager';
|
|
41
|
+
import { isFrameMetadataSupported } from '../frameMetadata/utils';
|
|
40
42
|
import log, { LoggerNames, getLogger } from '../logger';
|
|
41
43
|
import type {
|
|
42
44
|
InternalRoomConnectOptions,
|
|
@@ -44,8 +46,6 @@ import type {
|
|
|
44
46
|
RoomConnectOptions,
|
|
45
47
|
RoomOptions,
|
|
46
48
|
} from '../options';
|
|
47
|
-
import { PacketTrailerManager } from '../packetTrailer/PacketTrailerManager';
|
|
48
|
-
import { isPacketTrailerSupported } from '../packetTrailer/utils';
|
|
49
49
|
import TypedPromise from '../utils/TypedPromise';
|
|
50
50
|
import { getBrowser } from '../utils/browserParser';
|
|
51
51
|
import { CLIENT_PROTOCOL_DEFAULT } from '../version';
|
|
@@ -198,7 +198,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
198
198
|
|
|
199
199
|
private e2eeManager: BaseE2EEManager | undefined;
|
|
200
200
|
|
|
201
|
-
private
|
|
201
|
+
private frameMetadataManager: FrameMetadataManager | undefined;
|
|
202
202
|
|
|
203
203
|
private e2eeStateMutex: Mutex = new Mutex();
|
|
204
204
|
|
|
@@ -343,7 +343,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
343
343
|
this.rpcServerManager,
|
|
344
344
|
);
|
|
345
345
|
|
|
346
|
-
this.
|
|
346
|
+
this.setupFrameMetadata();
|
|
347
347
|
|
|
348
348
|
if (this.options.e2ee || this.options.encryption) {
|
|
349
349
|
this.setupE2EE();
|
|
@@ -514,11 +514,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
private
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
this.packetTrailerManager.setup(this);
|
|
517
|
+
private setupFrameMetadata() {
|
|
518
|
+
const opts = this.options.frameMetadata ?? this.options.packetTrailer;
|
|
519
|
+
this.frameMetadataManager = new FrameMetadataManager(opts);
|
|
520
|
+
this.frameMetadataManager.setup(this);
|
|
522
521
|
}
|
|
523
522
|
|
|
524
523
|
private get logContext() {
|
|
@@ -972,7 +971,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
972
971
|
adaptiveStream:
|
|
973
972
|
typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
|
974
973
|
clientInfoCapabilities:
|
|
975
|
-
|
|
974
|
+
isFrameMetadataSupported(roomOptions.frameMetadata ?? roomOptions.packetTrailer) ||
|
|
975
|
+
!!this.e2eeManager
|
|
976
976
|
? [ClientInfo_Capability.CAP_PACKET_TRAILER]
|
|
977
977
|
: undefined,
|
|
978
978
|
maxRetries: connectOptions.maxRetries,
|
|
@@ -5,18 +5,18 @@ import { Track } from '../track/Track';
|
|
|
5
5
|
import type { TrackPublishOptions } from '../track/options';
|
|
6
6
|
import LocalParticipant from './LocalParticipant';
|
|
7
7
|
|
|
8
|
-
type
|
|
9
|
-
|
|
8
|
+
type FrameMetadataTestParticipant = {
|
|
9
|
+
canPublishFrameMetadata: () => boolean;
|
|
10
10
|
log: { warn: ReturnType<typeof vi.fn> };
|
|
11
|
-
|
|
11
|
+
normalizeRequestedFrameMetadataOptions: (
|
|
12
12
|
track: LocalTrack,
|
|
13
13
|
opts: TrackPublishOptions,
|
|
14
14
|
) => PacketTrailerFeature[];
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
function makeParticipant(
|
|
18
|
-
const participant = Object.create(LocalParticipant.prototype) as
|
|
19
|
-
participant.
|
|
17
|
+
function makeParticipant(canPublishFrameMetadata: boolean) {
|
|
18
|
+
const participant = Object.create(LocalParticipant.prototype) as FrameMetadataTestParticipant;
|
|
19
|
+
participant.canPublishFrameMetadata = () => canPublishFrameMetadata;
|
|
20
20
|
participant.log = { warn: vi.fn() };
|
|
21
21
|
return participant;
|
|
22
22
|
}
|
|
@@ -35,12 +35,12 @@ function makeTrack(kind: Track.Kind) {
|
|
|
35
35
|
} as unknown as LocalTrack;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
describe('LocalParticipant
|
|
39
|
-
it('normalizes requested video
|
|
38
|
+
describe('LocalParticipant frame metadata publish options', () => {
|
|
39
|
+
it('normalizes requested video frame metadata options to advertised features', () => {
|
|
40
40
|
const participant = makeParticipant(true);
|
|
41
|
-
const opts: TrackPublishOptions = {
|
|
41
|
+
const opts: TrackPublishOptions = { frameMetadata: { timestamp: true, frameId: true } };
|
|
42
42
|
|
|
43
|
-
const features = participant.
|
|
43
|
+
const features = participant.normalizeRequestedFrameMetadataOptions(
|
|
44
44
|
makeTrack(Track.Kind.Video),
|
|
45
45
|
opts,
|
|
46
46
|
);
|
|
@@ -49,33 +49,33 @@ describe('LocalParticipant packet trailer publish options', () => {
|
|
|
49
49
|
PacketTrailerFeature.PTF_USER_TIMESTAMP,
|
|
50
50
|
PacketTrailerFeature.PTF_FRAME_ID,
|
|
51
51
|
]);
|
|
52
|
-
expect(opts.
|
|
52
|
+
expect(opts.frameMetadata).toEqual({ timestamp: true, frameId: true });
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it('clears
|
|
55
|
+
it('clears frame metadata options for non-video tracks', () => {
|
|
56
56
|
const participant = makeParticipant(true);
|
|
57
|
-
const opts: TrackPublishOptions = {
|
|
57
|
+
const opts: TrackPublishOptions = { frameMetadata: { timestamp: true } };
|
|
58
58
|
|
|
59
|
-
const features = participant.
|
|
59
|
+
const features = participant.normalizeRequestedFrameMetadataOptions(
|
|
60
60
|
makeTrack(Track.Kind.Audio),
|
|
61
61
|
opts,
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
expect(features).toEqual([]);
|
|
65
|
-
expect(opts.
|
|
65
|
+
expect(opts.frameMetadata).toBeUndefined();
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('clears
|
|
68
|
+
it('clears frame metadata options when publishing frame metadata is unsupported', () => {
|
|
69
69
|
const participant = makeParticipant(false);
|
|
70
|
-
const opts: TrackPublishOptions = {
|
|
70
|
+
const opts: TrackPublishOptions = { frameMetadata: { frameId: true } };
|
|
71
71
|
|
|
72
|
-
const features = participant.
|
|
72
|
+
const features = participant.normalizeRequestedFrameMetadataOptions(
|
|
73
73
|
makeTrack(Track.Kind.Video),
|
|
74
74
|
opts,
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
expect(features).toEqual([]);
|
|
78
|
-
expect(opts.
|
|
78
|
+
expect(opts.frameMetadata).toBeUndefined();
|
|
79
79
|
expect(participant.log.warn).toHaveBeenCalledOnce();
|
|
80
80
|
});
|
|
81
81
|
});
|