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,392 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { subscribeToEvents } from '../../../utils/subscribeToEvents';
4
+ import { EncryptionProvider } from '../e2ee';
5
+ import { DataTrackHandle } from '../handle';
6
+ import { DataTrackPacket, FrameMarker } from '../packet';
7
+ import OutgoingDataTrackManager, {
8
+ DataTrackOutgoingManagerCallbacks,
9
+ Descriptor,
10
+ } from './OutgoingDataTrackManager';
11
+ import { DataTrackPublishError } from './errors';
12
+
13
+ /** A fake "encryption" provider used for test purposes. Adds a prefix to the payload. */
14
+ const PrefixingEncryptionProvider: EncryptionProvider = {
15
+ encrypt(payload: Uint8Array) {
16
+ const prefix = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
17
+
18
+ const output = new Uint8Array(prefix.length + payload.length);
19
+ output.set(prefix, 0);
20
+ output.set(payload, prefix.length);
21
+
22
+ return {
23
+ payload: output,
24
+ iv: new Uint8Array(12), // Just leaving this empty, is this a bad idea?
25
+ keyIndex: 0,
26
+ };
27
+ },
28
+ };
29
+
30
+ describe('DataTrackOutgoingManager', () => {
31
+ it('should test track publishing (ok case)', async () => {
32
+ const manager = new OutgoingDataTrackManager();
33
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
34
+ 'sfuPublishRequest',
35
+ ]);
36
+
37
+ // 1. Publish a data track
38
+ const publishRequestPromise = manager.publishRequest({ name: 'test' });
39
+
40
+ // 2. This publish request should be sent along to the SFU
41
+ const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest');
42
+ expect(sfuPublishEvent.name).toStrictEqual('test');
43
+ expect(sfuPublishEvent.usesE2ee).toStrictEqual(false);
44
+ const handle = sfuPublishEvent.handle;
45
+
46
+ // 3. Respond to the SFU publish request with an OK response
47
+ manager.receivedSfuPublishResponse(handle, {
48
+ type: 'ok',
49
+ data: {
50
+ sid: 'bogus-sid',
51
+ pubHandle: sfuPublishEvent.handle,
52
+ name: 'test',
53
+ usesE2ee: false,
54
+ },
55
+ });
56
+
57
+ // Make sure that the original input event resolves.
58
+ const localDataTrack = await publishRequestPromise;
59
+ expect(localDataTrack.isPublished()).toStrictEqual(true);
60
+ });
61
+
62
+ it('should test track publishing (error case)', async () => {
63
+ const manager = new OutgoingDataTrackManager();
64
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
65
+ 'sfuPublishRequest',
66
+ ]);
67
+
68
+ // 1. Publish a data track
69
+ const publishRequestPromise = manager.publishRequest({ name: 'test' });
70
+
71
+ // 2. This publish request should be sent along to the SFU
72
+ const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest');
73
+
74
+ // 3. Respond to the SFU publish request with an ERROR response
75
+ manager.receivedSfuPublishResponse(sfuPublishEvent.handle, {
76
+ type: 'error',
77
+ error: DataTrackPublishError.limitReached(),
78
+ });
79
+
80
+ // Make sure that the rejection bubbles back to the caller
81
+ expect(publishRequestPromise).rejects.toThrowError('Data track publication limit reached');
82
+ });
83
+
84
+ it('should test track publishing (cancellation half way through)', async () => {
85
+ const manager = new OutgoingDataTrackManager();
86
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
87
+ 'sfuPublishRequest',
88
+ 'sfuUnpublishRequest',
89
+ ]);
90
+
91
+ // 1. Publish a data track
92
+ const controller = new AbortController();
93
+ const publishRequestPromise = manager.publishRequest({ name: 'test' }, controller.signal);
94
+
95
+ // 2. This publish request should be sent along to the SFU
96
+ const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest');
97
+ expect(sfuPublishEvent.name).toStrictEqual('test');
98
+ expect(sfuPublishEvent.usesE2ee).toStrictEqual(false);
99
+ const handle = sfuPublishEvent.handle;
100
+
101
+ // 3. Explictly cancel the publish
102
+ controller.abort();
103
+
104
+ // 4. Make sure an unpublish event is sent so that the SFU cleans up things properly
105
+ // on its end as well
106
+ const sfuUnpublishEvent = await managerEvents.waitFor('sfuUnpublishRequest');
107
+ expect(sfuUnpublishEvent.handle).toStrictEqual(handle);
108
+
109
+ // 5. Make sure cancellation is bubbled up as an error to stop further execution
110
+ expect(publishRequestPromise).rejects.toStrictEqual(DataTrackPublishError.cancelled());
111
+ });
112
+
113
+ it.each([
114
+ // Single packet payload case
115
+ [
116
+ new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]),
117
+ [
118
+ {
119
+ header: {
120
+ extensions: {
121
+ e2ee: null,
122
+ userTimestamp: null,
123
+ },
124
+ frameNumber: 0,
125
+ marker: FrameMarker.Single,
126
+ sequence: 0,
127
+ timestamp: expect.anything(),
128
+ trackHandle: 5,
129
+ },
130
+ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]),
131
+ },
132
+ ],
133
+ ],
134
+
135
+ // Multi packet payload case
136
+ [
137
+ new Uint8Array(24_000).fill(0xbe),
138
+ [
139
+ {
140
+ header: {
141
+ extensions: {
142
+ e2ee: null,
143
+ userTimestamp: null,
144
+ },
145
+ frameNumber: 0,
146
+ marker: FrameMarker.Start,
147
+ sequence: 0,
148
+ timestamp: expect.anything(),
149
+ trackHandle: 5,
150
+ },
151
+ payload: new Uint8Array(15988 /* 16k mtu - 12 header bytes */).fill(0xbe),
152
+ },
153
+ {
154
+ header: {
155
+ extensions: {
156
+ e2ee: null,
157
+ userTimestamp: null,
158
+ },
159
+ frameNumber: 0,
160
+ marker: FrameMarker.Final,
161
+ sequence: 1,
162
+ timestamp: expect.anything(),
163
+ trackHandle: 5,
164
+ },
165
+ payload: new Uint8Array(8012 /* 24k payload - (16k mtu - 12 header bytes) */).fill(0xbe),
166
+ },
167
+ ],
168
+ ],
169
+ ])(
170
+ 'should test track payload sending',
171
+ async (inputBytes: Uint8Array, outputPacketsJson: Array<unknown>) => {
172
+ // Create a manager prefilled with a descriptor
173
+ const manager = OutgoingDataTrackManager.withDescriptors(
174
+ new Map([
175
+ [
176
+ DataTrackHandle.fromNumber(5),
177
+ Descriptor.active(
178
+ {
179
+ sid: 'bogus-sid',
180
+ pubHandle: 5,
181
+ name: 'test',
182
+ usesE2ee: false,
183
+ },
184
+ null,
185
+ ),
186
+ ],
187
+ ]),
188
+ );
189
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
190
+ 'packetsAvailable',
191
+ ]);
192
+
193
+ const localDataTrack = manager.createLocalDataTrack(5)!;
194
+ expect(localDataTrack).not.toStrictEqual(null);
195
+
196
+ // Kick off sending the bytes...
197
+ localDataTrack.tryPush(inputBytes);
198
+
199
+ // ... and make sure the corresponding events are emitted to tell the SFU to send the packets
200
+ for (const outputPacketJson of outputPacketsJson) {
201
+ const packetBytes = await managerEvents.waitFor('packetsAvailable');
202
+ const [packet] = DataTrackPacket.fromBinary(packetBytes.bytes);
203
+
204
+ expect(packet.toJSON()).toStrictEqual(outputPacketJson);
205
+ }
206
+ },
207
+ );
208
+
209
+ it('should send e2ee encrypted datatrack payload', async () => {
210
+ const manager = new OutgoingDataTrackManager({
211
+ encryptionProvider: PrefixingEncryptionProvider,
212
+ });
213
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
214
+ 'sfuPublishRequest',
215
+ 'packetsAvailable',
216
+ ]);
217
+
218
+ // 1. Publish a data track
219
+ const publishRequestPromise = manager.publishRequest({ name: 'test' });
220
+
221
+ // 2. This publish request should be sent along to the SFU
222
+ const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest');
223
+ expect(sfuPublishEvent.name).toStrictEqual('test');
224
+ expect(sfuPublishEvent.usesE2ee).toStrictEqual(true); // NOTE: this is true, e2ee is enabled!
225
+ const handle = sfuPublishEvent.handle;
226
+
227
+ // 3. Respond to the SFU publish request with an OK response
228
+ manager.receivedSfuPublishResponse(handle, {
229
+ type: 'ok',
230
+ data: {
231
+ sid: 'bogus-sid',
232
+ pubHandle: sfuPublishEvent.handle,
233
+ name: 'test',
234
+ usesE2ee: true, // NOTE: this is true, e2ee is enabled!
235
+ },
236
+ });
237
+
238
+ // Get the connected local data track
239
+ const localDataTrack = await publishRequestPromise;
240
+ expect(localDataTrack.isPublished()).toStrictEqual(true);
241
+
242
+ // Kick off sending the payload bytes
243
+ localDataTrack.tryPush(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
244
+
245
+ // Make sure the packet that was sent was encrypted with the PrefixingEncryptionProvider
246
+ const packetBytes = await managerEvents.waitFor('packetsAvailable');
247
+ const [packet] = DataTrackPacket.fromBinary(packetBytes.bytes);
248
+
249
+ expect(packet.toJSON()).toStrictEqual({
250
+ header: {
251
+ extensions: {
252
+ e2ee: {
253
+ iv: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
254
+ keyIndex: 0,
255
+ lengthBytes: 13,
256
+ tag: 1,
257
+ },
258
+ userTimestamp: null,
259
+ },
260
+ frameNumber: 0,
261
+ marker: 3,
262
+ sequence: 0,
263
+ timestamp: expect.anything(),
264
+ trackHandle: 1,
265
+ },
266
+ payload: new Uint8Array([
267
+ // Encryption added prefix
268
+ 0xde, 0xad, 0xbe, 0xef,
269
+ // Actual payload
270
+ 0x01, 0x02, 0x03, 0x04, 0x05,
271
+ ]),
272
+ });
273
+ });
274
+
275
+ it('should test track unpublishing', async () => {
276
+ // Create a manager prefilled with a descriptor
277
+ const manager = OutgoingDataTrackManager.withDescriptors(
278
+ new Map([
279
+ [
280
+ DataTrackHandle.fromNumber(5),
281
+ Descriptor.active(
282
+ {
283
+ sid: 'bogus-sid',
284
+ pubHandle: 5,
285
+ name: 'test',
286
+ usesE2ee: false,
287
+ },
288
+ null,
289
+ ),
290
+ ],
291
+ ]),
292
+ );
293
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
294
+ 'sfuUnpublishRequest',
295
+ ]);
296
+
297
+ // Make sure the descriptor is in there
298
+ expect(manager.getDescriptor(5)?.type).toStrictEqual('active');
299
+
300
+ // Unpublish data track
301
+ const unpublishRequestPromise = manager.unpublishRequest(DataTrackHandle.fromNumber(5));
302
+
303
+ const sfuUnpublishEvent = await managerEvents.waitFor('sfuUnpublishRequest');
304
+ expect(sfuUnpublishEvent.handle).toStrictEqual(5);
305
+
306
+ manager.receivedSfuUnpublishResponse(DataTrackHandle.fromNumber(5));
307
+
308
+ await unpublishRequestPromise;
309
+
310
+ // Make sure data track is no longer
311
+ expect(manager.getDescriptor(5)).toStrictEqual(null);
312
+ });
313
+
314
+ it('should query currently active descriptors', async () => {
315
+ // Create a manager prefilled with a descriptor
316
+ const manager = OutgoingDataTrackManager.withDescriptors(
317
+ new Map([
318
+ [
319
+ DataTrackHandle.fromNumber(2),
320
+ Descriptor.active(
321
+ {
322
+ sid: 'bogus-sid-2',
323
+ pubHandle: 2,
324
+ name: 'twotwotwo',
325
+ usesE2ee: false,
326
+ },
327
+ null,
328
+ ),
329
+ ],
330
+ [
331
+ DataTrackHandle.fromNumber(6),
332
+ Descriptor.active(
333
+ {
334
+ sid: 'bogus-sid-6',
335
+ pubHandle: 6,
336
+ name: 'sixsixsix',
337
+ usesE2ee: false,
338
+ },
339
+ null,
340
+ ),
341
+ ],
342
+ ]),
343
+ );
344
+
345
+ const result = await manager.queryPublished();
346
+
347
+ expect(result).toStrictEqual([
348
+ { sid: 'bogus-sid-2', pubHandle: 2, name: 'twotwotwo', usesE2ee: false },
349
+ { sid: 'bogus-sid-6', pubHandle: 6, name: 'sixsixsix', usesE2ee: false },
350
+ ]);
351
+ });
352
+
353
+ it('should shutdown cleanly', async () => {
354
+ // Create a manager prefilled with a descriptor
355
+ const pendingDescriptor = Descriptor.pending();
356
+ const manager = OutgoingDataTrackManager.withDescriptors(
357
+ new Map<DataTrackHandle, Descriptor>([
358
+ [DataTrackHandle.fromNumber(2), pendingDescriptor],
359
+ [
360
+ DataTrackHandle.fromNumber(6),
361
+ Descriptor.active(
362
+ {
363
+ sid: 'bogus-sid-6',
364
+ pubHandle: 6,
365
+ name: 'sixsixsix',
366
+ usesE2ee: false,
367
+ },
368
+ null,
369
+ ),
370
+ ],
371
+ ]),
372
+ );
373
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
374
+ 'sfuUnpublishRequest',
375
+ ]);
376
+
377
+ // Shut down the manager
378
+ const shutdownPromise = manager.shutdown();
379
+
380
+ // The pending data track should be cancelled
381
+ expect(pendingDescriptor.completionFuture.promise).rejects.toThrowError('Room disconnected');
382
+
383
+ // And the active data track should be requested to be unpublished
384
+ const unpublishEvent = await managerEvents.waitFor('sfuUnpublishRequest');
385
+ expect(unpublishEvent.handle).toStrictEqual(6);
386
+
387
+ // Acknowledge that the unpublish has occurred
388
+ manager.receivedSfuUnpublishResponse(DataTrackHandle.fromNumber(6));
389
+
390
+ await shutdownPromise;
391
+ });
392
+ });
@@ -0,0 +1,302 @@
1
+ import { EventEmitter } from 'events';
2
+ import type TypedEmitter from 'typed-emitter';
3
+ import { LoggerNames, getLogger } from '../../../logger';
4
+ import type { Throws } from '../../../utils/throws';
5
+ import { Future } from '../../utils';
6
+ import { type EncryptionProvider } from '../e2ee';
7
+ import type { DataTrackFrame } from '../frame';
8
+ import { DataTrackHandle, DataTrackHandleAllocator } from '../handle';
9
+ import { DataTrackExtensions } from '../packet/extensions';
10
+ import { type DataTrackInfo, LocalDataTrack } from '../track';
11
+ import {
12
+ DataTrackPublishError,
13
+ DataTrackPublishErrorReason,
14
+ DataTrackPushFrameError,
15
+ DataTrackPushFrameErrorReason,
16
+ } from './errors';
17
+ import DataTrackOutgoingPipeline from './pipeline';
18
+ import {
19
+ type DataTrackOptions,
20
+ type OutputEventPacketsAvailable,
21
+ type OutputEventSfuPublishRequest,
22
+ type OutputEventSfuUnpublishRequest,
23
+ type SfuPublishResponseResult,
24
+ } from './types';
25
+
26
+ const log = getLogger(LoggerNames.DataTracks);
27
+
28
+ export type PendingDescriptor = {
29
+ type: 'pending';
30
+ completionFuture: Future<
31
+ LocalDataTrack,
32
+ | DataTrackPublishError<DataTrackPublishErrorReason.NotAllowed>
33
+ | DataTrackPublishError<DataTrackPublishErrorReason.DuplicateName>
34
+ | DataTrackPublishError<DataTrackPublishErrorReason.Timeout>
35
+ | DataTrackPublishError<DataTrackPublishErrorReason.LimitReached>
36
+ | DataTrackPublishError<DataTrackPublishErrorReason.Disconnected>
37
+ | DataTrackPublishError<DataTrackPublishErrorReason.Cancelled>
38
+ >;
39
+ };
40
+ export type ActiveDescriptor = {
41
+ type: 'active';
42
+ info: DataTrackInfo;
43
+
44
+ pipeline: DataTrackOutgoingPipeline;
45
+
46
+ /** Resolves when the descriptor is unpublished. */
47
+ unpublishingFuture: Future<void, never>;
48
+ };
49
+ export type Descriptor = PendingDescriptor | ActiveDescriptor;
50
+
51
+ export const Descriptor = {
52
+ pending(): PendingDescriptor {
53
+ return {
54
+ type: 'pending',
55
+ completionFuture: new Future(),
56
+ };
57
+ },
58
+ active(info: DataTrackInfo, encryptionProvider: EncryptionProvider | null): ActiveDescriptor {
59
+ return {
60
+ type: 'active',
61
+ info,
62
+ pipeline: new DataTrackOutgoingPipeline({ info, encryptionProvider }),
63
+ unpublishingFuture: new Future(),
64
+ };
65
+ },
66
+ };
67
+
68
+ export type DataTrackOutgoingManagerCallbacks = {
69
+ /** Request sent to the SFU to publish a track. */
70
+ sfuPublishRequest: (event: OutputEventSfuPublishRequest) => void;
71
+ /** Request sent to the SFU to unpublish a track. */
72
+ sfuUnpublishRequest: (event: OutputEventSfuUnpublishRequest) => void;
73
+ /** Serialized packets are ready to be sent over the transport. */
74
+ packetsAvailable: (event: OutputEventPacketsAvailable) => void;
75
+ };
76
+
77
+ type DataTrackLocalManagerOptions = {
78
+ /**
79
+ * Provider to use for encrypting outgoing frame payloads.
80
+ *
81
+ * If none, end-to-end encryption will be disabled for all published tracks.
82
+ */
83
+ encryptionProvider?: EncryptionProvider;
84
+ };
85
+
86
+ /** How long to wait when attempting to publish before timing out. */
87
+ const PUBLISH_TIMEOUT_MILLISECONDS = 10_000;
88
+
89
+ export default class OutgoingDataTrackManager extends (EventEmitter as new () => TypedEmitter<DataTrackOutgoingManagerCallbacks>) {
90
+ private encryptionProvider: EncryptionProvider | null;
91
+
92
+ private handleAllocator = new DataTrackHandleAllocator();
93
+
94
+ private descriptors = new Map<DataTrackHandle, Descriptor>();
95
+
96
+ constructor(options?: DataTrackLocalManagerOptions) {
97
+ super();
98
+ this.encryptionProvider = options?.encryptionProvider ?? null;
99
+ }
100
+
101
+ static withDescriptors(descriptors: Map<DataTrackHandle, Descriptor>) {
102
+ const manager = new OutgoingDataTrackManager();
103
+ manager.descriptors = descriptors;
104
+ return manager;
105
+ }
106
+
107
+ /**
108
+ * Used by attached {@link LocalDataTrack} instances to query their associated descriptor info.
109
+ * @internal
110
+ */
111
+ getDescriptor(handle: DataTrackHandle) {
112
+ return this.descriptors.get(handle) ?? null;
113
+ }
114
+
115
+ createLocalDataTrack(handle: DataTrackHandle) {
116
+ const descriptor = this.getDescriptor(handle);
117
+ if (descriptor?.type !== 'active') {
118
+ return null;
119
+ }
120
+ return new LocalDataTrack(descriptor.info, this);
121
+ }
122
+
123
+ /** Used by attached {@link LocalDataTrack} instances to broadcast data track packets to other
124
+ * subscribers.
125
+ * @internal
126
+ */
127
+ tryProcessAndSend(
128
+ handle: DataTrackHandle,
129
+ payload: Uint8Array,
130
+ ): Throws<
131
+ void,
132
+ | DataTrackPushFrameError<DataTrackPushFrameErrorReason.Dropped>
133
+ | DataTrackPushFrameError<DataTrackPushFrameErrorReason.TrackUnpublished>
134
+ > {
135
+ const descriptor = this.getDescriptor(handle);
136
+ if (descriptor?.type !== 'active') {
137
+ throw DataTrackPushFrameError.trackUnpublished();
138
+ }
139
+
140
+ const frame: DataTrackFrame = {
141
+ payload,
142
+ extensions: new DataTrackExtensions(),
143
+ };
144
+
145
+ try {
146
+ for (const packet of descriptor.pipeline.processFrame(frame)) {
147
+ this.emit('packetsAvailable', { bytes: packet.toBinary() });
148
+ }
149
+ } catch (err) {
150
+ // NOTE: In the rust implementation this "dropped" error means something different (not enough room
151
+ // in the track mpsc channel)
152
+ throw DataTrackPushFrameError.dropped(err);
153
+ }
154
+ }
155
+
156
+ /** Client requested to publish a track. */
157
+ async publishRequest(options: DataTrackOptions, signal?: AbortSignal) {
158
+ const handle = this.handleAllocator.get();
159
+ if (!handle) {
160
+ throw DataTrackPublishError.limitReached();
161
+ }
162
+
163
+ const timeoutSignal = AbortSignal.timeout(PUBLISH_TIMEOUT_MILLISECONDS);
164
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
165
+
166
+ if (this.descriptors.has(handle)) {
167
+ // @throws-transformer ignore - this should be treated as a "panic" and not be caught
168
+ throw new Error('Descriptor for handle already exists');
169
+ }
170
+
171
+ const descriptor = Descriptor.pending();
172
+ this.descriptors.set(handle, descriptor);
173
+
174
+ const onAbort = () => {
175
+ const existingDescriptor = this.descriptors.get(handle);
176
+ if (!existingDescriptor) {
177
+ log.warn(`No descriptor for ${handle}`);
178
+ return;
179
+ }
180
+ this.descriptors.delete(handle);
181
+
182
+ // Let the SFU know that the publish has been cancelled
183
+ this.emit('sfuUnpublishRequest', { handle });
184
+
185
+ if (existingDescriptor.type === 'pending') {
186
+ existingDescriptor.completionFuture.reject?.(
187
+ timeoutSignal.aborted
188
+ ? DataTrackPublishError.timeout()
189
+ : // NOTE: the below cancelled case was introduced by web / there isn't a corresponding case in the rust version.
190
+ DataTrackPublishError.cancelled(),
191
+ );
192
+ }
193
+ };
194
+ combinedSignal.addEventListener('abort', onAbort);
195
+
196
+ this.emit('sfuPublishRequest', {
197
+ handle,
198
+ name: options.name,
199
+ usesE2ee: this.encryptionProvider !== null,
200
+ });
201
+
202
+ const localDataTrack = await descriptor.completionFuture.promise;
203
+ combinedSignal.removeEventListener('abort', onAbort);
204
+ return localDataTrack;
205
+ }
206
+
207
+ /** Get information about all currently published tracks. */
208
+ async queryPublished() {
209
+ const descriptorInfos = Array.from(this.descriptors.values())
210
+ .filter((descriptor): descriptor is ActiveDescriptor => descriptor.type === 'active')
211
+ .map((descriptor) => descriptor.info);
212
+
213
+ return descriptorInfos;
214
+ }
215
+
216
+ /** Client request to unpublish a track. */
217
+ async unpublishRequest(handle: DataTrackHandle) {
218
+ const descriptor = this.descriptors.get(handle);
219
+ if (!descriptor) {
220
+ log.warn(`No descriptor for ${handle}`);
221
+ return;
222
+ }
223
+ if (descriptor.type !== 'active') {
224
+ log.warn(`Track ${handle} not active`);
225
+ return;
226
+ }
227
+
228
+ this.emit('sfuUnpublishRequest', { handle });
229
+
230
+ await descriptor.unpublishingFuture.promise;
231
+ }
232
+
233
+ /** SFU responded to a request to publish a data track. */
234
+ receivedSfuPublishResponse(handle: DataTrackHandle, result: SfuPublishResponseResult) {
235
+ const descriptor = this.descriptors.get(handle);
236
+ if (!descriptor) {
237
+ log.warn(`No descriptor for ${handle}`);
238
+ return;
239
+ }
240
+ this.descriptors.delete(handle);
241
+
242
+ if (descriptor.type !== 'pending') {
243
+ log.warn(`Track ${handle} already active`);
244
+ return;
245
+ }
246
+
247
+ if (result.type === 'ok') {
248
+ const info = result.data;
249
+
250
+ const encryptionProvider = info.usesE2ee ? this.encryptionProvider : null;
251
+ this.descriptors.set(info.pubHandle, Descriptor.active(info, encryptionProvider));
252
+
253
+ const localDataTrack = this.createLocalDataTrack(info.pubHandle);
254
+ if (!localDataTrack) {
255
+ // @throws-transformer ignore - this should be treated as a "panic" and not be caught
256
+ throw new Error(
257
+ 'DataTrackOutgoingManager.handleSfuPublishResponse: localDataTrack was not created after active descriptor stored.',
258
+ );
259
+ }
260
+
261
+ descriptor.completionFuture.resolve?.(localDataTrack);
262
+ } else {
263
+ descriptor.completionFuture.reject?.(result.error);
264
+ }
265
+ }
266
+
267
+ /** SFU notification that a track has been unpublished. */
268
+ receivedSfuUnpublishResponse(handle: DataTrackHandle) {
269
+ const descriptor = this.descriptors.get(handle);
270
+ if (!descriptor) {
271
+ log.warn(`No descriptor for ${handle}`);
272
+ return;
273
+ }
274
+ this.descriptors.delete(handle);
275
+
276
+ if (descriptor.type !== 'active') {
277
+ log.warn(`Track ${handle} not active`);
278
+ return;
279
+ }
280
+
281
+ descriptor.unpublishingFuture.resolve?.();
282
+ }
283
+
284
+ /** Shuts down the manager and all associated tracks. */
285
+ async shutdown() {
286
+ for (const descriptor of this.descriptors.values()) {
287
+ switch (descriptor.type) {
288
+ case 'pending':
289
+ descriptor.completionFuture.reject?.(DataTrackPublishError.disconnected());
290
+ break;
291
+ case 'active':
292
+ // Abandon any unpublishing descriptors that were in flight and assume they will get
293
+ // cleaned up automatically with the connection shutdown.
294
+ descriptor.unpublishingFuture.resolve?.();
295
+
296
+ await this.unpublishRequest(descriptor.info.pubHandle);
297
+ break;
298
+ }
299
+ }
300
+ this.descriptors.clear();
301
+ }
302
+ }