livekit-client 2.5.6 → 2.5.7

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 (45) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +624 -603
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2123 -1948
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/e2ee/worker/FrameCryptor.d.ts +2 -2
  10. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +2 -2
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/room/Room.d.ts +2 -1
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/events.d.ts +2 -2
  16. package/dist/src/room/participant/Participant.d.ts +1 -1
  17. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  18. package/dist/src/room/track/Track.d.ts +1 -1
  19. package/dist/src/room/track/Track.d.ts.map +1 -1
  20. package/dist/src/room/utils.d.ts +1 -1
  21. package/dist/src/room/utils.d.ts.map +1 -1
  22. package/dist/src/test/mocks.d.ts +1 -1
  23. package/dist/src/test/mocks.d.ts.map +1 -1
  24. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -2
  25. package/dist/ts4.2/src/index.d.ts +2 -2
  26. package/dist/ts4.2/src/room/Room.d.ts +2 -1
  27. package/dist/ts4.2/src/room/events.d.ts +2 -2
  28. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
  29. package/dist/ts4.2/src/room/track/Track.d.ts +1 -1
  30. package/dist/ts4.2/src/room/utils.d.ts +1 -1
  31. package/dist/ts4.2/src/test/mocks.d.ts +1 -1
  32. package/package.json +10 -11
  33. package/src/e2ee/worker/FrameCryptor.test.ts +249 -2
  34. package/src/e2ee/worker/FrameCryptor.ts +3 -3
  35. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +122 -0
  36. package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
  37. package/src/e2ee/worker/e2ee.worker.ts +1 -1
  38. package/src/index.ts +2 -0
  39. package/src/room/Room.ts +16 -10
  40. package/src/room/events.ts +2 -2
  41. package/src/room/participant/Participant.ts +3 -2
  42. package/src/room/track/Track.ts +1 -1
  43. package/src/room/track/utils.ts +1 -1
  44. package/src/room/utils.ts +1 -1
  45. package/src/test/mocks.ts +1 -1
@@ -1,13 +1,109 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { isFrameServerInjected } from './FrameCryptor';
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 > 255) {
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
@@ -135,6 +135,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
135
135
  /** reflects the sender encryption status of the local participant */
136
136
  isE2EEEnabled: boolean = false;
137
137
 
138
+ serverInfo?: Partial<ServerInfo>;
139
+
138
140
  private roomInfo?: RoomModel;
139
141
 
140
142
  private sidToIdentity: Map<string, string>;
@@ -609,6 +611,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
609
611
  if (!serverInfo) {
610
612
  serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
611
613
  }
614
+ this.serverInfo = serverInfo;
612
615
 
613
616
  this.log.debug(
614
617
  `connected to Livekit Server ${Object.entries(serverInfo)
@@ -621,11 +624,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
621
624
  },
622
625
  );
623
626
 
624
- if (!joinResponse.serverVersion) {
627
+ if (!serverInfo.version) {
625
628
  throw new UnsupportedServer('unknown server version');
626
629
  }
627
630
 
628
- if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
631
+ if (serverInfo.version === '0.15.1' && this.options.dynacast) {
629
632
  this.log.debug('disabling dynacast due to server version', this.logContext);
630
633
  // dynacast has a bug in 0.15.1, so we cannot use it then
631
634
  roomOptions.dynacast = false;
@@ -1490,14 +1493,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1490
1493
  if (!pub || !pub.track) {
1491
1494
  return;
1492
1495
  }
1493
- pub.track.streamState = Track.streamStateFromProto(streamState.state);
1494
- participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
1495
- this.emitWhenConnected(
1496
- RoomEvent.TrackStreamStateChanged,
1497
- pub,
1498
- pub.track.streamState,
1499
- participant,
1500
- );
1496
+ const newStreamState = Track.streamStateFromProto(streamState.state);
1497
+ if (newStreamState !== pub.track.streamState) {
1498
+ pub.track.streamState = newStreamState;
1499
+ participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
1500
+ this.emitWhenConnected(
1501
+ RoomEvent.TrackStreamStateChanged,
1502
+ pub,
1503
+ pub.track.streamState,
1504
+ participant,
1505
+ );
1506
+ }
1501
1507
  });
1502
1508
  };
1503
1509
 
@@ -294,7 +294,7 @@ export enum RoomEvent {
294
294
  MediaDevicesError = 'mediaDevicesError',
295
295
 
296
296
  /**
297
- * A participant's permission has changed. Currently only fired on LocalParticipant.
297
+ * A participant's permission has changed.
298
298
  * args: (prevPermissions: [[ParticipantPermission]], participant: [[Participant]])
299
299
  */
300
300
  ParticipantPermissionsChanged = 'participantPermissionsChanged',
@@ -502,7 +502,7 @@ export enum ParticipantEvent {
502
502
  AudioStreamAcquired = 'audioStreamAcquired',
503
503
 
504
504
  /**
505
- * A participant's permission has changed. Currently only fired on LocalParticipant.
505
+ * A participant's permission has changed.
506
506
  * args: (prevPermissions: [[ParticipantPermission]])
507
507
  */
508
508
  ParticipantPermissionsChanged = 'participantPermissionsChanged',
@@ -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) {
@@ -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';
@@ -8,7 +8,7 @@ import {
8
8
  type CreateLocalTracksOptions,
9
9
  type ScreenShareCaptureOptions,
10
10
  type VideoCaptureOptions,
11
- VideoCodec,
11
+ type VideoCodec,
12
12
  videoCodecs,
13
13
  } from './options';
14
14
  import type { AudioTrack } from './types';
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
@@ -1,5 +1,5 @@
1
1
  // eslint-disable-next-line import/no-extraneous-dependencies
2
- import { MockedClass, vi } from 'vitest';
2
+ import { type MockedClass, vi } from 'vitest';
3
3
  import { SignalClient } from '../api/SignalClient';
4
4
  import RTCEngine from '../room/RTCEngine';
5
5