livekit-client 2.3.1 → 2.4.0

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 (71) 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 +14 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +325 -175
  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/api/SignalClient.d.ts +5 -2
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  13. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  14. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  15. package/dist/src/e2ee/errors.d.ts +2 -1
  16. package/dist/src/e2ee/errors.d.ts.map +1 -1
  17. package/dist/src/e2ee/index.d.ts +1 -0
  18. package/dist/src/e2ee/index.d.ts.map +1 -1
  19. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  20. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  21. package/dist/src/logger.d.ts.map +1 -1
  22. package/dist/src/room/PCTransport.d.ts +1 -2
  23. package/dist/src/room/PCTransport.d.ts.map +1 -1
  24. package/dist/src/room/RTCEngine.d.ts +2 -1
  25. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +1 -0
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/errors.d.ts +5 -0
  29. package/dist/src/room/errors.d.ts.map +1 -1
  30. package/dist/src/room/events.d.ts +15 -2
  31. package/dist/src/room/events.d.ts.map +1 -1
  32. package/dist/src/room/participant/LocalParticipant.d.ts +14 -6
  33. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  34. package/dist/src/room/participant/Participant.d.ts +8 -0
  35. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  36. package/dist/src/room/timers.d.ts +4 -4
  37. package/dist/src/room/timers.d.ts.map +1 -1
  38. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  39. package/dist/src/room/track/Track.d.ts.map +1 -1
  40. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  41. package/dist/src/room/track/utils.d.ts +1 -0
  42. package/dist/src/room/track/utils.d.ts.map +1 -1
  43. package/dist/ts4.2/src/api/SignalClient.d.ts +5 -2
  44. package/dist/ts4.2/src/e2ee/errors.d.ts +2 -1
  45. package/dist/ts4.2/src/e2ee/index.d.ts +1 -0
  46. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -2
  47. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -1
  48. package/dist/ts4.2/src/room/Room.d.ts +1 -0
  49. package/dist/ts4.2/src/room/errors.d.ts +5 -0
  50. package/dist/ts4.2/src/room/events.d.ts +15 -2
  51. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +14 -6
  52. package/dist/ts4.2/src/room/participant/Participant.d.ts +8 -0
  53. package/dist/ts4.2/src/room/timers.d.ts +4 -4
  54. package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
  55. package/package.json +12 -12
  56. package/src/api/SignalClient.ts +24 -2
  57. package/src/e2ee/errors.ts +8 -1
  58. package/src/e2ee/index.ts +1 -0
  59. package/src/e2ee/worker/FrameCryptor.ts +18 -4
  60. package/src/e2ee/worker/e2ee.worker.ts +5 -1
  61. package/src/logger.ts +4 -3
  62. package/src/room/DeviceManager.ts +1 -1
  63. package/src/room/RTCEngine.ts +3 -0
  64. package/src/room/Room.ts +11 -3
  65. package/src/room/errors.ts +11 -0
  66. package/src/room/events.ts +15 -0
  67. package/src/room/participant/LocalParticipant.ts +102 -10
  68. package/src/room/participant/Participant.ts +23 -0
  69. package/src/room/track/Track.ts +1 -1
  70. package/src/room/track/utils.test.ts +35 -1
  71. package/src/room/track/utils.ts +22 -0
