livekit-client 2.19.1 → 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 (124) 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 +204 -145
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +664 -526
  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} +133 -99
  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 +20 -10
  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 +11 -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/rpc/client/RpcClientManager.d.ts.map +1 -1
  46. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  47. package/dist/src/room/token-source/types.d.ts +1 -0
  48. package/dist/src/room/token-source/types.d.ts.map +1 -1
  49. package/dist/src/room/token-source/utils.d.ts.map +1 -1
  50. package/dist/src/room/track/FrameMetadataExtractor.d.ts +19 -0
  51. package/dist/src/room/track/FrameMetadataExtractor.d.ts.map +1 -0
  52. package/dist/src/room/track/RemoteVideoTrack.d.ts +7 -7
  53. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  54. package/dist/src/room/track/options.d.ts +17 -5
  55. package/dist/src/room/track/options.d.ts.map +1 -1
  56. package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -1
  57. package/dist/ts4.2/e2ee/types.d.ts +5 -5
  58. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +19 -9
  59. package/dist/ts4.2/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
  60. package/dist/ts4.2/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +7 -7
  61. package/dist/ts4.2/{packetTrailer → frameMetadata}/types.d.ts +10 -6
  62. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -0
  63. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
  64. package/dist/ts4.2/index.d.ts +9 -3
  65. package/dist/ts4.2/options.d.ts +8 -4
  66. package/dist/ts4.2/room/PCTransport.d.ts +1 -0
  67. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  68. package/dist/ts4.2/room/RTCEngine.d.ts +11 -1
  69. package/dist/ts4.2/room/Room.d.ts +2 -2
  70. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +2 -2
  71. package/dist/ts4.2/room/token-source/types.d.ts +1 -0
  72. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -0
  73. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +7 -7
  74. package/dist/ts4.2/room/track/options.d.ts +17 -5
  75. package/package.json +25 -20
  76. package/src/api/SignalClient.test.ts +4 -4
  77. package/src/api/SignalClient.ts +7 -3
  78. package/src/api/WebSocketStream.test.ts +7 -7
  79. package/src/e2ee/E2eeManager.ts +13 -11
  80. package/src/e2ee/types.ts +5 -5
  81. package/src/e2ee/worker/FrameCryptor.test.ts +5 -5
  82. package/src/e2ee/worker/FrameCryptor.ts +69 -30
  83. package/src/e2ee/worker/e2ee.worker.ts +5 -5
  84. package/src/e2ee/worker/tsconfig.json +4 -1
  85. package/src/{packetTrailer/PacketTrailerManager.test.ts → frameMetadata/FrameMetadataManager.test.ts} +10 -8
  86. package/src/{packetTrailer/PacketTrailerManager.ts → frameMetadata/FrameMetadataManager.ts} +21 -15
  87. package/src/{packetTrailer/packetTrailer.test.ts → frameMetadata/frameMetadata.test.ts} +1 -1
  88. package/src/{packetTrailer/packetTrailer.ts → frameMetadata/frameMetadata.ts} +9 -9
  89. package/src/{packetTrailer → frameMetadata}/types.ts +12 -6
  90. package/src/{packetTrailer → frameMetadata}/utils.test.ts +19 -19
  91. package/src/{packetTrailer → frameMetadata}/utils.ts +11 -11
  92. package/src/{packetTrailer/worker/packetTrailer.worker.ts → frameMetadata/worker/frameMetadata.worker.ts} +5 -5
  93. package/src/{packetTrailer → frameMetadata}/worker/tsconfig.json +4 -1
  94. package/src/index.ts +13 -2
  95. package/src/options.ts +9 -4
  96. package/src/room/PCTransport.ts +4 -0
  97. package/src/room/PCTransportManager.ts +4 -0
  98. package/src/room/RTCEngine.test.ts +143 -9
  99. package/src/room/RTCEngine.ts +156 -48
  100. package/src/room/Room.ts +10 -10
  101. package/src/room/participant/LocalParticipant.test.ts +19 -19
  102. package/src/room/participant/LocalParticipant.ts +21 -16
  103. package/src/room/participant/publishUtils.test.ts +6 -0
  104. package/src/room/participant/publishUtils.ts +4 -1
  105. package/src/room/rpc/client/RpcClientManager.ts +6 -3
  106. package/src/room/rpc/server/RpcServerManager.ts +1 -1
  107. package/src/room/token-source/TokenSource.ts +9 -23
  108. package/src/room/token-source/types.ts +1 -0
  109. package/src/room/token-source/utils.ts +1 -0
  110. package/src/room/track/{PacketTrailerExtractor.ts → FrameMetadataExtractor.ts} +8 -8
  111. package/src/room/track/RemoteVideoTrack.ts +7 -11
  112. package/src/room/track/options.ts +19 -5
  113. package/dist/livekit-client.pt.worker.js +0 -2
  114. package/dist/livekit-client.pt.worker.mjs.map +0 -1
  115. package/dist/src/packetTrailer/packetTrailer.d.ts.map +0 -1
  116. package/dist/src/packetTrailer/types.d.ts.map +0 -1
  117. package/dist/src/packetTrailer/utils.d.ts +0 -9
  118. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
  119. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +0 -1
  120. package/dist/src/room/track/PacketTrailerExtractor.d.ts +0 -19
  121. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +0 -1
  122. package/dist/ts4.2/packetTrailer/utils.d.ts +0 -9
  123. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
  124. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +0 -19
