livekit-client 2.17.1 → 2.17.3

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 (145) hide show
  1. package/README.md +7 -5
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +21 -14
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2087 -1920
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/e2ee/E2eeManager.d.ts +2 -0
  11. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  12. package/dist/src/e2ee/KeyProvider.d.ts +2 -0
  13. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  14. package/dist/src/e2ee/events.d.ts +1 -1
  15. package/dist/src/e2ee/events.d.ts.map +1 -1
  16. package/dist/src/e2ee/types.d.ts +1 -0
  17. package/dist/src/e2ee/types.d.ts.map +1 -1
  18. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  19. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  20. package/dist/src/index.d.ts +7 -6
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/logger.d.ts +2 -1
  23. package/dist/src/logger.d.ts.map +1 -1
  24. package/dist/src/room/PCTransport.d.ts +1 -4
  25. package/dist/src/room/PCTransport.d.ts.map +1 -1
  26. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  27. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  28. package/dist/src/room/Room.d.ts.map +1 -1
  29. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  30. package/dist/src/room/data-stream/incoming/StreamReader.d.ts +2 -4
  31. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  32. package/dist/src/room/data-track/depacketizer.d.ts +51 -0
  33. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -0
  34. package/dist/src/room/data-track/e2ee.d.ts +12 -0
  35. package/dist/src/room/data-track/e2ee.d.ts.map +1 -0
  36. package/dist/src/room/data-track/frame.d.ts +7 -0
  37. package/dist/src/room/data-track/frame.d.ts.map +1 -0
  38. package/dist/src/room/data-track/handle.d.ts +6 -7
  39. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  40. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +76 -0
  41. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -0
  42. package/dist/src/room/data-track/outgoing/errors.d.ts +64 -0
  43. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -0
  44. package/dist/src/room/data-track/outgoing/pipeline.d.ts +22 -0
  45. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -0
  46. package/dist/src/room/data-track/outgoing/types.d.ts +31 -0
  47. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -0
  48. package/dist/src/room/data-track/packet/index.d.ts +3 -3
  49. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  50. package/dist/src/room/data-track/packetizer.d.ts +43 -0
  51. package/dist/src/room/data-track/packetizer.d.ts.map +1 -0
  52. package/dist/src/room/data-track/track.d.ts +30 -0
  53. package/dist/src/room/data-track/track.d.ts.map +1 -0
  54. package/dist/src/room/data-track/utils.d.ts +34 -2
  55. package/dist/src/room/data-track/utils.d.ts.map +1 -1
  56. package/dist/src/room/debounce.d.ts +11 -0
  57. package/dist/src/room/debounce.d.ts.map +1 -0
  58. package/dist/src/room/events.d.ts +1 -1
  59. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  60. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  61. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  62. package/dist/src/room/track/LocalTrack.d.ts +2 -1
  63. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  64. package/dist/src/room/types.d.ts +0 -2
  65. package/dist/src/room/types.d.ts.map +1 -1
  66. package/dist/src/room/utils.d.ts +6 -1
  67. package/dist/src/room/utils.d.ts.map +1 -1
  68. package/dist/src/utils/subscribeToEvents.d.ts +12 -0
  69. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -0
  70. package/dist/src/utils/throws.d.ts +4 -2
  71. package/dist/src/utils/throws.d.ts.map +1 -1
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +2 -0
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +2 -0
  74. package/dist/ts4.2/e2ee/events.d.ts +1 -1
  75. package/dist/ts4.2/e2ee/types.d.ts +1 -0
  76. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
  77. package/dist/ts4.2/index.d.ts +7 -3
  78. package/dist/ts4.2/logger.d.ts +2 -1
  79. package/dist/ts4.2/room/PCTransport.d.ts +1 -6
  80. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +2 -4
  81. package/dist/ts4.2/room/data-track/depacketizer.d.ts +51 -0
  82. package/dist/ts4.2/room/data-track/e2ee.d.ts +12 -0
  83. package/dist/ts4.2/room/data-track/frame.d.ts +7 -0
  84. package/dist/ts4.2/room/data-track/handle.d.ts +6 -7
  85. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +77 -0
  86. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +64 -0
  87. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +22 -0
  88. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +31 -0
  89. package/dist/ts4.2/room/data-track/packet/index.d.ts +3 -3
  90. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -0
  91. package/dist/ts4.2/room/data-track/track.d.ts +30 -0
  92. package/dist/ts4.2/room/data-track/utils.d.ts +34 -2
  93. package/dist/ts4.2/room/debounce.d.ts +11 -0
  94. package/dist/ts4.2/room/events.d.ts +1 -1
  95. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -1
  96. package/dist/ts4.2/room/track/LocalTrack.d.ts +2 -1
  97. package/dist/ts4.2/room/types.d.ts +0 -2
  98. package/dist/ts4.2/room/utils.d.ts +6 -1
  99. package/dist/ts4.2/utils/subscribeToEvents.d.ts +12 -0
  100. package/dist/ts4.2/utils/throws.d.ts +4 -2
  101. package/package.json +4 -5
  102. package/src/e2ee/E2eeManager.ts +9 -5
  103. package/src/e2ee/KeyProvider.ts +10 -1
  104. package/src/e2ee/events.ts +1 -1
  105. package/src/e2ee/types.ts +1 -0
  106. package/src/e2ee/worker/ParticipantKeyHandler.ts +7 -4
  107. package/src/e2ee/worker/e2ee.worker.ts +20 -10
  108. package/src/index.ts +15 -5
  109. package/src/logger.ts +1 -0
  110. package/src/room/PCTransport.ts +2 -1
  111. package/src/room/PCTransportManager.ts +27 -9
  112. package/src/room/RTCEngine.ts +13 -2
  113. package/src/room/Room.ts +11 -5
  114. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -25
  115. package/src/room/data-stream/incoming/StreamReader.ts +56 -73
  116. package/src/room/data-track/depacketizer.test.ts +442 -0
  117. package/src/room/data-track/depacketizer.ts +298 -0
  118. package/src/room/data-track/e2ee.ts +14 -0
  119. package/src/room/data-track/frame.ts +8 -0
  120. package/src/room/data-track/handle.test.ts +1 -1
  121. package/src/room/data-track/handle.ts +9 -14
  122. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +392 -0
  123. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +302 -0
  124. package/src/room/data-track/outgoing/errors.ts +157 -0
  125. package/src/room/data-track/outgoing/pipeline.ts +76 -0
  126. package/src/room/data-track/outgoing/types.ts +37 -0
  127. package/src/room/data-track/packet/index.test.ts +9 -9
  128. package/src/room/data-track/packet/index.ts +11 -9
  129. package/src/room/data-track/packet/serializable.ts +1 -1
  130. package/src/room/data-track/packetizer.test.ts +131 -0
  131. package/src/room/data-track/packetizer.ts +132 -0
  132. package/src/room/data-track/track.ts +50 -0
  133. package/src/room/data-track/utils.test.ts +27 -1
  134. package/src/room/data-track/utils.ts +125 -5
  135. package/src/room/debounce.ts +115 -0
  136. package/src/room/events.ts +1 -1
  137. package/src/room/participant/LocalParticipant.ts +2 -0
  138. package/src/room/track/LocalAudioTrack.ts +10 -10
  139. package/src/room/track/LocalTrack.ts +14 -5
  140. package/src/room/track/LocalVideoTrack.ts +1 -1
  141. package/src/room/track/RemoteVideoTrack.ts +1 -1
  142. package/src/room/types.ts +0 -2
  143. package/src/room/utils.ts +7 -2
  144. package/src/utils/subscribeToEvents.ts +63 -0
  145. package/src/utils/throws.ts +3 -1
