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.
Files changed (123) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +23 -23
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +179 -111
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +2 -0
  8. package/dist/{livekit-client.pt.worker.js.map → livekit-client.fm.worker.js.map} +1 -1
  9. package/dist/{livekit-client.pt.worker.mjs → livekit-client.fm.worker.mjs} +4 -4
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -0
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/types.d.ts +5 -5
  17. package/dist/src/e2ee/types.d.ts.map +1 -1
  18. package/dist/src/e2ee/worker/FrameCryptor.d.ts +8 -8
  19. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  20. package/dist/src/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
  21. package/dist/src/{packetTrailer/PacketTrailerManager.d.ts.map → frameMetadata/FrameMetadataManager.d.ts.map} +1 -1
  22. package/dist/src/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +7 -7
  23. package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -0
  24. package/dist/src/{packetTrailer → frameMetadata}/types.d.ts +10 -6
  25. package/dist/src/frameMetadata/types.d.ts.map +1 -0
  26. package/dist/src/frameMetadata/utils.d.ts +9 -0
  27. package/dist/src/{packetTrailer → frameMetadata}/utils.d.ts.map +1 -1
  28. package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
  29. package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts.map +1 -0
  30. package/dist/src/index.d.ts +10 -2
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/options.d.ts +8 -4
  33. package/dist/src/options.d.ts.map +1 -1
  34. package/dist/src/room/PCTransport.d.ts +1 -0
  35. package/dist/src/room/PCTransport.d.ts.map +1 -1
  36. package/dist/src/room/PCTransportManager.d.ts +1 -0
  37. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  38. package/dist/src/room/RTCEngine.d.ts +3 -1
  39. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  40. package/dist/src/room/Room.d.ts +2 -2
  41. package/dist/src/room/Room.d.ts.map +1 -1
  42. package/dist/src/room/participant/LocalParticipant.d.ts +2 -2
  43. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  44. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  45. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  46. package/dist/src/room/token-source/types.d.ts +1 -0
  47. package/dist/src/room/token-source/types.d.ts.map +1 -1
  48. package/dist/src/room/token-source/utils.d.ts.map +1 -1
  49. package/dist/src/room/track/FrameMetadataExtractor.d.ts +19 -0
  50. package/dist/src/room/track/FrameMetadataExtractor.d.ts.map +1 -0
  51. package/dist/src/room/track/RemoteVideoTrack.d.ts +7 -7
  52. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  53. package/dist/src/room/track/options.d.ts +17 -5
  54. package/dist/src/room/track/options.d.ts.map +1 -1
  55. package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -1
  56. package/dist/ts4.2/e2ee/types.d.ts +5 -5
  57. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +8 -8
  58. package/dist/ts4.2/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
  59. package/dist/ts4.2/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +7 -7
  60. package/dist/ts4.2/{packetTrailer → frameMetadata}/types.d.ts +10 -6
  61. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -0
  62. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
  63. package/dist/ts4.2/index.d.ts +9 -3
  64. package/dist/ts4.2/options.d.ts +8 -4
  65. package/dist/ts4.2/room/PCTransport.d.ts +1 -0
  66. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  67. package/dist/ts4.2/room/RTCEngine.d.ts +3 -1
  68. package/dist/ts4.2/room/Room.d.ts +2 -2
  69. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +2 -2
  70. package/dist/ts4.2/room/token-source/types.d.ts +1 -0
  71. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -0
  72. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +7 -7
  73. package/dist/ts4.2/room/track/options.d.ts +17 -5
  74. package/package.json +23 -18
  75. package/src/api/SignalClient.test.ts +4 -4
  76. package/src/api/SignalClient.ts +7 -3
  77. package/src/api/WebSocketStream.test.ts +7 -7
  78. package/src/e2ee/E2eeManager.ts +13 -11
  79. package/src/e2ee/types.ts +5 -5
  80. package/src/e2ee/worker/FrameCryptor.test.ts +5 -5
  81. package/src/e2ee/worker/FrameCryptor.ts +27 -23
  82. package/src/e2ee/worker/e2ee.worker.ts +5 -5
  83. package/src/e2ee/worker/tsconfig.json +4 -1
  84. package/src/{packetTrailer/PacketTrailerManager.test.ts → frameMetadata/FrameMetadataManager.test.ts} +10 -8
  85. package/src/{packetTrailer/PacketTrailerManager.ts → frameMetadata/FrameMetadataManager.ts} +21 -15
  86. package/src/{packetTrailer/packetTrailer.test.ts → frameMetadata/frameMetadata.test.ts} +1 -1
  87. package/src/{packetTrailer/packetTrailer.ts → frameMetadata/frameMetadata.ts} +9 -9
  88. package/src/{packetTrailer → frameMetadata}/types.ts +12 -6
  89. package/src/{packetTrailer → frameMetadata}/utils.test.ts +19 -19
  90. package/src/{packetTrailer → frameMetadata}/utils.ts +11 -11
  91. package/src/{packetTrailer/worker/packetTrailer.worker.ts → frameMetadata/worker/frameMetadata.worker.ts} +5 -5
  92. package/src/{packetTrailer → frameMetadata}/worker/tsconfig.json +4 -1
  93. package/src/index.ts +13 -2
  94. package/src/options.ts +9 -4
  95. package/src/room/PCTransport.ts +4 -0
  96. package/src/room/PCTransportManager.ts +4 -0
  97. package/src/room/RTCEngine.test.ts +143 -9
  98. package/src/room/RTCEngine.ts +111 -31
  99. package/src/room/Room.ts +10 -10
  100. package/src/room/participant/LocalParticipant.test.ts +19 -19
  101. package/src/room/participant/LocalParticipant.ts +21 -16
  102. package/src/room/participant/publishUtils.test.ts +6 -0
  103. package/src/room/participant/publishUtils.ts +4 -1
  104. package/src/room/rpc/client/RpcClientManager.ts +1 -1
  105. package/src/room/rpc/server/RpcServerManager.ts +1 -1
  106. package/src/room/token-source/TokenSource.ts +9 -23
  107. package/src/room/token-source/types.ts +1 -0
  108. package/src/room/token-source/utils.ts +1 -0
  109. package/src/room/track/{PacketTrailerExtractor.ts → FrameMetadataExtractor.ts} +8 -8
  110. package/src/room/track/RemoteVideoTrack.ts +7 -11
  111. package/src/room/track/options.ts +19 -5
  112. package/dist/livekit-client.pt.worker.js +0 -2
  113. package/dist/livekit-client.pt.worker.mjs.map +0 -1
  114. package/dist/src/packetTrailer/packetTrailer.d.ts.map +0 -1
  115. package/dist/src/packetTrailer/types.d.ts.map +0 -1
  116. package/dist/src/packetTrailer/utils.d.ts +0 -9
  117. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
  118. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +0 -1
  119. package/dist/src/room/track/PacketTrailerExtractor.d.ts +0 -19
  120. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +0 -1
  121. package/dist/ts4.2/packetTrailer/utils.d.ts +0 -9
  122. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
  123. 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 setupPacketTrailerSender(engine: RTCEngine, sender: RTCRtpSender, opts = {}) {
61
+ function setupFrameMetadataSender(engine: RTCEngine, sender: RTCRtpSender, opts = {}) {
60
62
  (
61
63
  engine as unknown as {
62
- setupPacketTrailerSender: (sender: RTCRtpSender, opts?: unknown) => void;
64
+ setupFrameMetadataSender: (sender: RTCRtpSender, opts?: unknown) => void;
63
65
  }
64
- ).setupPacketTrailerSender(sender, opts);
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
- setupPacketTrailerSender(engine, sender);
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
- setupPacketTrailerSender(engine, sender);
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
- setupPacketTrailerSender(engine, sender, { packetTrailer: { timestamp: true, frameId: true } });
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(() => transform);
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
- setupPacketTrailerSender(engine, sender, { packetTrailer: { timestamp: true } });
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
  });
