livekit-client 2.5.6 → 2.5.8
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 +2 -2
- 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 +628 -603
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2160 -1949
- 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/worker/FrameCryptor.d.ts +2 -2
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +7 -3
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +7 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/mocks.d.ts +1 -1
- package/dist/src/test/mocks.d.ts.map +1 -1
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -2
- package/dist/ts4.2/src/index.d.ts +2 -2
- package/dist/ts4.2/src/room/Room.d.ts +4 -1
- package/dist/ts4.2/src/room/events.d.ts +7 -3
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -0
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -1
- package/dist/ts4.2/src/room/utils.d.ts +1 -1
- package/dist/ts4.2/src/test/mocks.d.ts +1 -1
- package/package.json +14 -15
- package/src/e2ee/worker/FrameCryptor.test.ts +249 -2
- package/src/e2ee/worker/FrameCryptor.ts +3 -3
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +122 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
- package/src/e2ee/worker/e2ee.worker.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/Room.ts +24 -10
- package/src/room/events.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +22 -0
- package/src/room/participant/Participant.ts +3 -2
- package/src/room/track/LocalTrackPublication.ts +1 -1
- package/src/room/track/Track.ts +1 -1
- package/src/room/track/utils.ts +1 -1
- package/src/room/utils.ts +1 -1
- package/src/test/mocks.ts +1 -1
@@ -1,13 +1,109 @@
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
2
|
-
import {
|
1
|
+
import { afterEach, describe, expect, it, vitest } from 'vitest';
|
2
|
+
import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import { CryptorEvent } from '../events';
|
4
|
+
import type { KeyProviderOptions } from '../types';
|
5
|
+
import { createKeyMaterialFromString } from '../utils';
|
6
|
+
import { FrameCryptor, encryptionEnabledMap, isFrameServerInjected } from './FrameCryptor';
|
7
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
8
|
+
|
9
|
+
function mockEncryptedRTCEncodedVideoFrame(keyIndex: number): RTCEncodedVideoFrame {
|
10
|
+
const trailer = mockFrameTrailer(keyIndex);
|
11
|
+
const data = new Uint8Array(trailer.length + 10);
|
12
|
+
data.set(trailer, 10);
|
13
|
+
return mockRTCEncodedVideoFrame(data);
|
14
|
+
}
|
15
|
+
|
16
|
+
function mockRTCEncodedVideoFrame(data: Uint8Array): RTCEncodedVideoFrame {
|
17
|
+
return {
|
18
|
+
data: data.buffer,
|
19
|
+
timestamp: vitest.getMockedSystemTime()?.getTime() ?? 0,
|
20
|
+
type: 'key',
|
21
|
+
getMetadata(): RTCEncodedVideoFrameMetadata {
|
22
|
+
return {};
|
23
|
+
},
|
24
|
+
};
|
25
|
+
}
|
26
|
+
|
27
|
+
function mockFrameTrailer(keyIndex: number): Uint8Array {
|
28
|
+
const frameTrailer = new Uint8Array(2);
|
29
|
+
|
30
|
+
frameTrailer[0] = IV_LENGTH;
|
31
|
+
frameTrailer[1] = keyIndex;
|
32
|
+
|
33
|
+
return frameTrailer;
|
34
|
+
}
|
35
|
+
|
36
|
+
class TestUnderlyingSource<T> implements UnderlyingSource<T> {
|
37
|
+
controller: ReadableStreamController<T>;
|
38
|
+
|
39
|
+
start(controller: ReadableStreamController<T>): void {
|
40
|
+
this.controller = controller;
|
41
|
+
}
|
42
|
+
|
43
|
+
write(chunk: T): void {
|
44
|
+
this.controller.enqueue(chunk as any);
|
45
|
+
}
|
46
|
+
|
47
|
+
close(): void {
|
48
|
+
this.controller.close();
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
class TestUnderlyingSink<T> implements UnderlyingSink<T> {
|
53
|
+
public chunks: T[] = [];
|
54
|
+
|
55
|
+
write(chunk: T): void {
|
56
|
+
this.chunks.push(chunk);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
function prepareParticipantTestDecoder(
|
61
|
+
participantIdentity: string,
|
62
|
+
partialKeyProviderOptions: Partial<KeyProviderOptions>,
|
63
|
+
): {
|
64
|
+
keys: ParticipantKeyHandler;
|
65
|
+
cryptor: FrameCryptor;
|
66
|
+
input: TestUnderlyingSource<RTCEncodedVideoFrame>;
|
67
|
+
output: TestUnderlyingSink<RTCEncodedVideoFrame>;
|
68
|
+
} {
|
69
|
+
const keyProviderOptions = { ...KEY_PROVIDER_DEFAULTS, ...partialKeyProviderOptions };
|
70
|
+
const keys = new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
|
71
|
+
|
72
|
+
encryptionEnabledMap.set(participantIdentity, true);
|
73
|
+
|
74
|
+
const cryptor = new FrameCryptor({
|
75
|
+
participantIdentity,
|
76
|
+
keys,
|
77
|
+
keyProviderOptions,
|
78
|
+
sifTrailer: new Uint8Array(),
|
79
|
+
});
|
80
|
+
|
81
|
+
const input = new TestUnderlyingSource<RTCEncodedVideoFrame>();
|
82
|
+
const output = new TestUnderlyingSink<RTCEncodedVideoFrame>();
|
83
|
+
cryptor.setupTransform(
|
84
|
+
'decode',
|
85
|
+
new ReadableStream(input),
|
86
|
+
new WritableStream(output),
|
87
|
+
'testTrack',
|
88
|
+
);
|
89
|
+
|
90
|
+
return { keys, cryptor, input, output };
|
91
|
+
}
|
3
92
|
|
4
93
|
describe('FrameCryptor', () => {
|
94
|
+
const participantIdentity = 'testParticipant';
|
95
|
+
|
96
|
+
afterEach(() => {
|
97
|
+
encryptionEnabledMap.clear();
|
98
|
+
});
|
99
|
+
|
5
100
|
it('identifies server injected frame correctly', () => {
|
6
101
|
const frameTrailer = new TextEncoder().encode('LKROCKS');
|
7
102
|
const frameData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, ...frameTrailer]).buffer;
|
8
103
|
|
9
104
|
expect(isFrameServerInjected(frameData, frameTrailer)).toBe(true);
|
10
105
|
});
|
106
|
+
|
11
107
|
it('identifies server non server injected frame correctly', () => {
|
12
108
|
const frameTrailer = new TextEncoder().encode('LKROCKS');
|
13
109
|
const frameData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, ...frameTrailer, 10]);
|
@@ -16,4 +112,155 @@ describe('FrameCryptor', () => {
|
|
16
112
|
frameData.fill(0);
|
17
113
|
expect(isFrameServerInjected(frameData.buffer, frameTrailer)).toBe(false);
|
18
114
|
});
|
115
|
+
|
116
|
+
it('passthrough if participant encryption disabled', async () => {
|
117
|
+
vitest.useFakeTimers();
|
118
|
+
try {
|
119
|
+
const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
|
120
|
+
|
121
|
+
// disable encryption for participant
|
122
|
+
encryptionEnabledMap.set(participantIdentity, false);
|
123
|
+
|
124
|
+
const frame = mockEncryptedRTCEncodedVideoFrame(1);
|
125
|
+
|
126
|
+
input.write(frame);
|
127
|
+
await vitest.advanceTimersToNextTimerAsync();
|
128
|
+
|
129
|
+
expect(output.chunks).toEqual([frame]);
|
130
|
+
} finally {
|
131
|
+
vitest.useRealTimers();
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
it('passthrough for empty frame', async () => {
|
136
|
+
vitest.useFakeTimers();
|
137
|
+
try {
|
138
|
+
const { input, output } = prepareParticipantTestDecoder(participantIdentity, {});
|
139
|
+
|
140
|
+
// empty frame
|
141
|
+
const frame = mockRTCEncodedVideoFrame(new Uint8Array(0));
|
142
|
+
|
143
|
+
input.write(frame);
|
144
|
+
await vitest.advanceTimersToNextTimerAsync();
|
145
|
+
|
146
|
+
expect(output.chunks).toEqual([frame]);
|
147
|
+
} finally {
|
148
|
+
vitest.useRealTimers();
|
149
|
+
}
|
150
|
+
});
|
151
|
+
|
152
|
+
it('drops frames when invalid key', async () => {
|
153
|
+
vitest.useFakeTimers();
|
154
|
+
try {
|
155
|
+
const { keys, input, output } = prepareParticipantTestDecoder(participantIdentity, {
|
156
|
+
failureTolerance: 0,
|
157
|
+
});
|
158
|
+
|
159
|
+
expect(keys.hasValidKey).toBe(true);
|
160
|
+
|
161
|
+
await keys.setKey(await createKeyMaterialFromString('password'), 0);
|
162
|
+
|
163
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
164
|
+
await vitest.advanceTimersToNextTimerAsync();
|
165
|
+
|
166
|
+
expect(output.chunks).toEqual([]);
|
167
|
+
expect(keys.hasValidKey).toBe(false);
|
168
|
+
|
169
|
+
// this should still fail as keys are all marked as invalid
|
170
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(0));
|
171
|
+
await vitest.advanceTimersToNextTimerAsync();
|
172
|
+
|
173
|
+
expect(output.chunks).toEqual([]);
|
174
|
+
expect(keys.hasValidKey).toBe(false);
|
175
|
+
} finally {
|
176
|
+
vitest.useRealTimers();
|
177
|
+
}
|
178
|
+
});
|
179
|
+
|
180
|
+
it('marks key invalid after too many failures', async () => {
|
181
|
+
const { keys, cryptor, input } = prepareParticipantTestDecoder(participantIdentity, {
|
182
|
+
failureTolerance: 1,
|
183
|
+
});
|
184
|
+
|
185
|
+
expect(keys.hasValidKey).toBe(true);
|
186
|
+
|
187
|
+
await keys.setKey(await createKeyMaterialFromString('password'), 0);
|
188
|
+
|
189
|
+
vitest.spyOn(keys, 'getKeySet');
|
190
|
+
vitest.spyOn(keys, 'decryptionFailure');
|
191
|
+
|
192
|
+
const errorListener = vitest.fn().mockImplementation((e) => {
|
193
|
+
console.log('error', e);
|
194
|
+
});
|
195
|
+
cryptor.on(CryptorEvent.Error, errorListener);
|
196
|
+
|
197
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
198
|
+
|
199
|
+
await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalled());
|
200
|
+
expect(errorListener).toHaveBeenCalled();
|
201
|
+
expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
|
202
|
+
expect(keys.getKeySet).toHaveBeenCalled();
|
203
|
+
expect(keys.getKeySet).toHaveBeenLastCalledWith(1);
|
204
|
+
expect(keys.hasValidKey).toBe(true);
|
205
|
+
|
206
|
+
vitest.clearAllMocks();
|
207
|
+
|
208
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
209
|
+
|
210
|
+
await vitest.waitFor(() => expect(keys.decryptionFailure).toHaveBeenCalled());
|
211
|
+
expect(errorListener).toHaveBeenCalled();
|
212
|
+
expect(keys.decryptionFailure).toHaveBeenCalledTimes(1);
|
213
|
+
expect(keys.getKeySet).toHaveBeenCalled();
|
214
|
+
expect(keys.getKeySet).toHaveBeenLastCalledWith(1);
|
215
|
+
expect(keys.hasValidKey).toBe(false);
|
216
|
+
|
217
|
+
vitest.clearAllMocks();
|
218
|
+
|
219
|
+
// this should still fail as keys are all marked as invalid
|
220
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(0));
|
221
|
+
|
222
|
+
await vitest.waitFor(() => expect(keys.getKeySet).toHaveBeenCalled());
|
223
|
+
// decryptionFailure() isn't called in this case
|
224
|
+
expect(keys.getKeySet).toHaveBeenCalled();
|
225
|
+
expect(keys.getKeySet).toHaveBeenLastCalledWith(0);
|
226
|
+
expect(keys.hasValidKey).toBe(false);
|
227
|
+
});
|
228
|
+
|
229
|
+
it('mark as valid when a new key is set on same index', async () => {
|
230
|
+
const { keys, input } = prepareParticipantTestDecoder(participantIdentity, {
|
231
|
+
failureTolerance: 0,
|
232
|
+
});
|
233
|
+
|
234
|
+
const material = await createKeyMaterialFromString('password');
|
235
|
+
await keys.setKey(material, 0);
|
236
|
+
|
237
|
+
expect(keys.hasValidKey).toBe(true);
|
238
|
+
|
239
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
240
|
+
|
241
|
+
expect(keys.hasValidKey).toBe(false);
|
242
|
+
|
243
|
+
await keys.setKey(material, 0);
|
244
|
+
|
245
|
+
expect(keys.hasValidKey).toBe(true);
|
246
|
+
});
|
247
|
+
|
248
|
+
it('mark as valid when a new key is set on new index', async () => {
|
249
|
+
const { keys, input } = prepareParticipantTestDecoder(participantIdentity, {
|
250
|
+
failureTolerance: 0,
|
251
|
+
});
|
252
|
+
|
253
|
+
const material = await createKeyMaterialFromString('password');
|
254
|
+
await keys.setKey(material, 0);
|
255
|
+
|
256
|
+
expect(keys.hasValidKey).toBe(true);
|
257
|
+
|
258
|
+
input.write(mockEncryptedRTCEncodedVideoFrame(1));
|
259
|
+
|
260
|
+
expect(keys.hasValidKey).toBe(false);
|
261
|
+
|
262
|
+
await keys.setKey(material, 1);
|
263
|
+
|
264
|
+
expect(keys.hasValidKey).toBe(true);
|
265
|
+
});
|
19
266
|
});
|
@@ -6,7 +6,7 @@ import { workerLogger } from '../../logger';
|
|
6
6
|
import type { VideoCodec } from '../../room/track/options';
|
7
7
|
import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
|
8
8
|
import { CryptorError, CryptorErrorReason } from '../errors';
|
9
|
-
import { CryptorCallbacks, CryptorEvent } from '../events';
|
9
|
+
import { type CryptorCallbacks, CryptorEvent } from '../events';
|
10
10
|
import type { DecodeRatchetOptions, KeyProviderOptions, KeySet } from '../types';
|
11
11
|
import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
|
12
12
|
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
@@ -156,8 +156,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
156
156
|
|
157
157
|
setupTransform(
|
158
158
|
operation: 'encode' | 'decode',
|
159
|
-
readable: ReadableStream
|
160
|
-
writable: WritableStream
|
159
|
+
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
160
|
+
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
161
161
|
trackId: string,
|
162
162
|
codec?: VideoCodec,
|
163
163
|
) {
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import { createKeyMaterialFromString } from '../utils';
|
4
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
5
|
+
|
6
|
+
describe('ParticipantKeyHandler', () => {
|
7
|
+
const participantIdentity = 'testParticipant';
|
8
|
+
|
9
|
+
it('keyringSize must be greater than 0', () => {
|
10
|
+
expect(() => {
|
11
|
+
new ParticipantKeyHandler(participantIdentity, { ...KEY_PROVIDER_DEFAULTS, keyringSize: 0 });
|
12
|
+
}).toThrowError(TypeError);
|
13
|
+
});
|
14
|
+
|
15
|
+
it('keyringSize must be max 256', () => {
|
16
|
+
expect(() => {
|
17
|
+
new ParticipantKeyHandler(participantIdentity, {
|
18
|
+
...KEY_PROVIDER_DEFAULTS,
|
19
|
+
keyringSize: 257,
|
20
|
+
});
|
21
|
+
}).toThrowError(TypeError);
|
22
|
+
});
|
23
|
+
|
24
|
+
it('get and sets keys at an index', async () => {
|
25
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
26
|
+
...KEY_PROVIDER_DEFAULTS,
|
27
|
+
keyringSize: 128,
|
28
|
+
});
|
29
|
+
const materialA = await createKeyMaterialFromString('passwordA');
|
30
|
+
const materialB = await createKeyMaterialFromString('passwordB');
|
31
|
+
await keyHandler.setKey(materialA, 0);
|
32
|
+
expect(keyHandler.getKeySet(0)).toBeDefined();
|
33
|
+
expect(keyHandler.getKeySet(0)?.material).toEqual(materialA);
|
34
|
+
await keyHandler.setKey(materialB, 0);
|
35
|
+
expect(keyHandler.getKeySet(0)?.material).toEqual(materialB);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('marks invalid if more than failureTolerance failures', async () => {
|
39
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
40
|
+
...KEY_PROVIDER_DEFAULTS,
|
41
|
+
failureTolerance: 2,
|
42
|
+
});
|
43
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
44
|
+
|
45
|
+
// 1
|
46
|
+
keyHandler.decryptionFailure();
|
47
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
48
|
+
|
49
|
+
// 2
|
50
|
+
keyHandler.decryptionFailure();
|
51
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
52
|
+
|
53
|
+
// 3
|
54
|
+
keyHandler.decryptionFailure();
|
55
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
56
|
+
});
|
57
|
+
|
58
|
+
it('marks valid on encryption success', async () => {
|
59
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
60
|
+
...KEY_PROVIDER_DEFAULTS,
|
61
|
+
failureTolerance: 0,
|
62
|
+
});
|
63
|
+
|
64
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
65
|
+
|
66
|
+
keyHandler.decryptionFailure();
|
67
|
+
|
68
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
69
|
+
|
70
|
+
keyHandler.decryptionSuccess();
|
71
|
+
|
72
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
73
|
+
});
|
74
|
+
|
75
|
+
it('marks valid on new key', async () => {
|
76
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
77
|
+
...KEY_PROVIDER_DEFAULTS,
|
78
|
+
failureTolerance: 0,
|
79
|
+
});
|
80
|
+
|
81
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
82
|
+
|
83
|
+
keyHandler.decryptionFailure();
|
84
|
+
|
85
|
+
expect(keyHandler.hasValidKey).toBe(false);
|
86
|
+
|
87
|
+
await keyHandler.setKey(await createKeyMaterialFromString('passwordA'));
|
88
|
+
|
89
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
90
|
+
});
|
91
|
+
|
92
|
+
it('updates currentKeyIndex on new key', async () => {
|
93
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, KEY_PROVIDER_DEFAULTS);
|
94
|
+
const material = await createKeyMaterialFromString('password');
|
95
|
+
|
96
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(0);
|
97
|
+
|
98
|
+
// default is zero
|
99
|
+
await keyHandler.setKey(material);
|
100
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(0);
|
101
|
+
|
102
|
+
// should go to next index
|
103
|
+
await keyHandler.setKey(material, 1);
|
104
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(1);
|
105
|
+
|
106
|
+
// should be able to jump ahead
|
107
|
+
await keyHandler.setKey(material, 10);
|
108
|
+
expect(keyHandler.getCurrentKeyIndex()).toBe(10);
|
109
|
+
});
|
110
|
+
|
111
|
+
it('allows many failures if failureTolerance is -1', async () => {
|
112
|
+
const keyHandler = new ParticipantKeyHandler(participantIdentity, {
|
113
|
+
...KEY_PROVIDER_DEFAULTS,
|
114
|
+
failureTolerance: -1,
|
115
|
+
});
|
116
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
117
|
+
for (let i = 0; i < 100; i++) {
|
118
|
+
keyHandler.decryptionFailure();
|
119
|
+
expect(keyHandler.hasValidKey).toBe(true);
|
120
|
+
}
|
121
|
+
});
|
122
|
+
});
|
@@ -38,7 +38,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
38
38
|
constructor(participantIdentity: string, keyProviderOptions: KeyProviderOptions) {
|
39
39
|
super();
|
40
40
|
this.currentKeyIndex = 0;
|
41
|
-
if (keyProviderOptions.keyringSize < 1 || keyProviderOptions.keyringSize >
|
41
|
+
if (keyProviderOptions.keyringSize < 1 || keyProviderOptions.keyringSize > 256) {
|
42
42
|
throw new TypeError('Keyring size needs to be between 1 and 256');
|
43
43
|
}
|
44
44
|
this.cryptoKeyRing = new Array(keyProviderOptions.keyringSize).fill(undefined);
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { workerLogger } from '../../logger';
|
2
|
-
import { VideoCodec } from '../../room/track/options';
|
2
|
+
import type { VideoCodec } from '../../room/track/options';
|
3
3
|
import { AsyncQueue } from '../../utils/AsyncQueue';
|
4
4
|
import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
5
5
|
import { CryptorErrorReason } from '../errors';
|
package/src/index.ts
CHANGED
@@ -27,6 +27,7 @@ import type { LiveKitReactNativeInfo } from './room/types';
|
|
27
27
|
import type { AudioAnalyserOptions } from './room/utils';
|
28
28
|
import {
|
29
29
|
Mutex,
|
30
|
+
compareVersions,
|
30
31
|
createAudioAnalyser,
|
31
32
|
getEmptyAudioStreamTrack,
|
32
33
|
getEmptyVideoStreamTrack,
|
@@ -81,6 +82,7 @@ export {
|
|
81
82
|
Room,
|
82
83
|
SubscriptionError,
|
83
84
|
TrackPublication,
|
85
|
+
compareVersions,
|
84
86
|
createAudioAnalyser,
|
85
87
|
getBrowser,
|
86
88
|
getEmptyAudioStreamTrack,
|
package/src/room/Room.ts
CHANGED
@@ -7,6 +7,7 @@ import {
|
|
7
7
|
JoinResponse,
|
8
8
|
LeaveRequest,
|
9
9
|
LeaveRequest_Action,
|
10
|
+
MetricsBatch,
|
10
11
|
ParticipantInfo,
|
11
12
|
ParticipantInfo_State,
|
12
13
|
ParticipantPermission,
|
@@ -135,6 +136,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
135
136
|
/** reflects the sender encryption status of the local participant */
|
136
137
|
isE2EEEnabled: boolean = false;
|
137
138
|
|
139
|
+
serverInfo?: Partial<ServerInfo>;
|
140
|
+
|
138
141
|
private roomInfo?: RoomModel;
|
139
142
|
|
140
143
|
private sidToIdentity: Map<string, string>;
|
@@ -609,6 +612,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
609
612
|
if (!serverInfo) {
|
610
613
|
serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
|
611
614
|
}
|
615
|
+
this.serverInfo = serverInfo;
|
612
616
|
|
613
617
|
this.log.debug(
|
614
618
|
`connected to Livekit Server ${Object.entries(serverInfo)
|
@@ -621,11 +625,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
621
625
|
},
|
622
626
|
);
|
623
627
|
|
624
|
-
if (!
|
628
|
+
if (!serverInfo.version) {
|
625
629
|
throw new UnsupportedServer('unknown server version');
|
626
630
|
}
|
627
631
|
|
628
|
-
if (
|
632
|
+
if (serverInfo.version === '0.15.1' && this.options.dynacast) {
|
629
633
|
this.log.debug('disabling dynacast due to server version', this.logContext);
|
630
634
|
// dynacast has a bug in 0.15.1, so we cannot use it then
|
631
635
|
roomOptions.dynacast = false;
|
@@ -1490,14 +1494,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1490
1494
|
if (!pub || !pub.track) {
|
1491
1495
|
return;
|
1492
1496
|
}
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1497
|
+
const newStreamState = Track.streamStateFromProto(streamState.state);
|
1498
|
+
if (newStreamState !== pub.track.streamState) {
|
1499
|
+
pub.track.streamState = newStreamState;
|
1500
|
+
participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
|
1501
|
+
this.emitWhenConnected(
|
1502
|
+
RoomEvent.TrackStreamStateChanged,
|
1503
|
+
pub,
|
1504
|
+
pub.track.streamState,
|
1505
|
+
participant,
|
1506
|
+
);
|
1507
|
+
}
|
1501
1508
|
});
|
1502
1509
|
};
|
1503
1510
|
|
@@ -1540,6 +1547,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1540
1547
|
this.handleSipDtmf(participant, packet.value.value);
|
1541
1548
|
} else if (packet.value.case === 'chatMessage') {
|
1542
1549
|
this.handleChatMessage(participant, packet.value.value);
|
1550
|
+
} else if (packet.value.case === 'metrics') {
|
1551
|
+
this.handleMetrics(packet.value.value, participant);
|
1543
1552
|
}
|
1544
1553
|
};
|
1545
1554
|
|
@@ -1589,6 +1598,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1589
1598
|
this.emit(RoomEvent.ChatMessage, msg, participant);
|
1590
1599
|
};
|
1591
1600
|
|
1601
|
+
private handleMetrics = (metrics: MetricsBatch, participant?: Participant) => {
|
1602
|
+
this.emit(RoomEvent.MetricsReceived, metrics, participant);
|
1603
|
+
};
|
1604
|
+
|
1592
1605
|
private handleAudioPlaybackStarted = () => {
|
1593
1606
|
if (this.canPlaybackAudio) {
|
1594
1607
|
return;
|
@@ -2289,4 +2302,5 @@ export type RoomEventCallbacks = {
|
|
2289
2302
|
activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
|
2290
2303
|
chatMessage: (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant) => void;
|
2291
2304
|
localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
|
2305
|
+
metricsReceived: (metrics: MetricsBatch, participant?: Participant) => void;
|
2292
2306
|
};
|
package/src/room/events.ts
CHANGED
@@ -294,7 +294,7 @@ export enum RoomEvent {
|
|
294
294
|
MediaDevicesError = 'mediaDevicesError',
|
295
295
|
|
296
296
|
/**
|
297
|
-
* A participant's permission has changed.
|
297
|
+
* A participant's permission has changed.
|
298
298
|
* args: (prevPermissions: [[ParticipantPermission]], participant: [[Participant]])
|
299
299
|
*/
|
300
300
|
ParticipantPermissionsChanged = 'participantPermissionsChanged',
|
@@ -330,6 +330,11 @@ export enum RoomEvent {
|
|
330
330
|
* fired when the first remote participant has subscribed to the localParticipant's track
|
331
331
|
*/
|
332
332
|
LocalTrackSubscribed = 'localTrackSubscribed',
|
333
|
+
|
334
|
+
/**
|
335
|
+
* fired when the client receives connection metrics from other participants
|
336
|
+
*/
|
337
|
+
MetricsReceived = 'metricsReceived',
|
333
338
|
}
|
334
339
|
|
335
340
|
export enum ParticipantEvent {
|
@@ -502,7 +507,7 @@ export enum ParticipantEvent {
|
|
502
507
|
AudioStreamAcquired = 'audioStreamAcquired',
|
503
508
|
|
504
509
|
/**
|
505
|
-
* A participant's permission has changed.
|
510
|
+
* A participant's permission has changed.
|
506
511
|
* args: (prevPermissions: [[ParticipantPermission]])
|
507
512
|
*/
|
508
513
|
ParticipantPermissionsChanged = 'participantPermissionsChanged',
|
@@ -10,6 +10,7 @@ import {
|
|
10
10
|
RequestResponse,
|
11
11
|
RequestResponse_Reason,
|
12
12
|
SimulcastCodec,
|
13
|
+
SipDTMF,
|
13
14
|
SubscribedQualityUpdate,
|
14
15
|
TrackInfo,
|
15
16
|
TrackUnpublishedResponse,
|
@@ -1349,6 +1350,27 @@ export default class LocalParticipant extends Participant {
|
|
1349
1350
|
await this.engine.sendDataPacket(packet, kind);
|
1350
1351
|
}
|
1351
1352
|
|
1353
|
+
/**
|
1354
|
+
* Publish SIP DTMF message to the room.
|
1355
|
+
*
|
1356
|
+
* @param code DTMF code
|
1357
|
+
* @param digit DTMF digit
|
1358
|
+
*/
|
1359
|
+
async publishDtmf(code: number, digit: string): Promise<void> {
|
1360
|
+
const packet = new DataPacket({
|
1361
|
+
kind: DataPacket_Kind.RELIABLE,
|
1362
|
+
value: {
|
1363
|
+
case: 'sipDtmf',
|
1364
|
+
value: new SipDTMF({
|
1365
|
+
code: code,
|
1366
|
+
digit: digit,
|
1367
|
+
}),
|
1368
|
+
},
|
1369
|
+
});
|
1370
|
+
|
1371
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1372
|
+
}
|
1373
|
+
|
1352
1374
|
async sendChatMessage(text: string): Promise<ChatMessage> {
|
1353
1375
|
const msg = {
|
1354
1376
|
id: crypto.randomUUID(),
|
@@ -9,7 +9,7 @@ import {
|
|
9
9
|
} from '@livekit/protocol';
|
10
10
|
import { EventEmitter } from 'events';
|
11
11
|
import type TypedEmitter from 'typed-emitter';
|
12
|
-
import log, { LoggerNames, StructuredLogger, getLogger } from '../../logger';
|
12
|
+
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
13
13
|
import { ParticipantEvent, TrackEvent } from '../events';
|
14
14
|
import LocalAudioTrack from '../track/LocalAudioTrack';
|
15
15
|
import type LocalTrackPublication from '../track/LocalTrackPublication';
|
@@ -279,7 +279,8 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
279
279
|
permissions.canPublishSources.length !== this.permissions.canPublishSources.length ||
|
280
280
|
permissions.canPublishSources.some(
|
281
281
|
(value, index) => value !== this.permissions?.canPublishSources[index],
|
282
|
-
)
|
282
|
+
) ||
|
283
|
+
permissions.canSubscribeMetrics !== this.permissions?.canSubscribeMetrics;
|
283
284
|
this.permissions = permissions;
|
284
285
|
|
285
286
|
if (changed) {
|
@@ -99,7 +99,7 @@ export default class LocalTrackPublication extends TrackPublication {
|
|
99
99
|
features.add(AudioTrackFeature.TF_STEREO);
|
100
100
|
}
|
101
101
|
if (!this.options?.dtx) {
|
102
|
-
features.add(AudioTrackFeature.
|
102
|
+
features.add(AudioTrackFeature.TF_NO_DTX);
|
103
103
|
}
|
104
104
|
if (this.track.enhancedNoiseCancellation) {
|
105
105
|
features.add(AudioTrackFeature.TF_ENHANCED_NOISE_CANCELLATION);
|
package/src/room/track/Track.ts
CHANGED
@@ -8,7 +8,7 @@ import {
|
|
8
8
|
import { EventEmitter } from 'events';
|
9
9
|
import type TypedEventEmitter from 'typed-emitter';
|
10
10
|
import type { SignalClient } from '../../api/SignalClient';
|
11
|
-
import log, { LoggerNames, StructuredLogger, getLogger } from '../../logger';
|
11
|
+
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
12
12
|
import { TrackEvent } from '../events';
|
13
13
|
import type { LoggerOptions } from '../types';
|
14
14
|
import { isFireFox, isSafari, isWeb } from '../utils';
|
package/src/room/track/utils.ts
CHANGED
package/src/room/utils.ts
CHANGED
@@ -9,7 +9,7 @@ import { protocolVersion, version } from '../version';
|
|
9
9
|
import CriticalTimers from './timers';
|
10
10
|
import type LocalAudioTrack from './track/LocalAudioTrack';
|
11
11
|
import type RemoteAudioTrack from './track/RemoteAudioTrack';
|
12
|
-
import { VideoCodec, videoCodecs } from './track/options';
|
12
|
+
import { type VideoCodec, videoCodecs } from './track/options';
|
13
13
|
import { getNewAudioContext } from './track/utils';
|
14
14
|
import type { ChatMessage, LiveKitReactNativeInfo, TranscriptionSegment } from './types';
|
15
15
|
|
package/src/test/mocks.ts
CHANGED