@@ -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;
@@ -324,12 +332,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
324
332
  this.joinAttempts += 1;
325
333
 
326
334
  this.setupSignalClientCallbacks();
335
+ // Whether the initial publisher offer is bundled with the join request. Computed once and
336
+ // reused after the join below. Only the (non-Firefox) offer-with-join path does this.
337
+ const sendOfferWithJoin = !useV0Path && isPublisherOfferWithJoinSupported();
338
+
327
339
  let offerProto: SessionDescription | undefined;
328
- if (!useV0Path && isPublisherOfferWithJoinSupported()) {
340
+ if (sendOfferWithJoin) {
329
341
  if (!this.pcManager) {
342
+ // Firefox is excluded from offer-with-join (see isPublisherOfferWithJoinSupported):
343
+ // customers reported ICE connectivity problems for FF on this path (#1919) that we were
344
+ // never able to reproduce, so out of caution FF stays on the deferred path below. The
345
+ // exact cause is unknown — note that ICE gathering does not actually start here, since
346
+ // createInitialOffer() defers setLocalDescription (via pendingInitialOffer) until the
347
+ // answer is applied, after updateConfiguration() has set the server's TURN servers.
330
348
  await this.configure();
331
- this.createDataChannels();
332
- this.addMediaSections(initialMediaSectionsAudio, initialMediaSectionsVideo);
349
+ this.applyInitialPublisherLayout();
333
350
  }
334
351
  const offer = await this.pcManager?.publisher.createInitialOffer();
335
352
  if (offer) {
@@ -358,11 +375,20 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
358
375
  this.participantSid = joinResponse.participant?.sid;
359
376
 
360
377
  this.subscriberPrimary = joinResponse.subscriberPrimary;
361
- if (!useV0Path && isPublisherOfferWithJoinSupported()) {
378
+ if (sendOfferWithJoin) {
362
379
  this.pcManager?.updateConfiguration(this.makeRTCConfiguration(joinResponse));
363
380
  } else {
364
381
  if (!this.pcManager) {
382
+ // Deferred path (Firefox, and V0): configure with the join response so the PC picks up
383
+ // the server's ICE servers and topology, then negotiate separately rather than bundling
384
+ // the offer with the join.
365
385
  await this.configure(joinResponse, !useV0Path);
386
+ if (!useV0Path) {
387
+ // The V1 first offer must carry the media layout so Firefox binds receive decoders for
388
+ // subscribed tracks — without it, subscribed audio/video arrive as RTP but
389
+ // never decode. V0 (legacy dual-PC) keeps its original lazy behavior.
390
+ this.applyInitialPublisherLayout();
391
+ }
366
392
  }
367
393
  // create offer
368
394
  if (!this.subscriberPrimary || joinResponse.fastPublish) {
@@ -431,18 +457,25 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
431
457
  }
432
458
 
433
459
  async cleanupPeerConnections() {
434
- await this.pcManager?.close();
435
- this.pcManager = undefined;
436
-
437
460
  const dcCleanup = (dc: RTCDataChannel | undefined) => {
438
- if (!dc) return;
439
- 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.
440
471
  dc.onbufferedamountlow = null;
441
472
  dc.onclose = null;
442
473
  dc.onclosing = null;
443
474
  dc.onerror = null;
444
475
  dc.onmessage = null;
445
476
  dc.onopen = null;
477
+
478
+ dc.close();
446
479
  };
447
480
  dcCleanup(this.lossyDC);
448
481
  dcCleanup(this.lossyDCSub);
@@ -451,6 +484,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
451
484
  dcCleanup(this.dataTrackDC);
452
485
  dcCleanup(this.dataTrackDCSub);
453
486
 
487
+ await this.pcManager?.close();
488
+ this.pcManager = undefined;
489
+
454
490
  this.lossyDC = undefined;
455
491
  this.lossyDCSub = undefined;
456
492
  this.reliableDC = undefined;
@@ -770,7 +806,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
770
806
  // used; RTCRtpScriptTransform does not need the PeerConnection flag.
771
807
  const needsInsertableStreams =
772
808
  this.signalOpts?.e2eeEnabled ||
773
- (this.options.packetTrailer?.worker && !shouldUsePacketTrailerScriptTransform());
809
+ (this.frameMetadataWorker && !shouldUseFrameMetadataScriptTransform());
774
810
 
775
811
  if (needsInsertableStreams && isInsertableStreamSupported()) {
776
812
  this.log.debug('E2EE - setting up transports with insertable streams');
@@ -814,6 +850,25 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
814
850
  return rtcConfig;
815
851
  }
816
852
 
853
+ /**
854
+ * Populate the publisher PC so its first offer carries the data channels + recvonly media
855
+ * sections. Required for every V1 connection: Firefox only binds receive decoders for media
856
+ * present in that first offer, and the offer-with-join path needs the sections to
857
+ * build a meaningful initial offer. Must be called on a configured pcManager.
858
+ */
859
+ private applyInitialPublisherLayout() {
860
+ this.createDataChannels();
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
+ }
870
+ }
871
+
817
872
  private addMediaSections(numAudios: number, numVideos: number) {
818
873
  const transceiverInit: RTCRtpTransceiverInit = { direction: 'recvonly' };
819
874
  for (let i: number = 0; i < numAudios; i++) {
@@ -833,14 +888,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
833
888
  if (this.lossyDC) {
834
889
  this.lossyDC.onmessage = null;
835
890
  this.lossyDC.onerror = null;
891
+ this.lossyDC.onclose = null;
836
892
  }
837
893
  if (this.reliableDC) {
838
894
  this.reliableDC.onmessage = null;
839
895
  this.reliableDC.onerror = null;
896
+ this.reliableDC.onclose = null;
840
897
  }
841
898
  if (this.dataTrackDC) {
842
899
  this.dataTrackDC.onmessage = null;
843
900
  this.dataTrackDC.onerror = null;
901
+ this.dataTrackDC.onclose = null;
844
902
  }
845
903
 
846
904
  // create data channels
@@ -866,6 +924,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
866
924
  this.reliableDC.onerror = this.handleDataError;
867
925
  this.dataTrackDC.onerror = this.handleDataError;
868
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
+
869
932
  // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
870
933
  this.lossyDC.bufferedAmountLowThreshold = 65535;
871
934
  this.reliableDC.bufferedAmountLowThreshold = 65535;
@@ -994,17 +1057,41 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
994
1057
  };
995
1058
 
996
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
+
997
1068
  const channel = event.currentTarget as RTCDataChannel;
998
1069
  const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable';
999
1070
 
1000
- if (event instanceof ErrorEvent && event.error) {
1001
- const { error } = event.error;
1002
- 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
+ });
1003
1078
  } else {
1004
1079
  this.log.error(`Unknown DataChannel error on ${channelKind}`, { event });
1005
1080
  }
1006
1081
  };
1007
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
+
1008
1095
  private handleBufferedAmountLow = (channelKind: DataChannelKind) => {
1009
1096
  this.updateAndEmitDCBufferStatus(channelKind);
1010
1097
  };
@@ -1023,7 +1110,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1023
1110
  } else {
1024
1111
  throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
1025
1112
  }
1026
- this.setupPacketTrailerSender(sender, opts);
1113
+ this.setupFrameMetadataSender(sender, opts);
1027
1114
  return sender;
1028
1115
  }
1029
1116
 
@@ -1043,50 +1130,55 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1043
1130
  throw new UnexpectedConnectionState('Cannot stream on this device');
1044
1131
  }