@@ -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) return;
457
- dc.close();
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.options.packetTrailer?.worker && !shouldUsePacketTrailerScriptTransform());
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
- this.addMediaSections(initialMediaSectionsAudio, initialMediaSectionsVideo);
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 ErrorEvent && event.error) {
1030
- const { error } = event.error;
1031
- this.log.error(`DataChannel error on ${channelKind}: ${event.message}`, { error });
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.setupPacketTrailerSender(sender, opts);
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.setupPacketTrailerSender(sender, opts);
1133
+ this.setupFrameMetadataSender(sender, opts);
1076
1134
  }
1077
1135
  return sender;
1078
1136
  }
1079
1137
 
1080
- private setupPacketTrailerSender(sender: RTCRtpSender, opts: TrackPublishOptions = {}) {
1081
- if (!this.options.packetTrailer?.worker || this.signalOpts?.e2eeEnabled) {
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 packetTrailer = opts.packetTrailer;
1086
- const hasPacketTrailer = hasPacketTrailerPublishOptions(packetTrailer);
1148
+ const frameMetadata = opts.frameMetadata ?? opts.packetTrailer;
1149
+ const hasMetadata = hasFrameMetadataPublishOptions(frameMetadata);
1087
1150
 
1088
- if (shouldUsePacketTrailerScriptTransform()) {
1089
- if (hasPacketTrailer) {
1151
+ if (shouldUseFrameMetadataScriptTransform()) {
1152
+ if (hasMetadata) {
1090
1153
  // @ts-ignore
1091
- sender.transform = new RTCRtpScriptTransform(this.options.packetTrailer.worker, {
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
- !isPacketTrailerSupported(this.options.packetTrailer) ||
1163
+ !isFrameMetadataSupported(this.options.frameMetadata ?? this.options.packetTrailer) ||
1101
1164
  !('createEncodedStreams' in sender)
1102
1165
  ) {
1103
- if (hasPacketTrailer) {
1104
- this.log.warn('packet trailer transform not supported; skipping write', this.logContext);
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 (hasPacketTrailer) {
1112
- this.options.packetTrailer.worker.postMessage(
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 packetTrailerManager: PacketTrailerManager | undefined;
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.setupPacketTrailer();
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 setupPacketTrailer() {
518
- // The manager is always created so tracks that advertise packet trailer
519
- // features can be wired up when the app passes a packet trailer worker.
520
- this.packetTrailerManager = new PacketTrailerManager(this.options.packetTrailer);
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
- isPacketTrailerSupported(roomOptions.packetTrailer) || !!this.e2eeManager
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 PacketTrailerTestParticipant = {
9
- canPublishPacketTrailer: () => boolean;
8
+ type FrameMetadataTestParticipant = {
9
+ canPublishFrameMetadata: () => boolean;
10
10
  log: { warn: ReturnType<typeof vi.fn> };
11
- normalizeRequestedPacketTrailerOptions: (
11
+ normalizeRequestedFrameMetadataOptions: (
12
12
  track: LocalTrack,
13
13
  opts: TrackPublishOptions,
14
14
  ) => PacketTrailerFeature[];
15
15
  };
16
16
 
17
- function makeParticipant(canPublishPacketTrailer: boolean) {
18
- const participant = Object.create(LocalParticipant.prototype) as PacketTrailerTestParticipant;
19
- participant.canPublishPacketTrailer = () => canPublishPacketTrailer;
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 packet trailer publish options', () => {
39
- it('normalizes requested video packet trailer options to advertised features', () => {
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 = { packetTrailer: { timestamp: true, frameId: true } };
41
+ const opts: TrackPublishOptions = { frameMetadata: { timestamp: true, frameId: true } };
42
42
 
43
- const features = participant.normalizeRequestedPacketTrailerOptions(
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.packetTrailer).toEqual({ timestamp: true, frameId: true });
52
+ expect(opts.frameMetadata).toEqual({ timestamp: true, frameId: true });
53
53
  });
54
54
 
55
- it('clears packet trailer options for non-video tracks', () => {
55
+ it('clears frame metadata options for non-video tracks', () => {
56
56
  const participant = makeParticipant(true);
57
- const opts: TrackPublishOptions = { packetTrailer: { timestamp: true } };
57
+ const opts: TrackPublishOptions = { frameMetadata: { timestamp: true } };
58
58
 
59
- const features = participant.normalizeRequestedPacketTrailerOptions(
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.packetTrailer).toBeUndefined();
65
+ expect(opts.frameMetadata).toBeUndefined();
66
66
  });
67
67
 
68
- it('clears packet trailer options when publishing packet trailers is unsupported', () => {
68
+ it('clears frame metadata options when publishing frame metadata is unsupported', () => {
69
69
  const participant = makeParticipant(false);
70
- const opts: TrackPublishOptions = { packetTrailer: { frameId: true } };
70
+ const opts: TrackPublishOptions = { frameMetadata: { frameId: true } };
71
71
 
72
- const features = participant.normalizeRequestedPacketTrailerOptions(
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.packetTrailer).toBeUndefined();
78
+ expect(opts.frameMetadata).toBeUndefined();
79
79
  expect(participant.log.warn).toHaveBeenCalledOnce();
80
80
  });
81
81
  });