livekit-client 2.18.9 → 2.18.10

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 (184) 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 +5609 -644
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2870 -2420
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.pt.worker.js +2 -0
  8. package/dist/livekit-client.pt.worker.js.map +1 -0
  9. package/dist/livekit-client.pt.worker.mjs +5834 -0
  10. package/dist/livekit-client.pt.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 +2 -1
  14. package/dist/src/api/SignalClient.d.ts.map +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts +8 -7
  16. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  17. package/dist/src/e2ee/types.d.ts +35 -8
  18. package/dist/src/e2ee/types.d.ts.map +1 -1
  19. package/dist/src/e2ee/utils.d.ts +5 -5
  20. package/dist/src/e2ee/utils.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
  22. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  25. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
  26. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  27. package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
  28. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  29. package/dist/src/index.d.ts +4 -1
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/options.d.ts +7 -0
  32. package/dist/src/options.d.ts.map +1 -1
  33. package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
  34. package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
  35. package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
  36. package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
  37. package/dist/src/packetTrailer/types.d.ts +57 -0
  38. package/dist/src/packetTrailer/types.d.ts.map +1 -0
  39. package/dist/src/packetTrailer/utils.d.ts +9 -0
  40. package/dist/src/packetTrailer/utils.d.ts.map +1 -0
  41. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  42. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
  43. package/dist/src/room/RTCEngine.d.ts +2 -1
  44. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  45. package/dist/src/room/Room.d.ts +3 -1
  46. package/dist/src/room/Room.d.ts.map +1 -1
  47. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
  48. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  49. package/dist/src/room/data-track/depacketizer.d.ts +12 -4
  50. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  51. package/dist/src/room/data-track/frame.d.ts +3 -3
  52. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  53. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  54. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  55. package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
  56. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  57. package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
  58. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  59. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  60. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  61. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  62. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  63. package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
  64. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  65. package/dist/src/room/data-track/types.d.ts +7 -0
  66. package/dist/src/room/data-track/types.d.ts.map +1 -1
  67. package/dist/src/room/events.d.ts +2 -2
  68. package/dist/src/room/participant/LocalParticipant.d.ts +3 -1
  69. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  70. package/dist/src/room/participant/Participant.d.ts +1 -1
  71. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  72. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  73. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  74. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  75. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  76. package/dist/src/room/track/Track.d.ts +1 -1
  77. package/dist/src/room/track/Track.d.ts.map +1 -1
  78. package/dist/src/room/track/create.d.ts.map +1 -1
  79. package/dist/src/room/track/options.d.ts +10 -0
  80. package/dist/src/room/track/options.d.ts.map +1 -1
  81. package/dist/src/room/track/utils.d.ts.map +1 -1
  82. package/dist/src/room/utils.d.ts +4 -3
  83. package/dist/src/room/utils.d.ts.map +1 -1
  84. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  85. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  86. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  87. package/dist/src/version.d.ts +1 -1
  88. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  89. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  90. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  91. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  92. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  93. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  94. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  95. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  96. package/dist/ts4.2/index.d.ts +5 -1
  97. package/dist/ts4.2/options.d.ts +7 -0
  98. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  99. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  100. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  101. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  102. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  103. package/dist/ts4.2/room/RTCEngine.d.ts +2 -1
  104. package/dist/ts4.2/room/Room.d.ts +3 -1
  105. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  106. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  107. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  108. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  109. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  110. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
  111. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  112. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  113. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  114. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  115. package/dist/ts4.2/room/events.d.ts +2 -2
  116. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +3 -1
  117. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  118. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  119. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  120. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  121. package/dist/ts4.2/room/track/options.d.ts +10 -0
  122. package/dist/ts4.2/room/utils.d.ts +4 -3
  123. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  124. package/dist/ts4.2/version.d.ts +1 -1
  125. package/package.json +24 -16
  126. package/src/api/SignalClient.test.ts +102 -10
  127. package/src/api/SignalClient.ts +4 -2
  128. package/src/api/WebSocketStream.test.ts +0 -1
  129. package/src/e2ee/E2eeManager.ts +82 -30
  130. package/src/e2ee/types.ts +37 -8
  131. package/src/e2ee/utils.ts +7 -6
  132. package/src/e2ee/worker/DataCryptor.ts +6 -6
  133. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  134. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  135. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  136. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  137. package/src/e2ee/worker/naluUtils.ts +4 -4
  138. package/src/e2ee/worker/sifPayload.ts +10 -8
  139. package/src/index.ts +7 -0
  140. package/src/options.ts +8 -0
  141. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  142. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  143. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  144. package/src/packetTrailer/packetTrailer.ts +276 -0
  145. package/src/packetTrailer/types.ts +75 -0
  146. package/src/packetTrailer/utils.test.ts +105 -0
  147. package/src/packetTrailer/utils.ts +50 -0
  148. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  149. package/src/packetTrailer/worker/tsconfig.json +14 -0
  150. package/src/room/BackOffStrategy.test.ts +1 -1
  151. package/src/room/RTCEngine.test.ts +219 -0
  152. package/src/room/RTCEngine.ts +86 -20
  153. package/src/room/Room.test.ts +62 -1
  154. package/src/room/Room.ts +28 -5
  155. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  156. package/src/room/data-track/depacketizer.test.ts +433 -1
  157. package/src/room/data-track/depacketizer.ts +79 -61
  158. package/src/room/data-track/frame.ts +2 -2
  159. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  160. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  161. package/src/room/data-track/incoming/pipeline.ts +13 -2
  162. package/src/room/data-track/outgoing/types.ts +3 -2
  163. package/src/room/data-track/packet/extensions.ts +2 -2
  164. package/src/room/data-track/packet/index.ts +6 -6
  165. package/src/room/data-track/packet/serializable.ts +1 -1
  166. package/src/room/data-track/types.ts +8 -0
  167. package/src/room/events.ts +2 -2
  168. package/src/room/participant/LocalParticipant.test.ts +81 -0
  169. package/src/room/participant/LocalParticipant.ts +48 -7
  170. package/src/room/participant/Participant.ts +1 -1
  171. package/src/room/participant/publishUtils.ts +1 -1
  172. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  173. package/src/room/track/RemoteVideoTrack.ts +23 -2
  174. package/src/room/track/Track.ts +1 -1
  175. package/src/room/track/create.ts +0 -4
  176. package/src/room/track/options.ts +11 -0
  177. package/src/room/track/record.ts +1 -1
  178. package/src/room/track/utils.ts +4 -1
  179. package/src/room/utils.test.ts +14 -1
  180. package/src/room/utils.ts +17 -3
  181. package/src/test/MockMediaStreamTrack.ts +0 -1
  182. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  183. package/src/utils/dataPacketBuffer.ts +1 -1
  184. package/src/version.ts +1 -1