@@ -0,0 +1,157 @@
1
+ import { LivekitReasonedError } from '../../errors';
2
+ import { DataTrackPacketizerError, DataTrackPacketizerReason } from '../packetizer';
3
+
4
+ export enum DataTrackPublishErrorReason {
5
+ /**
6
+ * Local participant does not have permission to publish data tracks.
7
+ *
8
+ * Ensure the participant's token contains the `canPublishData` grant.
9
+ */
10
+ NotAllowed = 0,
11
+
12
+ /** A track with the same name is already published by the local participant. */
13
+ DuplicateName = 1,
14
+
15
+ /** Request to publish the track took long to complete. */
16
+ Timeout = 2,
17
+
18
+ /** No additional data tracks can be published by the local participant. */
19
+ LimitReached = 3,
20
+
21
+ /** Cannot publish data track when the room is disconnected. */
22
+ Disconnected = 4,
23
+
24
+ // NOTE: this was introduced by web / there isn't a corresponding case in the rust version.
25
+ Cancelled = 5,
26
+ }
27
+
28
+ export class DataTrackPublishError<
29
+ Reason extends DataTrackPublishErrorReason,
30
+ > extends LivekitReasonedError<Reason> {
31
+ readonly name = 'DataTrackPublishError';
32
+
33
+ reason: Reason;
34
+
35
+ reasonName: string;
36
+
37
+ constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
38
+ super(21, message, options);
39
+ this.reason = reason;
40
+ this.reasonName = DataTrackPublishErrorReason[reason];
41
+ }
42
+
43
+ static notAllowed() {
44
+ return new DataTrackPublishError(
45
+ 'Data track publishing unauthorized',
46
+ DataTrackPublishErrorReason.NotAllowed,
47
+ );
48
+ }
49
+
50
+ static duplicateName() {
51
+ return new DataTrackPublishError(
52
+ 'Track name already taken',
53
+ DataTrackPublishErrorReason.DuplicateName,
54
+ );
55
+ }
56
+
57
+ static timeout() {
58
+ return new DataTrackPublishError(
59
+ 'Publish data track timed-out',
60
+ DataTrackPublishErrorReason.Timeout,
61
+ );
62
+ }
63
+
64
+ static limitReached() {
65
+ return new DataTrackPublishError(
66
+ 'Data track publication limit reached',
67
+ DataTrackPublishErrorReason.LimitReached,
68
+ );
69
+ }
70
+
71
+ static disconnected() {
72
+ return new DataTrackPublishError('Room disconnected', DataTrackPublishErrorReason.Disconnected);
73
+ }
74
+
75
+ // NOTE: this was introduced by web / there isn't a corresponding case in the rust version.
76
+ static cancelled() {
77
+ return new DataTrackPublishError(
78
+ 'Publish data track cancelled by caller',
79
+ DataTrackPublishErrorReason.Cancelled,
80
+ );
81
+ }
82
+ }
83
+
84
+ export enum DataTrackPushFrameErrorReason {
85
+ /** Track is no longer published. */
86
+ TrackUnpublished = 0,
87
+ /** Frame was dropped. */
88
+ // NOTE: this should become a web specific error, the rust version of this "dropped" error means
89
+ // something different and will be renamed to "QueueFull".
90
+ Dropped = 1,
91
+ }
92
+
93
+ export class DataTrackPushFrameError<
94
+ Reason extends DataTrackPushFrameErrorReason,
95
+ > extends LivekitReasonedError<Reason> {
96
+ readonly name = 'DataTrackPushFrameError';
97
+
98
+ reason: Reason;
99
+
100
+ reasonName: string;
101
+
102
+ constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
103
+ super(22, message, options);
104
+ this.reason = reason;
105
+ this.reasonName = DataTrackPushFrameErrorReason[reason];
106
+ }
107
+
108
+ static trackUnpublished() {
109
+ return new DataTrackPushFrameError(
110
+ 'Track is no longer published',
111
+ DataTrackPushFrameErrorReason.TrackUnpublished,
112
+ );
113
+ }
114
+
115
+ static dropped(cause: unknown) {
116
+ return new DataTrackPushFrameError('Frame was dropped', DataTrackPushFrameErrorReason.Dropped, {
117
+ cause,
118
+ });
119
+ }
120
+ }
121
+
122
+ export enum DataTrackOutgoingPipelineErrorReason {
123
+ Packetizer = 0,
124
+ Encryption = 1,
125
+ }
126
+
127
+ export class DataTrackOutgoingPipelineError<
128
+ Reason extends DataTrackOutgoingPipelineErrorReason,
129
+ > extends LivekitReasonedError<Reason> {
130
+ readonly name = 'DataTrackOutgoingPipelineError';
131
+
132
+ reason: Reason;
133
+
134
+ reasonName: string;
135
+
136
+ constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
137
+ super(21, message, options);
138
+ this.reason = reason;
139
+ this.reasonName = DataTrackOutgoingPipelineErrorReason[reason];
140
+ }
141
+
142
+ static packetizer(cause: DataTrackPacketizerError<DataTrackPacketizerReason.MtuTooShort>) {
143
+ return new DataTrackOutgoingPipelineError(
144
+ 'Error packetizing frame',
145
+ DataTrackOutgoingPipelineErrorReason.Packetizer,
146
+ { cause },
147
+ );
148
+ }
149
+
150
+ static encryption(cause: unknown) {
151
+ return new DataTrackOutgoingPipelineError(
152
+ 'Error encrypting frame',
153
+ DataTrackOutgoingPipelineErrorReason.Encryption,
154
+ { cause },
155
+ );
156
+ }
157
+ }
@@ -0,0 +1,76 @@
1
+ import { type Throws } from '../../../utils/throws';
2
+ import { type EncryptedPayload, type EncryptionProvider } from '../e2ee';
3
+ import { type DataTrackFrame } from '../frame';
4
+ import { DataTrackPacket } from '../packet';
5
+ import { DataTrackE2eeExtension } from '../packet/extensions';
6
+ import DataTrackPacketizer, { DataTrackPacketizerError } from '../packetizer';
7
+ import type { DataTrackInfo } from '../track';
8
+ import { DataTrackOutgoingPipelineError, DataTrackOutgoingPipelineErrorReason } from './errors';
9
+
10
+ type Options = {
11
+ info: DataTrackInfo;
12
+ encryptionProvider: EncryptionProvider | null;
13
+ };
14
+
15
+ /** Processes outgoing frames into final packets for distribution to the SFU. */
16
+ export default class DataTrackOutgoingPipeline {
17
+ private encryptionProvider: EncryptionProvider | null;
18
+
19
+ private packetizer: DataTrackPacketizer;
20
+
21
+ /** Maximum transmission unit (MTU) of the transport. */
22
+ private static TRANSPORT_MTU_BYTES = 16_000;
23
+
24
+ constructor(options: Options) {
25
+ this.encryptionProvider = options.encryptionProvider;
26
+ this.packetizer = new DataTrackPacketizer(
27
+ options.info.pubHandle,
28
+ DataTrackOutgoingPipeline.TRANSPORT_MTU_BYTES,
29
+ );
30
+ }
31
+
32
+ *processFrame(
33
+ frame: DataTrackFrame,
34
+ ): Throws<
35
+ Generator<DataTrackPacket>,
36
+ | DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Packetizer>
37
+ | DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Encryption>
38
+ > {
39
+ const encryptedFrame = this.encryptIfNeeded(frame);
40
+
41
+ try {
42
+ yield* this.packetizer.packetize(encryptedFrame);
43
+ } catch (error) {
44
+ if (error instanceof DataTrackPacketizerError) {
45
+ throw DataTrackOutgoingPipelineError.packetizer(error);
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ encryptIfNeeded(
52
+ frame: DataTrackFrame,
53
+ ): Throws<
54
+ DataTrackFrame,
55
+ DataTrackOutgoingPipelineError<DataTrackOutgoingPipelineErrorReason.Encryption>
56
+ > {
57
+ if (!this.encryptionProvider) {
58
+ return frame;
59
+ }
60
+
61
+ let encryptedResult: EncryptedPayload;
62
+ try {
63
+ encryptedResult = this.encryptionProvider.encrypt(frame.payload);
64
+ } catch (err) {
65
+ throw DataTrackOutgoingPipelineError.encryption(err);
66
+ }
67
+
68
+ frame.payload = encryptedResult.payload;
69
+ frame.extensions.e2ee = new DataTrackE2eeExtension(
70
+ encryptedResult.keyIndex,
71
+ encryptedResult.iv,
72
+ );
73
+
74
+ return frame;
75
+ }
76
+ }
@@ -0,0 +1,37 @@
1
+ import { type DataTrackHandle } from '../handle';
2
+ import { type DataTrackInfo } from '../track';
3
+ import { type DataTrackPublishError, type DataTrackPublishErrorReason } from './errors';
4
+
5
+ /** Options for publishing a data track. */
6
+ export type DataTrackOptions = {
7
+ name: string;
8
+ };
9
+
10
+ /** Encodes whether a data track publish request to the SFU has been successful or not. */
11
+ export type SfuPublishResponseResult =
12
+ | { type: 'ok'; data: DataTrackInfo }
13
+ | {
14
+ type: 'error';
15
+ error:
16
+ | DataTrackPublishError<DataTrackPublishErrorReason.NotAllowed>
17
+ | DataTrackPublishError<DataTrackPublishErrorReason.DuplicateName>
18
+ | DataTrackPublishError<DataTrackPublishErrorReason.LimitReached>;
19
+ };
20
+
21
+ /** Request sent to the SFU to publish a track. */
22
+ export type OutputEventSfuPublishRequest = {
23
+ handle: DataTrackHandle;
24
+ name: string;
25
+ usesE2ee: boolean;
26
+ };
27
+
28
+ /** Request sent to the SFU to unpublish a track. */
29
+ export type OutputEventSfuUnpublishRequest = {
30
+ handle: DataTrackHandle;
31
+ };
32
+
33
+ /** Serialized packets are ready to be sent over the transport. */
34
+ export type OutputEventPacketsAvailable = {
35
+ bytes: Uint8Array;
36
+ signal?: AbortSignal;
37
+ };
@@ -24,7 +24,7 @@ describe('DataTrackPacket', () => {
24
24
 
25
25
  const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
26
26
 
27
- const packet = new DataTrackPacket(header, payloadBytes.buffer);
27
+ const packet = new DataTrackPacket(header, payloadBytes);
28
28
 
29
29
  expect(packet.toBinaryLengthBytes()).toStrictEqual(22);
30
30
  expect(packet.toBinary()).toStrictEqual(
@@ -70,7 +70,7 @@ describe('DataTrackPacket', () => {
70
70
 
71
71
  const payloadBytes = new Uint8Array(32).fill(0xfa);
72
72
 
73
- const packet = new DataTrackPacket(header, payloadBytes.buffer);
73
+ const packet = new DataTrackPacket(header, payloadBytes);
74
74
 
75
75
  expect(packet.toBinaryLengthBytes()).toStrictEqual(78);
76
76
  expect(packet.toBinary()).toStrictEqual(
@@ -176,7 +176,7 @@ describe('DataTrackPacket', () => {
176
176
 
177
177
  const payloadBytes = new Uint8Array(32).fill(0xfa);
178
178
 
179
- const packet = new DataTrackPacket(header, payloadBytes.buffer);
179
+ const packet = new DataTrackPacket(header, payloadBytes);
180
180
 
181
181
  expect(packet.toBinaryLengthBytes()).toStrictEqual(66);
182
182
  expect(packet.toBinary()).toStrictEqual(
@@ -264,7 +264,7 @@ describe('DataTrackPacket', () => {
264
264
  timestamp: DataTrackTimestamp.fromRtpTicks(104),
265
265
  });
266
266
  const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
267
- const packet = new DataTrackPacket(header, payloadBytes.buffer);
267
+ const packet = new DataTrackPacket(header, payloadBytes);
268
268
 
269
269
  const twoByteLongDataView = new DataView(new ArrayBuffer(2));
270
270
  expect(() => packet.toBinaryInto(twoByteLongDataView)).toThrow('Buffer cannot fit header');
@@ -278,7 +278,7 @@ describe('DataTrackPacket', () => {
278
278
  timestamp: DataTrackTimestamp.fromRtpTicks(104),
279
279
  });
280
280
  const payloadBytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
281
- const packet = new DataTrackPacket(header, payloadBytes.buffer);
281
+ const packet = new DataTrackPacket(header, payloadBytes);
282
282
 
283
283
  const fourteenByteLongDataView = new DataView(
284
284
  new ArrayBuffer(14 /* 12 byte header + 2 extra bytes */),
@@ -333,7 +333,7 @@ describe('DataTrackPacket', () => {
333
333
  userTimestamp: null,
334
334
  },
335
335
  },
336
- payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]).buffer,
336
+ payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]),
337
337
  });
338
338
  });
339
339
 
@@ -406,7 +406,7 @@ describe('DataTrackPacket', () => {
406
406
  userTimestamp: null,
407
407
  },
408
408
  },
409
- payload: new Uint8Array([]).buffer,
409
+ payload: new Uint8Array([]),
410
410
  });
411
411
  });
412
412
 
@@ -563,7 +563,7 @@ describe('DataTrackPacket', () => {
563
563
 
564
564
  const payloadBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]);
565
565
 
566
- const encodedPacket = new DataTrackPacket(header, payloadBytes.buffer);
566
+ const encodedPacket = new DataTrackPacket(header, payloadBytes);
567
567
 
568
568
  expect(encodedPacket.toBinaryLengthBytes()).toStrictEqual(21);
569
569
  expect(encodedPacket.toBinary()).toStrictEqual(
@@ -608,7 +608,7 @@ describe('DataTrackPacket', () => {
608
608
  userTimestamp: null,
609
609
  },
610
610
  },
611
- payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]).buffer,
611
+ payload: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]),
612
612
  });
