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.
- package/README.md +7 -5
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +21 -14
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2087 -1920
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +2 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/KeyProvider.d.ts +2 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/e2ee/events.d.ts +1 -1
- package/dist/src/e2ee/events.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +1 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -4
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +2 -4
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts +51 -0
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -0
- package/dist/src/room/data-track/e2ee.d.ts +12 -0
- package/dist/src/room/data-track/e2ee.d.ts.map +1 -0
- package/dist/src/room/data-track/frame.d.ts +7 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -0
- package/dist/src/room/data-track/handle.d.ts +6 -7
- package/dist/src/room/data-track/handle.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +76 -0
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/errors.d.ts +64 -0
- package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/pipeline.d.ts +22 -0
- package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts +31 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -0
- package/dist/src/room/data-track/packet/index.d.ts +3 -3
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packetizer.d.ts +43 -0
- package/dist/src/room/data-track/packetizer.d.ts.map +1 -0
- package/dist/src/room/data-track/track.d.ts +30 -0
- package/dist/src/room/data-track/track.d.ts.map +1 -0
- package/dist/src/room/data-track/utils.d.ts +34 -2
- package/dist/src/room/data-track/utils.d.ts.map +1 -1
- package/dist/src/room/debounce.d.ts +11 -0
- package/dist/src/room/debounce.d.ts.map +1 -0
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +2 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +0 -2
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +6 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/utils/subscribeToEvents.d.ts +12 -0
- package/dist/src/utils/subscribeToEvents.d.ts.map +1 -0
- package/dist/src/utils/throws.d.ts +4 -2
- package/dist/src/utils/throws.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +2 -0
- package/dist/ts4.2/e2ee/KeyProvider.d.ts +2 -0
- package/dist/ts4.2/e2ee/events.d.ts +1 -1
- package/dist/ts4.2/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +2 -2
- package/dist/ts4.2/index.d.ts +7 -3
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/room/PCTransport.d.ts +1 -6
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +2 -4
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +51 -0
- package/dist/ts4.2/room/data-track/e2ee.d.ts +12 -0
- package/dist/ts4.2/room/data-track/frame.d.ts +7 -0
- package/dist/ts4.2/room/data-track/handle.d.ts +6 -7
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +77 -0
- package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +64 -0
- package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +22 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +31 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +3 -3
- package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -0
- package/dist/ts4.2/room/data-track/track.d.ts +30 -0
- package/dist/ts4.2/room/data-track/utils.d.ts +34 -2
- package/dist/ts4.2/room/debounce.d.ts +11 -0
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +2 -1
- package/dist/ts4.2/room/types.d.ts +0 -2
- package/dist/ts4.2/room/utils.d.ts +6 -1
- package/dist/ts4.2/utils/subscribeToEvents.d.ts +12 -0
- package/dist/ts4.2/utils/throws.d.ts +4 -2
- package/package.json +4 -5
- package/src/e2ee/E2eeManager.ts +9 -5
- package/src/e2ee/KeyProvider.ts +10 -1
- package/src/e2ee/events.ts +1 -1
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +7 -4
- package/src/e2ee/worker/e2ee.worker.ts +20 -10
- package/src/index.ts +15 -5
- package/src/logger.ts +1 -0
- package/src/room/PCTransport.ts +2 -1
- package/src/room/PCTransportManager.ts +27 -9
- package/src/room/RTCEngine.ts +13 -2
- package/src/room/Room.ts +11 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -25
- package/src/room/data-stream/incoming/StreamReader.ts +56 -73
- package/src/room/data-track/depacketizer.test.ts +442 -0
- package/src/room/data-track/depacketizer.ts +298 -0
- package/src/room/data-track/e2ee.ts +14 -0
- package/src/room/data-track/frame.ts +8 -0
- package/src/room/data-track/handle.test.ts +1 -1
- package/src/room/data-track/handle.ts +9 -14
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +392 -0
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +302 -0
- package/src/room/data-track/outgoing/errors.ts +157 -0
- package/src/room/data-track/outgoing/pipeline.ts +76 -0
- package/src/room/data-track/outgoing/types.ts +37 -0
- package/src/room/data-track/packet/index.test.ts +9 -9
- package/src/room/data-track/packet/index.ts +11 -9
- package/src/room/data-track/packet/serializable.ts +1 -1
- package/src/room/data-track/packetizer.test.ts +131 -0
- package/src/room/data-track/packetizer.ts +132 -0
- package/src/room/data-track/track.ts +50 -0
- package/src/room/data-track/utils.test.ts +27 -1
- package/src/room/data-track/utils.ts +125 -5
- package/src/room/debounce.ts +115 -0
- package/src/room/events.ts +1 -1
- package/src/room/participant/LocalParticipant.ts +2 -0
- package/src/room/track/LocalAudioTrack.ts +10 -10
- package/src/room/track/LocalTrack.ts +14 -5
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +1 -1
- package/src/room/types.ts +0 -2
- package/src/room/utils.ts +7 -2
- package/src/utils/subscribeToEvents.ts +63 -0
- 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
|
+
}
|