package/src/room/Room.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Mutex } from '@livekit/mutex';
2
2
  import {
3
3
  ChatMessage as ChatMessageModel,
4
+ ClientInfo_Capability,
4
5
  ConnectionQualityUpdate,
5
6
  type DataPacket,
6
7
  DataPacket_Kind,
@@ -43,6 +44,8 @@ import type {
43
44
  RoomConnectOptions,
44
45
  RoomOptions,
45
46
  } from '../options';
47
+ import { PacketTrailerManager } from '../packetTrailer/PacketTrailerManager';
48
+ import { isPacketTrailerSupported } from '../packetTrailer/utils';
46
49
  import TypedPromise from '../utils/TypedPromise';
47
50
  import { getBrowser } from '../utils/browserParser';
48
51
  import { BackOffStrategy } from './BackOffStrategy';
@@ -187,6 +190,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
187
190
 
188
191
  private e2eeManager: BaseE2EEManager | undefined;
189
192
 
193
+ private packetTrailerManager: PacketTrailerManager | undefined;
194
+
190
195
  private e2eeStateMutex: Mutex = new Mutex();
191
196
 
192
197
  private connectionReconcileInterval?: ReturnType<typeof setInterval>;
@@ -307,6 +312,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
307
312
  this.outgoingDataTrackManager,
308
313
  );
309
314
 
315
+ this.setupPacketTrailer();
316
+
310
317
  if (this.options.e2ee || this.options.encryption) {
311
318
  this.setupE2EE();
312
319
  }
@@ -465,6 +472,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
465
472
  }
466
473
  }
467
474
 