613
613
  });
614
614
  });
@@ -126,7 +126,7 @@ export class DataTrackPacketHeader extends Serializable {
126
126
  dataView.setUint8(byteIndex, 0); // Reserved
127
127
  byteIndex += U8_LENGTH_BYTES;
128
128
 
129
- dataView.setUint16(byteIndex, this.trackHandle.value);
129
+ dataView.setUint16(byteIndex, this.trackHandle);
130
130
  byteIndex += U16_LENGTH_BYTES;
131
131
  dataView.setUint16(byteIndex, this.sequence.value);
132
132
  byteIndex += U16_LENGTH_BYTES;
@@ -277,10 +277,10 @@ export class DataTrackPacketHeader extends Serializable {
277
277
  toJSON() {
278
278
  return {
279
279
  marker: this.marker,
280
- trackHandle: this.trackHandle.value,
280
+ trackHandle: this.trackHandle,
281
281
  sequence: this.sequence.value,
282
282
  frameNumber: this.frameNumber.value,
283
- timestamp: this.timestamp.timestamp,
283
+ timestamp: this.timestamp.asTicks(),
284
284
  extensions: this.extensions.toJSON(),
285
285
  };
286
286
  }
@@ -302,9 +302,9 @@ export enum FrameMarker {
302
302
  export class DataTrackPacket extends Serializable {
303
303
  header: DataTrackPacketHeader;
304
304
 
305
- payload: ArrayBuffer;
305
+ payload: Uint8Array;
306
306
 
307
- constructor(header: DataTrackPacketHeader, payload: ArrayBuffer) {
307
+ constructor(header: DataTrackPacketHeader, payload: Uint8Array) {
308
308
  super();
309
309
  this.header = header;
310
310
  this.payload = payload;
@@ -323,9 +323,8 @@ export class DataTrackPacket extends Serializable {
323
323
  throw DataTrackSerializeError.tooSmallForPayload();
324
324
  }
325
325
 
326
- const payloadBytes = new Uint8Array(this.payload);
327
- for (let index = 0; index < payloadBytes.length; index += 1) {
328
- dataView.setUint8(byteIndex, payloadBytes[index]);
326
+ for (let index = 0; index < this.payload.length; index += 1) {
327
+ dataView.setUint8(byteIndex, this.payload[index]);
329
328
  byteIndex += U8_LENGTH_BYTES;
330
329
  }
331
330
 
@@ -352,7 +351,10 @@ export class DataTrackPacket extends Serializable {
352
351
  dataView.byteOffset + dataView.byteLength,
353
352
  );
354
353
 
355
- return [new DataTrackPacket(header, payload), dataView.byteLength] as [DataTrackPacket, number];
354
+ return [new DataTrackPacket(header, new Uint8Array(payload)), dataView.byteLength] as [
355
+ DataTrackPacket,
356
+ number,
357
+ ];
356
358
  }
357
359
 
358
360
  toJSON() {
@@ -24,6 +24,6 @@ export default abstract class Serializable {
24
24
  );
25
25
  }
26
26
 
27
- return new Uint8Array(output); // FIXME: return uint8array here? Or the arraybuffer?
27
+ return new Uint8Array(output);
28
28
  }
29
29
  }
@@ -0,0 +1,131 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { DataTrackFrame } from './frame';
4
+ import { DataTrackHandle } from './handle';
5
+ import { FrameMarker } from './packet';
6
+ import { DataTrackExtensions } from './packet/extensions';
7
+ import DataTrackPacketizer from './packetizer';
8
+ import { DataTrackTimestamp } from './utils';
9
+
10
+ describe('DataTrackPacketizer', () => {
11
+ it('should packetize a large payload properly', () => {
12
+ const packetizer = new DataTrackPacketizer(DataTrackHandle.fromNumber(1), 100);
13
+ const packets = Array.from(
14
+ packetizer.packetize(
15
+ {
16
+ payload: new Uint8Array(300).fill(0xbe),
17
+ extensions: new DataTrackExtensions(),
18
+ },
19
+ { now: DataTrackTimestamp.fromRtpTicks(1804548298) },
20
+ ),
21
+ );
22
+
23
+ expect(packets.map((packet) => packet.toJSON())).toStrictEqual([
24
+ {
25
+ header: {
26
+ extensions: {
27
+ e2ee: null,
28
+ userTimestamp: null,
29
+ },
30
+ frameNumber: 0,
31
+ marker: FrameMarker.Start,
32
+ sequence: 0,
33
+ timestamp: 1804548298,
34
+ trackHandle: 1,
35
+ },
36
+ payload: new Uint8Array(88).fill(0xbe),
37
+ },
38
+ {
39
+ header: {
40
+ extensions: {
41
+ e2ee: null,
42
+ userTimestamp: null,
43
+ },
44
+ frameNumber: 0,
45
+ marker: FrameMarker.Inter,
46
+ sequence: 1,
47
+ timestamp: 1804548298,
48
+ trackHandle: 1,
49
+ },
50
+ payload: new Uint8Array(88).fill(0xbe),
51
+ },
52
+ {
53
+ header: {
54
+ extensions: {
55
+ e2ee: null,
56
+ userTimestamp: null,
57
+ },
58
+ frameNumber: 0,
59
+ marker: FrameMarker.Inter,
60
+ sequence: 2,
61
+ timestamp: 1804548298,
62
+ trackHandle: 1,
63
+ },
64
+ payload: new Uint8Array(88).fill(0xbe),
65
+ },
66
+ {
67
+ header: {
68
+ extensions: {
69
+ e2ee: null,
70
+ userTimestamp: null,
71
+ },
72
+ frameNumber: 0,
73
+ marker: FrameMarker.Final,
74
+ sequence: 3,
75
+ timestamp: 1804548298,
76
+ trackHandle: 1,
77
+ },
78
+ payload: new Uint8Array(36 /* 300 total bytes - (88 bytes * 3 full length packets) */).fill(
79
+ 0xbe,
80
+ ),
81
+ },
82
+ ]);
83
+ });
84
+
85
+ it.each([
86
+ [0, 1_024, 'zero payload'],
87
+ [128, 1_024, 'single packet'],
88
+ [20_480, 1_024, 'multi packet'],
89
+ [40_960, 16_000, 'multi packet mtu 16000'],
90
+ ])('should test packetizer edge cases', (payloadSizeBytes, mtuSizeBytes, label) => {
91
+ const packetizer = new DataTrackPacketizer(DataTrackHandle.fromNumber(1), mtuSizeBytes);
92
+
93
+ const frame: DataTrackFrame = {
94
+ payload: new Uint8Array(payloadSizeBytes).fill(0xab),
95
+ extensions: new DataTrackExtensions(),
96
+ };
97
+ const packets = Array.from(
98
+ packetizer.packetize(frame, {
99
+ now: DataTrackTimestamp.fromRtpTicks(0),
100
+ }),
101
+ );
102
+
103
+ if (packets.length === 0) {
104
+ expect(payloadSizeBytes, `${label}: Should be no packets for zero payload`).toStrictEqual(0);
105
+ }
106
+
107
+ let index = 0;
108
+ for (const packet of packets) {
109
+ const packetHeaderJson = packet.header.toJSON();
110
+ expect(packetHeaderJson.marker).toStrictEqual(
111
+ DataTrackPacketizer.computeFrameMarker(index, packets.length),
112
+ );
113
+ expect(packetHeaderJson.frameNumber).toStrictEqual(0);
114
+ expect(packetHeaderJson.trackHandle).toStrictEqual(1);
115
+ expect(packetHeaderJson.sequence).toStrictEqual(index);
116
+ expect(packetHeaderJson.extensions).toStrictEqual(frame.extensions.toJSON());
117
+ index += 1;
118
+ }
119
+ });
120
+
121
+ it.each([
122
+ [0, 1, FrameMarker.Single],
123
+ [0, 10, FrameMarker.Start],
124
+ [4, 10, FrameMarker.Inter],
125
+ [9, 10, FrameMarker.Final],
126
+ ])('should test frame marker utility function', (index, packetCount, expectedMarker) => {
127
+ expect(DataTrackPacketizer.computeFrameMarker(index, packetCount)).toStrictEqual(
128
+ expectedMarker,
129
+ );
130
+ });
131
+ });
@@ -0,0 +1,132 @@
1
+ import type { Throws } from '../../utils/throws';
2
+ import { LivekitReasonedError } from '../errors';
3
+ import { type DataTrackFrame } from './frame';
4
+ import { DataTrackHandle } from './handle';
5
+ import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from './packet';
6
+ import { DataTrackClock, DataTrackTimestamp, WrapAroundUnsignedInt } from './utils';
7
+
8
+ type PacketizeOptions = {
9
+ /** "now" timestamp to use as a base when generating new packet timestamps. If not specified,
10
+ * defaults to the return value of {@link DataTrackClock#now}. */
11
+ now?: DataTrackTimestamp<90_000>;
12
+ };
13
+
14
+ export class DataTrackPacketizerError<
15
+ Reason extends DataTrackPacketizerReason,
16
+ > extends LivekitReasonedError<Reason> {
17
+ readonly name = 'DataTrackPacketizerError';
18
+
19
+ reason: Reason;
20
+
21
+ reasonName: string;
22
+
23
+ constructor(message: string, reason: Reason, options?: { cause?: unknown }) {
24
+ super(19, message, options);
25
+ this.reason = reason;
26
+ this.reasonName = DataTrackPacketizerReason[reason];
27
+ }
28
+
29
+ static mtuTooShort() {
30
+ return new DataTrackPacketizerError(
31
+ 'MTU is too short to send frame',
32
+ DataTrackPacketizerReason.MtuTooShort,
33
+ );
34
+ }
35
+ }
36
+
37
+ export enum DataTrackPacketizerReason {
38
+ MtuTooShort = 0,
39
+ }
40
+
41
+ /** A packetizer takes a {@link DataTrackFrame} as input and generates a series
42
+ * of {@link DataTrackPacket}s for transmission to other clients over webrtc. */
43
+ export default class DataTrackPacketizer {
44
+ private handle: DataTrackHandle;
45
+
46
+ private mtuSizeBytes: number;
47
+
48
+ private sequence = WrapAroundUnsignedInt.u16(0);
49
+
50
+ private frameNumber = WrapAroundUnsignedInt.u16(0);
51
+
52
+ private clock = DataTrackClock.rtpStartingNow(DataTrackTimestamp.rtpRandom());
53
+
54
+ constructor(trackHandle: DataTrackHandle, mtuSizeBytes: number) {
55
+ this.handle = trackHandle;
56
+ this.mtuSizeBytes = mtuSizeBytes;
57
+ }
58
+
59
+ /** @internal */
60
+ static computeFrameMarker(index: number, packetCount: number) {
61
+ if (packetCount <= 1) {
62
+ return FrameMarker.Single;
63
+ }
64
+ if (index === 0) {
65
+ return FrameMarker.Start;
66
+ } else if (index === packetCount - 1) {
67
+ return FrameMarker.Final;
68
+ } else {
69
+ return FrameMarker.Inter;
70
+ }
71
+ }
72
+
73
+ /** Generates a series of packets for the specified {@link DataTrackPacketizerFrame}.
74
+ *
75
+ * NOTE: The return value of this function is a generator, so it can be lazily ran if desired,
76
+ * or converted to an array with {@link Array.from}.
77
+ */
78
+ *packetize(
79
+ frame: DataTrackFrame,
80
+ options?: PacketizeOptions,
81
+ ): Throws<
82
+ Generator<DataTrackPacket>,
83
+ DataTrackPacketizerError<DataTrackPacketizerReason.MtuTooShort>
84
+ > {
85
+ const frameNumber = this.frameNumber.getThenIncrement();
86
+ const headerParams = {
87
+ marker: FrameMarker.Inter,
88
+ trackHandle: this.handle,
89
+ sequence: WrapAroundUnsignedInt.u16(0),
90
+ frameNumber,
91
+ timestamp: options?.now ?? this.clock.now(),
92
+ extensions: frame.extensions,
93
+ };
94
+ const headerSerializedLengthBytes = new DataTrackPacketHeader(
95
+ headerParams,
96
+ ).toBinaryLengthBytes();
97
+ if (headerSerializedLengthBytes >= this.mtuSizeBytes) {
98
+ throw DataTrackPacketizerError.mtuTooShort();
99
+ }
100
+
101
+ const maxPayloadSizeBytes = this.mtuSizeBytes - headerSerializedLengthBytes;
102
+
103
+ const packetCount = Math.ceil(frame.payload.byteLength / maxPayloadSizeBytes);
104
+
105
+ for (
106
+ let index = 0, indexBytes = 0;
107
+ indexBytes < frame.payload.byteLength;
108
+ [index, indexBytes] = [index + 1, indexBytes + maxPayloadSizeBytes]
109
+ ) {
110
+ const sequence = this.sequence.getThenIncrement();
111
+ const packetHeader = new DataTrackPacketHeader({
112
+ ...headerParams,
113
+ marker: DataTrackPacketizer.computeFrameMarker(index, packetCount),
114
+ sequence,
115
+ });
116
+
117
+ const packetPayloadLengthBytes = Math.min(
118
+ // All but the last packet will be max length ...
119
+ maxPayloadSizeBytes,
120
+ // ... and the last packet will be as long as it needs to be to finish out the buffer.
121
+ frame.payload.byteLength - indexBytes,
122
+ );
123
+ const packetPayload = new Uint8Array(
124
+ frame.payload.buffer,
125
+ frame.payload.byteOffset + indexBytes,
126
+ packetPayloadLengthBytes,
127
+ );
128
+
129
+ yield new DataTrackPacket(packetHeader, packetPayload);
130
+ }
131
+ }
132
+ }