@@ -3,6 +3,7 @@ import {
3
3
  DataPacket,
4
4
  DataPacket_Kind,
5
5
  Encryption_Type,
6
+ ErrorResponse,
6
7
  ParticipantInfo,
7
8
  ParticipantPermission,
8
9
  SimulcastCodec,
@@ -14,7 +15,13 @@ import type { InternalRoomOptions } from '../../options';
14
15
  import { PCTransportState } from '../PCTransportManager';
15
16
  import type RTCEngine from '../RTCEngine';
16
17
  import { defaultVideoCodec } from '../defaults';
17
- import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
18
+ import {
19
+ DeviceUnsupportedError,
20
+ LivekitError,
21
+ SignalRequestError,
22
+ TrackInvalidError,
23
+ UnexpectedConnectionState,
24
+ } from '../errors';
18
25
  import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
19
26
  import LocalAudioTrack from '../track/LocalAudioTrack';
20
27
  import LocalTrack from '../track/LocalTrack';
@@ -46,6 +53,7 @@ import {
46
53
  isSVCCodec,
47
54
  isSafari17,
48
55
  isWeb,
56
+ sleep,
49
57
  supportsAV1,
50
58
  supportsVP9,
51
59
  } from '../utils';
@@ -92,6 +100,15 @@ export default class LocalParticipant extends Participant {
92
100
 
93
101
  private reconnectFuture?: Future<void>;
94
102
 
103
+ private pendingSignalRequests: Map<
104
+ number,
105
+ {
106
+ resolve: (arg: any) => void;
107
+ reject: (reason: LivekitError) => void;
108
+ values: Partial<Record<keyof LocalParticipant, any>>;
109
+ }
110
+ >;
111
+
95
112
  /** @internal */
96
113
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
97
114
  super(sid, identity, undefined, undefined, {
@@ -105,6 +122,7 @@ export default class LocalParticipant extends Participant {
105
122
  this.roomOptions = options;
106
123
  this.setupEngine(engine);
107
124
  this.activeDeviceMap = new Map();
125
+ this.pendingSignalRequests = new Map();
108
126
  }
109
127
 
110
128
  get lastCameraError(): Error | undefined {
@@ -158,7 +176,8 @@ export default class LocalParticipant extends Participant {
158
176
  .on(EngineEvent.Resuming, this.handleReconnecting)
159
177
  .on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
160
178
  .on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
161
- .on(EngineEvent.Disconnected, this.handleDisconnected);
179
+ .on(EngineEvent.Disconnected, this.handleDisconnected)
180
+ .on(EngineEvent.SignalRequestError, this.handleSignalRequestError);
162
181
  }
163
182
 
164
183
  private handleReconnecting = () => {
@@ -181,26 +200,89 @@ export default class LocalParticipant extends Participant {
181
200
  }
182
201
  };
183
202
 
203
+ private handleSignalRequestError = (error: ErrorResponse) => {
204
+ const { requestId, reason, message } = error;
205
+ const failedRequest = this.pendingSignalRequests.get(requestId);
206
+ if (failedRequest) {
207
+ failedRequest.reject(new SignalRequestError(message, reason));
208
+ this.pendingSignalRequests.delete(requestId);
209
+ }
210
+ };
211
+
184
212
  /**
185
213
  * Sets and updates the metadata of the local participant.
186
- * The change does not take immediate effect.
187
- * If successful, a `ParticipantEvent.MetadataChanged` event will be emitted on the local participant.
188
214
  * Note: this requires `canUpdateOwnMetadata` permission.
215
+ * method will throw if the user doesn't have the required permissions
189
216
  * @param metadata
190
217
  */
191
- setMetadata(metadata: string): void {
192
- this.engine.client.sendUpdateLocalMetadata(metadata, this.name ?? '');
218
+ async setMetadata(metadata: string): Promise<void> {
219
+ await this.requestMetadataUpdate({ metadata });
193
220
  }
194
221
 
195
222
  /**
196
223
  * Sets and updates the name of the local participant.
197
- * The change does not take immediate effect.
198
- * If successful, a `ParticipantEvent.ParticipantNameChanged` event will be emitted on the local participant.
199
224
  * Note: this requires `canUpdateOwnMetadata` permission.
225
+ * method will throw if the user doesn't have the required permissions
200
226
  * @param metadata
201
227
  */
202
- setName(name: string): void {
203
- this.engine.client.sendUpdateLocalMetadata(this.metadata ?? '', name);
228
+ async setName(name: string): Promise<void> {
229
+ await this.requestMetadataUpdate({ name });
230
+ }
231
+
232
+ /**
233
+ * Set or update participant attributes. It will make updates only to keys that
234
+ * are present in `attributes`, and will not override others.
235
+ * Note: this requires `canUpdateOwnMetadata` permission.
236
+ * @param attributes attributes to update
237
+ */
238
+ async setAttributes(attributes: Record<string, string>) {
239
+ await this.requestMetadataUpdate({ attributes });
240
+ }
241
+
242
+ private async requestMetadataUpdate({
243
+ metadata,
244
+ name,
245
+ attributes,
246
+ }: {
247
+ metadata?: string;
248
+ name?: string;
249
+ attributes?: Record<string, string>;
250
+ }) {
251
+ return new Promise<void>(async (resolve, reject) => {
252
+ try {
253
+ let isRejected = false;
254
+ const requestId = await this.engine.client.sendUpdateLocalMetadata(
255
+ metadata ?? this.metadata ?? '',
256
+ name ?? this.name ?? '',
257
+ attributes,
258
+ );
259
+ const startTime = performance.now();
260
+ this.pendingSignalRequests.set(requestId, {
261
+ resolve,
262
+ reject: (error: LivekitError) => {
263
+ reject(error);
264
+ isRejected = true;
265
+ },
266
+ values: { name, metadata, attributes },
267
+ });
268
+ while (performance.now() - startTime < 5_000 && !isRejected) {
269
+ if (
270
+ (!name || this.name === name) &&
271
+ (!metadata || this.metadata === metadata) &&
272
+ (!attributes ||
273
+ Object.entries(attributes).every(([key, value]) => this.attributes[key] === value))
274
+ ) {
275
+ this.pendingSignalRequests.delete(requestId);
276
+ resolve();
277
+ return;
278
+ }
279
+ await sleep(50);
280
+ }
281
+ reject(new SignalRequestError('Request to update local metadata timed out'));
282
+ } catch (e: any) {
283
+ if (e instanceof Error) reject(e);
284
+ }
285
+ });
204
286
  }
205
287
 
206
288
  /**
@@ -1014,6 +1096,16 @@ export default class LocalParticipant extends Participant {
1014
1096
  track: LocalTrack | MediaStreamTrack,
1015
1097
  stopOnUnpublish?: boolean,
1016
1098
  ): Promise<LocalTrackPublication | undefined> {
1099
+ if (track instanceof LocalTrack) {
1100
+ const publishPromise = this.pendingPublishPromises.get(track);
1101
+ if (publishPromise) {
1102
+ this.log.info('awaiting publish promise before attempting to unpublish', {
1103
+ ...this.logContext,
1104
+ ...getLogContextFromTrack(track),
1105
+ });
1106
+ await publishPromise;
1107
+ }
1108
+ }
1017
1109
  // look through all published tracks to find the right ones
1018
1110
  const publication = this.getPublicationForTrack(track);
1019
1111
 
@@ -18,6 +18,7 @@ import type RemoteTrack from '../track/RemoteTrack';
18
18
  import type RemoteTrackPublication from '../track/RemoteTrackPublication';
19
19
  import { Track } from '../track/Track';
20
20
  import type { TrackPublication } from '../track/TrackPublication';
21
+ import { diffAttributes } from '../track/utils';
21
22
  import type { LoggerOptions, TranscriptionSegment } from '../types';
22
23
 
23
24
  export enum ConnectionQuality {
@@ -77,6 +78,8 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
77
78
  /** client metadata, opaque to livekit */
78
79
  metadata?: string;
79
80
 
81
+ private _attributes: Record<string, string>;
82
+
80
83
  lastSpokeAt?: Date | undefined;
81
84
 
82
85
  permissions?: ParticipantPermission;
@@ -112,6 +115,11 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
112
115
  return this._kind;
113
116
  }
114
117
 
118
+ /** participant attributes, similar to metadata, but as a key/value map */
119
+ get attributes(): Readonly<Record<string, string>> {
120
+ return Object.freeze({ ...this._attributes });
121
+ }
122
+
115
123
  /** @internal */
116
124
  constructor(
117
125
  sid: string,
@@ -135,6 +143,7 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
135
143
  this.videoTrackPublications = new Map();
136
144
  this.trackPublications = new Map();
137
145
  this._kind = kind;
146
+ this._attributes = {};
138
147
  }
139
148
 
140
149
  getTrackPublications(): TrackPublication[] {
@@ -214,6 +223,7 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
214
223
  this.sid = info.sid;
215
224
  this._setName(info.name);
216
225
  this._setMetadata(info.metadata);
226
+ this._setAttributes(info.attributes);
217
227
  if (info.permission) {
218
228
  this.setPermissions(info.permission);
219
229
  }
@@ -245,6 +255,18 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
245
255
  }
246
256
  }
247
257
 
258
+ /**
259
+ * Updates metadata from server
260
+ **/
261
+ private _setAttributes(attributes: Record<string, string>) {
262
+ const diff = diffAttributes(attributes, this.attributes);
263
+ this._attributes = attributes;
264
+
265
+ if (Object.keys(diff).length > 0) {
266
+ this.emit(ParticipantEvent.AttributesChanged, diff);
267
+ }
268
+ }
269
+
248
270
  /** @internal */
249
271
  setPermissions(permissions: ParticipantPermission): boolean {
250
272
  const prevPermissions = this.permissions;
@@ -363,4 +385,5 @@ export type ParticipantEventCallbacks = {
363
385
  publication: RemoteTrackPublication,
364
386
  status: TrackPublication.SubscriptionStatus,
365
387
  ) => void;
388
+ attributesChanged: (changedAttributes: Record<string, string>) => void;
366
389
  };
@@ -129,7 +129,7 @@ export abstract class Track<
129
129
  if (this.kind === Track.Kind.Video) {
130
130
  elementType = 'video';
131
131
  }
132
- if (this.attachedElements.length === 0 && Track.Kind.Video) {
132
+ if (this.attachedElements.length === 0 && this.kind === Track.Kind.Video) {
133
133
  this.addAppVisibilityListener();
134
134
  }
135
135
  if (!element) {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { AudioCaptureOptions, VideoCaptureOptions, VideoPresets } from './options';
3
- import { constraintsForOptions, mergeDefaultOptions } from './utils';
3
+ import { constraintsForOptions, diffAttributes, mergeDefaultOptions } from './utils';
4
4
 
5
5
  describe('mergeDefaultOptions', () => {
6
6
  const audioDefaults: AudioCaptureOptions = {
@@ -109,3 +109,37 @@ describe('constraintsForOptions', () => {
109
109
  expect(videoOpts.aspectRatio).toEqual(VideoPresets.h720.resolution.aspectRatio);
110
110
  });
111
111
  });
112
+
113
+ describe('diffAttributes', () => {
114
+ it('detects changed values', () => {
115
+ const oldValues: Record<string, string> = { a: 'value', b: 'initial', c: 'value' };
116
+ const newValues: Record<string, string> = { a: 'value', b: 'updated', c: 'value' };
117
+
118
+ const diff = diffAttributes(oldValues, newValues);
119
+ expect(Object.keys(diff).length).toBe(1);
120
+ expect(diff.b).toBe('updated');
121
+ });
122
+ it('detects new values', () => {
123
+ const newValues: Record<string, string> = { a: 'value', b: 'value', c: 'value' };
124
+ const oldValues: Record<string, string> = { a: 'value', b: 'value' };
125
+
126
+ const diff = diffAttributes(oldValues, newValues);
127
+ expect(Object.keys(diff).length).toBe(1);
128
+ expect(diff.c).toBe('value');
129
+ });
130
+ it('detects deleted values as empty strings', () => {
131
+ const newValues: Record<string, string> = { a: 'value', b: 'value' };
132
+ const oldValues: Record<string, string> = { a: 'value', b: 'value', c: 'value' };
133
+
134
+ const diff = diffAttributes(oldValues, newValues);
135
+ expect(Object.keys(diff).length).toBe(1);
136
+ expect(diff.c).toBe('');
137
+ });
138
+ it('compares with undefined values', () => {
139
+ const newValues: Record<string, string> = { a: 'value', b: 'value' };
140
+
141
+ const diff = diffAttributes(undefined, newValues);
142
+ expect(Object.keys(diff).length).toBe(2);
143
+ expect(diff.a).toBe('value');
144
+ });
145
+ });
@@ -243,3 +243,25 @@ export function getLogContextFromTrack(track: Track | TrackPublication): Record<
243
243
  export function supportsSynchronizationSources(): boolean {
244
244
  return typeof RTCRtpReceiver !== 'undefined' && 'getSynchronizationSources' in RTCRtpReceiver;
245
245
  }
246
+
247
+ export function diffAttributes(
248
+ oldValues: Record<string, string> | undefined,
249
+ newValues: Record<string, string> | undefined,
250
+ ) {
251
+ if (oldValues === undefined) {
252
+ oldValues = {};
253
+ }
254
+ if (newValues === undefined) {
255
+ newValues = {};
256
+ }
257
+ const allKeys = [...Object.keys(newValues), ...Object.keys(oldValues)];
258
+ const diff: Record<string, string> = {};
259
+
260
+ for (const key of allKeys) {
261
+ if (oldValues[key] !== newValues[key]) {
262
+ diff[key] = newValues[key] ?? '';
263
+ }
264
+ }
265
+
266
+ return diff;
267
+ }