475
+ private setupPacketTrailer() {
476
+ // The manager is always created so tracks that advertise packet trailer
477
+ // features can be wired up when the app passes a packet trailer worker.
478
+ this.packetTrailerManager = new PacketTrailerManager(this.options.packetTrailer);
479
+ this.packetTrailerManager.setup(this);
480
+ }
481
+
468
482
  private get logContext() {
469
483
  return {
470
484
  room: this.name,
@@ -914,6 +928,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
914
928
  autoSubscribe: connectOptions.autoSubscribe,
915
929
  adaptiveStream:
916
930
  typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
931
+ clientInfoCapabilities:
932
+ isPacketTrailerSupported(roomOptions.packetTrailer) || !!this.e2eeManager
933
+ ? [ClientInfo_Capability.CAP_PACKET_TRAILER]
934
+ : undefined,
917
935
  maxRetries: connectOptions.maxRetries,
918
936
  e2eeEnabled: !!this.e2eeManager,
919
937
  websocketTimeout: connectOptions.websocketTimeout,
@@ -946,7 +964,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
946
964
 
947
965
  if (this.e2eeManager) {
948
966
  try {
949
- this.e2eeManager.setSifTrailer(joinResponse.sifTrailer);
967
+ this.e2eeManager.setSifTrailer(joinResponse.sifTrailer as NonSharedUint8Array);
950
968
  } catch (e: any) {
951
969
  this.log.error(e instanceof Error ? e.message : 'Could not set SifTrailer', {
952
970
  error: e,
@@ -1825,7 +1843,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1825
1843
  this.handleParticipantDisconnected(info.identity, remoteParticipant);
1826
1844
  } else {
1827
1845
  // create participant if doesn't exist
1828
- remoteParticipant = this.getOrCreateParticipant(info.identity, info);
1846
+ this.getOrCreateParticipant(info.identity, info);
1829
1847
  }
1830
1848
  }
1831
1849
 
@@ -2018,7 +2036,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2018
2036
  ) => {
2019
2037
  this.emit(
2020
2038
  RoomEvent.DataReceived,
2021
- userPacket.payload,
2039
+ userPacket.payload as NonSharedUint8Array,
2022
2040
  participant,
2023
2041
  kind,
2024
2042
  userPacket.topic,
@@ -2026,7 +2044,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2026
2044
  );
2027
2045
 
2028
2046
  // also emit on the participant
2029
- participant?.emit(ParticipantEvent.DataReceived, userPacket.payload, kind, encryptionType);
2047
+ participant?.emit(
2048
+ ParticipantEvent.DataReceived,
2049
+ userPacket.payload as NonSharedUint8Array,
2050
+ kind,
2051
+ encryptionType,
2052
+ );
2030
2053
  };
2031
2054
 
2032
2055
  private handleSipDtmf = (participant: RemoteParticipant | undefined, dtmf: SipDTMF) => {
@@ -2874,7 +2897,7 @@ export type RoomEventCallbacks = {
2874
2897
  activeSpeakersChanged: (speakers: Array<Participant>) => void;
2875
2898
  roomMetadataChanged: (metadata: string) => void;
2876
2899
  dataReceived: (
2877
- payload: Uint8Array,
2900
+ payload: NonSharedUint8Array,
2878
2901
  participant?: RemoteParticipant,
2879
2902
  kind?: DataPacket_Kind,
2880
2903
  topic?: string,
@@ -7,7 +7,7 @@ import {
7
7
  type IRemoteTrack,
8
8
  TrackSymbol,
9
9
  } from './track-interfaces';
10
- import { type DataTrackInfo } from './types';
10
+ import { type DataTrackInfo, type RemoteDataTrackPipelineOptions } from './types';
11
11
 
12
12
  type RemoteDataTrackOptions = {
13
13
  publisherIdentity: Participant['identity'];
@@ -80,4 +80,11 @@ export default class RemoteDataTrack implements IRemoteTrack, IDataTrack {
80
80
  throw err;
81
81
  }
82
82
  }
83
+
84
+ /** Configure how incoming frames for this track are processed before they are handed out to
85
+ * subscribers (the "pipeline"). These options apply to all current and future subscriptions
86
+ * of this track, and may be set at any time. */
87
+ setPipelineOptions(options: RemoteDataTrackPipelineOptions): void {
88
+ this.manager.setPipelineOptions(this.info.sid, options);
89
+ }
83
90
  }
@@ -106,7 +106,7 @@ describe('DataTrackDepacketizer', () => {
106
106
  new Uint8Array(8),
107
107
  );
108
108
 
109
- expect(() => depacketizer.push(packetB, { errorOnPartialFrames: true })).toThrowError(
109
+ expect(() => depacketizer.push(packetB, { throwOnInterruption: true })).toThrowError(
110
110
  'Frame 5 dropped: Interrupted by the start of a new frame',
111
111
  );
112
112
  });
@@ -439,4 +439,436 @@ describe('DataTrackDepacketizer', () => {
439
439
  new Uint8Array([0x01, 0x02, 0x03]),
440
440
  );
441
441
  });
442
+
443
+ it('should assemble multiple partial frames concurrently when maxPartialFrames is set', () => {
444
+ const depacketizer = new DataTrackDepacketizer();
445
+ const pushOptions = { throwOnInterruption: true, maxPartialFrames: 2 };
446
+
447
+ const packetPayload = new Uint8Array(8);
448
+ const baseHeaderParams = {
449
+ trackHandle: DataTrackHandle.fromNumber(101),
450
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
451
+ };
452
+
453
+ // Begin frame A
454
+ const startA = new DataTrackPacket(
455
+ new DataTrackPacketHeader({
456
+ ...baseHeaderParams,
457
+ marker: FrameMarker.Start,
458
+ sequence: WrapAroundUnsignedInt.u16(0),
459
+ frameNumber: WrapAroundUnsignedInt.u16(1),
460
+ }),
461
+ packetPayload,
462
+ );
463
+ expect(depacketizer.push(startA, pushOptions)).toBeNull();
464
+
465
+ // Begin frame B - should not throw because we're under capacity
466
+ const startB = new DataTrackPacket(
467
+ new DataTrackPacketHeader({
468
+ ...baseHeaderParams,
469
+ marker: FrameMarker.Start,
470
+ sequence: WrapAroundUnsignedInt.u16(100),
471
+ frameNumber: WrapAroundUnsignedInt.u16(2),
472
+ }),
473
+ packetPayload,
474
+ );
475
+ expect(depacketizer.push(startB, pushOptions)).toBeNull();
476
+
477
+ // Complete frame A out of order - should produce a frame
478
+ const finalA = new DataTrackPacket(
479
+ new DataTrackPacketHeader({
480
+ ...baseHeaderParams,
481
+ marker: FrameMarker.Final,
482
+ sequence: WrapAroundUnsignedInt.u16(1),
483
+ frameNumber: WrapAroundUnsignedInt.u16(1),
484
+ }),
485
+ packetPayload,
486
+ );
487
+ const frameA = depacketizer.push(finalA, pushOptions);
488
+ expect(frameA).not.toBeNull();
489
+ expect(frameA!.payload.byteLength).toStrictEqual(packetPayload.byteLength * 2);
490
+
491
+ // Frame B is still in flight and should still complete cleanly
492
+ const finalB = new DataTrackPacket(
493
+ new DataTrackPacketHeader({
494
+ ...baseHeaderParams,
495
+ marker: FrameMarker.Final,
496
+ sequence: WrapAroundUnsignedInt.u16(101),
497
+ frameNumber: WrapAroundUnsignedInt.u16(2),
498
+ }),
499
+ packetPayload,
500
+ );
501
+ const frameB = depacketizer.push(finalB, pushOptions);
502
+ expect(frameB).not.toBeNull();
503
+ expect(frameB!.payload.byteLength).toStrictEqual(packetPayload.byteLength * 2);
504
+ });
505
+
506
+ it('should throw when starting a new partial frame would exceed maxPartialFrames', () => {
507
+ const depacketizer = new DataTrackDepacketizer();
508
+ const pushOptions = { throwOnInterruption: true, maxPartialFrames: 2 };
509
+
510
+ const packetPayload = new Uint8Array(8);
511
+ const baseHeaderParams = {
512
+ trackHandle: DataTrackHandle.fromNumber(101),
513
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
514
+ };
515
+
516
+ // Fill the partials map with two in-flight frames
517
+ const startA = new DataTrackPacket(
518
+ new DataTrackPacketHeader({
519
+ ...baseHeaderParams,
520
+ marker: FrameMarker.Start,
521
+ sequence: WrapAroundUnsignedInt.u16(0),
522
+ frameNumber: WrapAroundUnsignedInt.u16(1),
523
+ }),
524
+ packetPayload,
525
+ );
526
+ expect(depacketizer.push(startA, pushOptions)).toBeNull();
527
+
528
+ const startB = new DataTrackPacket(
529
+ new DataTrackPacketHeader({
530
+ ...baseHeaderParams,
531
+ marker: FrameMarker.Start,
532
+ sequence: WrapAroundUnsignedInt.u16(100),
533
+ frameNumber: WrapAroundUnsignedInt.u16(2),
534
+ }),
535
+ packetPayload,
536
+ );
537
+ expect(depacketizer.push(startB, pushOptions)).toBeNull();
538
+
539
+ // A third in-flight start should throw, naming the oldest evicted frame (1) and the new one (3)
540
+ const startC = new DataTrackPacket(
541
+ new DataTrackPacketHeader({
542
+ ...baseHeaderParams,
543
+ marker: FrameMarker.Start,
544
+ sequence: WrapAroundUnsignedInt.u16(200),
545
+ frameNumber: WrapAroundUnsignedInt.u16(3),
546
+ }),
547
+ packetPayload,
548
+ );
549
+ expect(() => depacketizer.push(startC, pushOptions)).toThrowError(
550
+ 'Frame 1 dropped: Interrupted by the start of a new frame 3',
551
+ );
552
+ });
553
+
554
+ it('should throw when a single-packet frame arrives while the partials map is at capacity', () => {
555
+ const depacketizer = new DataTrackDepacketizer();
556
+ const pushOptions = { throwOnInterruption: true, maxPartialFrames: 2 };
557
+
558
+ const packetPayload = new Uint8Array(8);
559
+ const baseHeaderParams = {
560
+ trackHandle: DataTrackHandle.fromNumber(101),
561
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
562
+ };
563
+
564
+ // Fill the partials map with two in-flight frames
565
+ const startA = new DataTrackPacket(
566
+ new DataTrackPacketHeader({
567
+ ...baseHeaderParams,
568
+ marker: FrameMarker.Start,
569
+ sequence: WrapAroundUnsignedInt.u16(0),
570
+ frameNumber: WrapAroundUnsignedInt.u16(1),
571
+ }),
572
+ packetPayload,
573
+ );
574
+ expect(depacketizer.push(startA, pushOptions)).toBeNull();
575
+
576
+ const startB = new DataTrackPacket(
577
+ new DataTrackPacketHeader({
578
+ ...baseHeaderParams,
579
+ marker: FrameMarker.Start,
580
+ sequence: WrapAroundUnsignedInt.u16(100),
581
+ frameNumber: WrapAroundUnsignedInt.u16(2),
582
+ }),
583
+ packetPayload,
584
+ );
585
+ expect(depacketizer.push(startB, pushOptions)).toBeNull();
586
+
587
+ // A single-packet frame arriving at capacity should evict the oldest (frame 1) and throw
588
+ const singleC = new DataTrackPacket(
589
+ new DataTrackPacketHeader({
590
+ ...baseHeaderParams,
591
+ marker: FrameMarker.Single,
592
+ sequence: WrapAroundUnsignedInt.u16(200),
593
+ frameNumber: WrapAroundUnsignedInt.u16(3),
594
+ }),
595
+ packetPayload,
596
+ );
597
+ expect(() => depacketizer.push(singleC, pushOptions)).toThrowError(
598
+ 'Frame 1 dropped: Interrupted by the start of a new frame 3',
599
+ );
600
+ });
601
+
602
+ it('should evict the oldest partial frame when start packets exceed maxPartialFrames', () => {
603
+ const depacketizer = new DataTrackDepacketizer();
604
+ const pushOptions = { throwOnInterruption: false, maxPartialFrames: 5 };
605
+ const totalFrames = 10;
606
+
607
+ const packetPayload = new Uint8Array(8);
608
+ const baseHeaderParams = {
609
+ trackHandle: DataTrackHandle.fromNumber(101),
610
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
611
+ };
612
+
613
+ // Begin 10 partial frames. Each frame's Start uses sequence i*2; its Final uses i*2 + 1.
614
+ // After all 10 starts, only frames 6..10 remain in the partials map (oldest evicted first).
615
+ for (let i = 0; i < totalFrames; i += 1) {
616
+ const start = new DataTrackPacket(
617
+ new DataTrackPacketHeader({
618
+ ...baseHeaderParams,
619
+ marker: FrameMarker.Start,
620
+ sequence: WrapAroundUnsignedInt.u16(i * 2),
621
+ frameNumber: WrapAroundUnsignedInt.u16(i + 1),
622
+ }),
623
+ packetPayload,
624
+ );
625
+ expect(depacketizer.push(start, pushOptions)).toBeNull();
626
+ }
627
+
628
+ // Send Final for each frame. Frames 1..5 were evicted → unknownFrame; frames 6..10 produce.
629
+ let producedFrames = 0;
630
+ let unknownFrameErrors = 0;
631
+ for (let i = 0; i < totalFrames; i += 1) {
632
+ const final = new DataTrackPacket(
633
+ new DataTrackPacketHeader({
634
+ ...baseHeaderParams,
635
+ marker: FrameMarker.Final,
636
+ sequence: WrapAroundUnsignedInt.u16(i * 2 + 1),
637
+ frameNumber: WrapAroundUnsignedInt.u16(i + 1),
638
+ }),
639
+ packetPayload,
640
+ );
641
+ try {
642
+ const frame = depacketizer.push(final, pushOptions);
643
+ if (frame) {
644
+ producedFrames += 1;
645
+ }
646
+ } catch (err) {
647
+ expect((err as Error).message).toContain('Initial packet was never received.');
648
+ unknownFrameErrors += 1;
649
+ }
650
+ }
651
+
652
+ expect(producedFrames).toStrictEqual(5);
653
+ expect(unknownFrameErrors).toStrictEqual(5);
654
+ });
655
+
656
+ it('should throw unknownFrame for late Inter and Final packets belonging to an evicted frame', () => {
657
+ const depacketizer = new DataTrackDepacketizer();
658
+ const pushOptions = { throwOnInterruption: false, maxPartialFrames: 3 };
659
+
660
+ const packetPayload = new Uint8Array(8);
661
+ const baseHeaderParams = {
662
+ trackHandle: DataTrackHandle.fromNumber(101),
663
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
664
+ };
665
+
666
+ // Fill the partials map with three in-flight frames.
667
+ for (let i = 1; i <= 3; i += 1) {
668
+ const start = new DataTrackPacket(
669
+ new DataTrackPacketHeader({
670
+ ...baseHeaderParams,
671
+ marker: FrameMarker.Start,
672
+ sequence: WrapAroundUnsignedInt.u16(i * 100),
673
+ frameNumber: WrapAroundUnsignedInt.u16(i),
674
+ }),
675
+ packetPayload,
676
+ );
677
+ expect(depacketizer.push(start, pushOptions)).toBeNull();
678
+ }
679
+
680
+ // A fourth Start evicts the oldest (frame 1).
681
+ const startFour = new DataTrackPacket(
682
+ new DataTrackPacketHeader({
683
+ ...baseHeaderParams,
684
+ marker: FrameMarker.Start,
685
+ sequence: WrapAroundUnsignedInt.u16(400),
686
+ frameNumber: WrapAroundUnsignedInt.u16(4),
687
+ }),
688
+ packetPayload,
689
+ );
690
+ expect(depacketizer.push(startFour, pushOptions)).toBeNull();
691
+
692
+ // A late Inter for the evicted frame 1 should throw unknownFrame.
693
+ const lateInterOne = new DataTrackPacket(
694
+ new DataTrackPacketHeader({
695
+ ...baseHeaderParams,
696
+ marker: FrameMarker.Inter,
697
+ sequence: WrapAroundUnsignedInt.u16(101),
698
+ frameNumber: WrapAroundUnsignedInt.u16(1),
699
+ }),
700
+ packetPayload,
701
+ );
702
+ expect(() => depacketizer.push(lateInterOne, pushOptions)).toThrowError(
703
+ 'Frame 1 dropped: Initial packet was never received.',
704
+ );
705
+
706
+ // A late Final for the evicted frame 1 should also throw unknownFrame.
707
+ const lateFinalOne = new DataTrackPacket(
708
+ new DataTrackPacketHeader({
709
+ ...baseHeaderParams,
710
+ marker: FrameMarker.Final,
711
+ sequence: WrapAroundUnsignedInt.u16(102),
712
+ frameNumber: WrapAroundUnsignedInt.u16(1),
713
+ }),
714
+ packetPayload,
715
+ );
716
+ expect(() => depacketizer.push(lateFinalOne, pushOptions)).toThrowError(
717
+ 'Frame 1 dropped: Initial packet was never received.',
718
+ );
719
+
720
+ // Frames 2, 3 and 4 should all still complete cleanly despite the late packets for frame 1.
721
+ for (const frameNumber of [2, 3, 4]) {
722
+ const final = new DataTrackPacket(
723
+ new DataTrackPacketHeader({
724
+ ...baseHeaderParams,
725
+ marker: FrameMarker.Final,
726
+ sequence: WrapAroundUnsignedInt.u16(frameNumber * 100 + 1),
727
+ frameNumber: WrapAroundUnsignedInt.u16(frameNumber),
728
+ }),
729
+ packetPayload,
730
+ );
731
+ expect(depacketizer.push(final, pushOptions)).not.toBeNull();
732
+ }
733
+ });
734
+
735
+ it('should keep partial frame state isolated when packets for multiple frames are heavily interleaved', () => {
736
+ const depacketizer = new DataTrackDepacketizer();
737
+ const pushOptions = { throwOnInterruption: true, maxPartialFrames: 3 };
738
+ const baseHeaderParams = {
739
+ trackHandle: DataTrackHandle.fromNumber(101),
740
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
741
+ };
742
+
743
+ // Three frames each carrying three uniquely-tagged payloads. Sequence ranges are chosen so
744
+ // that no two frames share a sequence value.
745
+ const frames = [
746
+ {
747
+ frameNumber: 1,
748
+ startSequence: 0,
749
+ payloads: [new Uint8Array([0xa1]), new Uint8Array([0xa2]), new Uint8Array([0xa3])],
750
+ },
751
+ {
752
+ frameNumber: 2,
753
+ startSequence: 100,
754
+ payloads: [new Uint8Array([0xb1]), new Uint8Array([0xb2]), new Uint8Array([0xb3])],
755
+ },
756
+ {
757
+ frameNumber: 3,
758
+ startSequence: 200,
759
+ payloads: [new Uint8Array([0xc1]), new Uint8Array([0xc2]), new Uint8Array([0xc3])],
760
+ },
761
+ ];
762
+
763
+ const buildPacket = (
764
+ frameIndex: number,
765
+ packetIndex: number,
766
+ marker: FrameMarker,
767
+ ): DataTrackPacket =>
768
+ new DataTrackPacket(
769
+ new DataTrackPacketHeader({
770
+ ...baseHeaderParams,
771
+ marker,
772
+ sequence: WrapAroundUnsignedInt.u16(frames[frameIndex].startSequence + packetIndex),
773
+ frameNumber: WrapAroundUnsignedInt.u16(frames[frameIndex].frameNumber),
774
+ }),
775
+ frames[frameIndex].payloads[packetIndex],
776
+ );
777
+
778
+ // Round-robin Starts and Inters across all three frames.
779
+ expect(depacketizer.push(buildPacket(0, 0, FrameMarker.Start), pushOptions)).toBeNull();
780
+ expect(depacketizer.push(buildPacket(1, 0, FrameMarker.Start), pushOptions)).toBeNull();
781
+ expect(depacketizer.push(buildPacket(2, 0, FrameMarker.Start), pushOptions)).toBeNull();
782
+ expect(depacketizer.push(buildPacket(0, 1, FrameMarker.Inter), pushOptions)).toBeNull();
783
+ expect(depacketizer.push(buildPacket(1, 1, FrameMarker.Inter), pushOptions)).toBeNull();
784
+ expect(depacketizer.push(buildPacket(2, 1, FrameMarker.Inter), pushOptions)).toBeNull();
785
+
786
+ // Finals arrive in a different order than the Starts to confirm per-frame isolation.
787
+ const frameTwo = depacketizer.push(buildPacket(1, 2, FrameMarker.Final), pushOptions);
788
+ expect(frameTwo).not.toBeNull();
789
+ expect(frameTwo!.payload).toStrictEqual(new Uint8Array([0xb1, 0xb2, 0xb3]));
790
+
791
+ const frameZero = depacketizer.push(buildPacket(0, 2, FrameMarker.Final), pushOptions);
792
+ expect(frameZero).not.toBeNull();
793
+ expect(frameZero!.payload).toStrictEqual(new Uint8Array([0xa1, 0xa2, 0xa3]));
794
+
795
+ const frameThree = depacketizer.push(buildPacket(2, 2, FrameMarker.Final), pushOptions);
796
+ expect(frameThree).not.toBeNull();
797
+ expect(frameThree!.payload).toStrictEqual(new Uint8Array([0xc1, 0xc2, 0xc3]));
798
+ });
799
+
800
+ it('should respect maxPartialFrames changing across push calls, both expanding to allow more in-flight frames and shrinking to evict older ones', () => {
801
+ const depacketizer = new DataTrackDepacketizer();
802
+
803
+ const packetPayload = new Uint8Array(8);
804
+ const baseHeaderParams = {
805
+ trackHandle: DataTrackHandle.fromNumber(101),
806
+ timestamp: DataTrackTimestamp.fromRtpTicks(104),
807
+ };
808
+
809
+ const startFor = (frameNumber: number) =>
810
+ new DataTrackPacket(
811
+ new DataTrackPacketHeader({
812
+ ...baseHeaderParams,
813
+ marker: FrameMarker.Start,
814
+ sequence: WrapAroundUnsignedInt.u16(frameNumber * 100),
815
+ frameNumber: WrapAroundUnsignedInt.u16(frameNumber),
816
+ }),
817
+ packetPayload,
818
+ );
819
+
820
+ const finalFor = (frameNumber: number) =>
821
+ new DataTrackPacket(
822
+ new DataTrackPacketHeader({
823
+ ...baseHeaderParams,
824
+ marker: FrameMarker.Final,
825
+ sequence: WrapAroundUnsignedInt.u16(frameNumber * 100 + 1),
826
+ frameNumber: WrapAroundUnsignedInt.u16(frameNumber),
827
+ }),
828
+ packetPayload,
829
+ );
830
+
831
+ // Fill the partials map exactly with maxPartialFrames=2.
832
+ expect(
833
+ depacketizer.push(startFor(1), { throwOnInterruption: true, maxPartialFrames: 2 }),
834
+ ).toBeNull();
835
+ expect(
836
+ depacketizer.push(startFor(2), { throwOnInterruption: true, maxPartialFrames: 2 }),
837
+ ).toBeNull();
838
+
839
+ // Expand maxPartialFrames to 4 mid-stream. Frames 3 and 4 should be added without evicting
840
+ // anything, and throwOnInterruption: true confirms no interruption fires.
841
+ expect(
842
+ depacketizer.push(startFor(3), { throwOnInterruption: true, maxPartialFrames: 4 }),
843
+ ).toBeNull();
844
+ expect(
845
+ depacketizer.push(startFor(4), { throwOnInterruption: true, maxPartialFrames: 4 }),
846
+ ).toBeNull();
847
+
848
+ // Spot-check that frame 1 is still tracked despite the cap changes.
849
+ expect(
850
+ depacketizer.push(finalFor(1), { throwOnInterruption: true, maxPartialFrames: 4 }),
851
+ ).not.toBeNull();
852
+ // Three partials remain in flight: frames 2, 3, 4.
853
+
854
+ // Shrink maxPartialFrames to 2. Adding frame 5 should evict frames 2 and 3 in this single
855
+ // push call to bring the in-flight count back under the new cap.
856
+ expect(
857
+ depacketizer.push(startFor(5), { throwOnInterruption: false, maxPartialFrames: 2 }),
858
+ ).toBeNull();
859
+
860
+ // Only frames 4 and 5 should remain in the map.
861
+ expect(() =>
862
+ depacketizer.push(finalFor(2), { throwOnInterruption: false, maxPartialFrames: 2 }),
863
+ ).toThrowError('Frame 2 dropped: Initial packet was never received.');
864
+ expect(() =>
865
+ depacketizer.push(finalFor(3), { throwOnInterruption: false, maxPartialFrames: 2 }),
866
+ ).toThrowError('Frame 3 dropped: Initial packet was never received.');
867
+ expect(
868
+ depacketizer.push(finalFor(4), { throwOnInterruption: false, maxPartialFrames: 2 }),
869
+ ).not.toBeNull();
870
+ expect(
871
+ depacketizer.push(finalFor(5), { throwOnInterruption: false, maxPartialFrames: 2 }),
872
+ ).not.toBeNull();
873
+ });
442
874
  });