1045
1132
  if (sender) {
1046
- this.setupPacketTrailerSender(sender, opts);
1133
+ this.setupFrameMetadataSender(sender, opts);
1047
1134
  }
1048
1135
  return sender;
1049
1136
  }
1050
1137
 
1051
- private setupPacketTrailerSender(sender: RTCRtpSender, opts: TrackPublishOptions = {}) {
1052
- 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) {
1053
1145
  return;
1054
1146
  }
1055
1147
 
1056
- const packetTrailer = opts.packetTrailer;
1057
- const hasPacketTrailer = hasPacketTrailerPublishOptions(packetTrailer);
1148
+ const frameMetadata = opts.frameMetadata ?? opts.packetTrailer;
1149
+ const hasMetadata = hasFrameMetadataPublishOptions(frameMetadata);
1058
1150
 
1059
- if (shouldUsePacketTrailerScriptTransform()) {
1060
- if (hasPacketTrailer) {
1151
+ if (shouldUseFrameMetadataScriptTransform()) {
1152
+ if (hasMetadata) {
1061
1153
  // @ts-ignore
1062
- sender.transform = new RTCRtpScriptTransform(this.options.packetTrailer.worker, {
1154
+ sender.transform = new RTCRtpScriptTransform(worker, {
1063
1155
  kind: 'encode',
1064
- packetTrailer,
1156
+ packetTrailer: frameMetadata,
1065
1157
  });
1066
1158
  }
1067
1159
  return;
1068
1160
  }
1069
1161
 
1070
1162
  if (
1071
- !isPacketTrailerSupported(this.options.packetTrailer) ||
1163
+ !isFrameMetadataSupported(this.options.frameMetadata ?? this.options.packetTrailer) ||
1072
1164
  !('createEncodedStreams' in sender)
1073
1165
  ) {
1074
- if (hasPacketTrailer) {
1075
- 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);
1076
1168
  }
1077
1169
  return;
1078
1170
  }
1079
1171
 
1080
1172
  // @ts-ignore
1081
1173
  const { readable, writable } = sender.createEncodedStreams();
1082
- if (hasPacketTrailer) {
1083
- this.options.packetTrailer.worker.postMessage(
1174
+ if (hasMetadata) {
1175
+ worker.postMessage(
1084
1176
  {
1085
1177
  kind: 'encode',
1086
1178
  data: {
1087
1179
  readableStream: readable,
1088
1180
  writableStream: writable,
1089
- packetTrailer,
1181
+ packetTrailer: frameMetadata,
1090
1182
  },
1091
1183
  },
1092
1184
  [readable, writable],
@@ -1506,6 +1598,23 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1506
1598
 
1507
1599
  const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
1508
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
+
1509
1618
  switch (kind) {
1510
1619
  case DataChannelKind.LOSSY:
1511
1620
  case DataChannelKind.DATA_TRACK_LOSSY:
@@ -1746,20 +1855,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1746
1855
  }
1747
1856
  this.on(EngineEvent.Closing, handleClosed);
1748
1857
  this.on(EngineEvent.Restarting, handleClosed);
1749
-
1750
- this.pcManager.publisher.once(
1751
- PCEvents.RTPVideoPayloadTypes,
1752
- (rtpTypes: MediaAttributes['rtp']) => {
1753
- const rtpMap = new Map<number, VideoCodec>();
1754
- rtpTypes.forEach((rtp) => {
1755
- const codec = rtp.codec.toLowerCase();
1756
- if (isVideoCodec(codec)) {
1757
- rtpMap.set(rtp.payload, codec);
1758
- }
1759
- });
1760
- this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1761
- },
1762
- );
1858
+ this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1859
+ this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1763
1860
 
1764
1861
  try {
1765
1862
  await this.pcManager.negotiate(abortController);
@@ -1906,6 +2003,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1906
2003
  this.shouldFailOnV1Path = true;
1907
2004
  }
1908
2005
 
2006
+ private onRtpMapAvailable = (rtpTypes: MediaAttributes['rtp']) => {
2007
+ const rtpMap = new Map<number, VideoCodec>();
2008
+ rtpTypes.forEach((rtp) => {
2009
+ const codec = rtp.codec.toLowerCase();
2010
+ if (isVideoCodec(codec)) {
2011
+ rtpMap.set(rtp.payload, codec);
2012
+ }
2013
+ });
2014
+ this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
2015
+ };
2016
+
1909
2017
  private dataChannelsInfo(): DataChannelInfo[] {
1910
2018
  const infos: DataChannelInfo[] = [];
1911
2019
  const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
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
  });
@@ -22,13 +22,13 @@ import {
22
22
  protoInt64,
23
23
  } from '@livekit/protocol';
24
24
  import { SignalConnectionState } from '../../api/SignalClient';
25
- import type { InternalRoomOptions } from '../../options';
26
25
  import {
27
- getPacketTrailerFeatures,
28
- getPacketTrailerPublishOptions,
29
- hasPacketTrailerPublishOptions,
30
- isPacketTrailerSupported,
31
- } from '../../packetTrailer/utils';
26
+ getFrameMetadataFeatures,
27
+ getFrameMetadataPublishOptions,
28
+ hasFrameMetadataPublishOptions,
29
+ isFrameMetadataSupported,
30
+ } from '../../frameMetadata/utils';
31
+ import type { InternalRoomOptions } from '../../options';
32
32
  import TypedPromise from '../../utils/TypedPromise';
33
33
  import { PCTransportState } from '../PCTransportManager';
34
34
  import type RTCEngine from '../RTCEngine';
@@ -1085,7 +1085,7 @@ export default class LocalParticipant extends Participant {
1085
1085
  audioFeatures.push(AudioTrackFeature.TF_PRECONNECT_BUFFER);
1086
1086
  }
1087
1087
  const packetTrailerFeatures: PacketTrailerFeature[] =
1088
- this.normalizeRequestedPacketTrailerOptions(track, opts);
1088
+ this.normalizeRequestedFrameMetadataOptions(track, opts);
1089
1089
 
1090
1090
  // create track publication from track
1091
1091
  const req = new AddTrackRequest({
@@ -1416,31 +1416,36 @@ export default class LocalParticipant extends Participant {
1416
1416
  return publication;
1417
1417
  }
1418
1418
 
1419
- private canPublishPacketTrailer() {
1419
+ private canPublishFrameMetadata() {
1420
1420
  return !!(
1421
1421
  this.roomOptions.e2ee ||
1422
1422
  this.roomOptions.encryption ||
1423
- isPacketTrailerSupported(this.roomOptions.packetTrailer)
1423
+ isFrameMetadataSupported(this.roomOptions.frameMetadata ?? this.roomOptions.packetTrailer)
1424
1424
  );
1425
1425
  }
1426
1426
 
1427
- private normalizeRequestedPacketTrailerOptions(track: LocalTrack, opts: TrackPublishOptions) {
1428
- if (track.kind !== Track.Kind.Video || !hasPacketTrailerPublishOptions(opts.packetTrailer)) {
1427
+ private normalizeRequestedFrameMetadataOptions(track: LocalTrack, opts: TrackPublishOptions) {
1428
+ const fmOpts = opts.frameMetadata ?? opts.packetTrailer;
1429
+ if (track.kind !== Track.Kind.Video || !hasFrameMetadataPublishOptions(fmOpts)) {
1430
+ opts.frameMetadata = undefined;
1429
1431
  opts.packetTrailer = undefined;
1430
1432
  return [];
1431
1433
  }
1432
1434
 
1433
- if (!this.canPublishPacketTrailer()) {
1434
- this.log.warn('packet trailer transform not supported; not advertising features', {
1435
+ if (!this.canPublishFrameMetadata()) {
1436
+ this.log.warn('frame metadata transform not supported; not advertising features', {
1435
1437
  ...this.logContext,
1436
1438
  ...getLogContextFromTrack(track),
1437
1439
  });
1440
+ opts.frameMetadata = undefined;
1438
1441
  opts.packetTrailer = undefined;
1439
1442
  return [];
1440
1443
  }
1441
1444
 
1442
- const features = getPacketTrailerFeatures(opts.packetTrailer);
1443
- opts.packetTrailer = getPacketTrailerPublishOptions(features);
1445
+ const features = getFrameMetadataFeatures(fmOpts);
1446
+ const normalized = getFrameMetadataPublishOptions(features);
1447
+ opts.frameMetadata = normalized;
1448
+ opts.packetTrailer = normalized;
1444
1449
  return features;
1445
1450
  }
1446
1451
 
@@ -1496,7 +1501,7 @@ export default class LocalParticipant extends Participant {
1496
1501
  if (!simulcastTrack) {
1497
1502
  return;
1498
1503
  }
1499
- const packetTrailerFeatures = this.normalizeRequestedPacketTrailerOptions(track, opts);
1504
+ const packetTrailerFeatures = this.normalizeRequestedFrameMetadataOptions(track, opts);
1500
1505
 
1501
1506
  const req = new AddTrackRequest({
1502
1507
  cid: simulcastTrack.mediaStreamTrack.id,
@@ -174,6 +174,12 @@ describe('customSimulcastLayers', () => {
174
174
  expect(sortedPresets[1].encoding.maxFramerate).toBe(15);
175
175
  expect(sortedPresets[2].encoding.maxFramerate).toBe(20);
176
176
  });
177
+ it('does not mutate the passed-in array', () => {
178
+ const presets = [VideoPresets.h1440, VideoPresets.h360, VideoPresets.h1080, VideoPresets.h90];
179
+ const original = [...presets];
180
+ sortPresets(presets);
181
+ expect(presets).toEqual(original);
182
+ });
177
183
  });
178
184
 
179
185
  describe('screenShareSimulcastDefaults', () => {
@@ -419,7 +419,10 @@ function encodingsFromPresets(
419
419
  /** @internal */
420
420
  export function sortPresets(presets: Array<VideoPreset> | undefined) {
421
421
  if (!presets) return;
422
- return presets.sort((a, b) => {
422
+ // Sort a copy so we don't mutate the caller's array in place. Mutating the
423
+ // passed-in simulcast layers can cause consumers that compare options by value
424
+ // (e.g. components-react's useLiveKitRoom) to detect a spurious change.
425
+ return presets.slice().sort((a, b) => {
423
426
  const { encoding: aEnc } = a;
424
427
  const { encoding: bEnc } = b;
425
428