livekit-client 2.3.2 → 2.4.1

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 (60) 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 +8 -6
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +268 -45
  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/checks/reconnect.d.ts.map +1 -1
  12. package/dist/src/e2ee/errors.d.ts +2 -1
  13. package/dist/src/e2ee/errors.d.ts.map +1 -1
  14. package/dist/src/e2ee/index.d.ts +1 -0
  15. package/dist/src/e2ee/index.d.ts.map +1 -1
  16. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  17. package/dist/src/logger.d.ts.map +1 -1
  18. package/dist/src/room/RTCEngine.d.ts +2 -1
  19. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  20. package/dist/src/room/Room.d.ts +2 -0
  21. package/dist/src/room/Room.d.ts.map +1 -1
  22. package/dist/src/room/errors.d.ts +5 -0
  23. package/dist/src/room/errors.d.ts.map +1 -1
  24. package/dist/src/room/events.d.ts +15 -2
  25. package/dist/src/room/events.d.ts.map +1 -1
  26. package/dist/src/room/participant/LocalParticipant.d.ts +14 -6
  27. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  28. package/dist/src/room/participant/Participant.d.ts +8 -0
  29. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  30. package/dist/src/room/timers.d.ts +4 -4
  31. package/dist/src/room/timers.d.ts.map +1 -1
  32. package/dist/src/room/track/utils.d.ts +1 -0
  33. package/dist/src/room/track/utils.d.ts.map +1 -1
  34. package/dist/ts4.2/src/api/SignalClient.d.ts +5 -2
  35. package/dist/ts4.2/src/e2ee/errors.d.ts +2 -1
  36. package/dist/ts4.2/src/e2ee/index.d.ts +1 -0
  37. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -1
  38. package/dist/ts4.2/src/room/Room.d.ts +2 -0
  39. package/dist/ts4.2/src/room/errors.d.ts +5 -0
  40. package/dist/ts4.2/src/room/events.d.ts +15 -2
  41. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +14 -6
  42. package/dist/ts4.2/src/room/participant/Participant.d.ts +8 -0
  43. package/dist/ts4.2/src/room/timers.d.ts +4 -4
  44. package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
  45. package/package.json +2 -2
  46. package/src/api/SignalClient.ts +24 -2
  47. package/src/connectionHelper/checks/reconnect.ts +6 -3
  48. package/src/e2ee/errors.ts +8 -1
  49. package/src/e2ee/index.ts +1 -0
  50. package/src/e2ee/worker/FrameCryptor.ts +15 -3
  51. package/src/logger.ts +4 -3
  52. package/src/room/DeviceManager.ts +1 -1
  53. package/src/room/RTCEngine.ts +3 -0
  54. package/src/room/Room.ts +25 -3
  55. package/src/room/errors.ts +11 -0
  56. package/src/room/events.ts +15 -0
  57. package/src/room/participant/LocalParticipant.ts +92 -10
  58. package/src/room/participant/Participant.ts +23 -0
  59. package/src/room/track/utils.test.ts +35 -1
  60. package/src/room/track/utils.ts +22 -0
@@ -26,6 +26,7 @@ export default class LocalParticipant extends Participant {
26
26
  private roomOptions;
27
27
  private encryptionType;
28
28
  private reconnectFuture?;
29
+ private pendingSignalRequests;
29
30
  /** @internal */
30
31
  constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions);
31
32
  get lastCameraError(): Error | undefined;
@@ -40,22 +41,29 @@ export default class LocalParticipant extends Participant {
40
41
  private handleReconnecting;
41
42
  private handleReconnected;
42
43
  private handleDisconnected;
44
+ private handleSignalRequestError;
43
45
  /**
44
46
  * Sets and updates the metadata of the local participant.
45
- * The change does not take immediate effect.
46
- * If successful, a `ParticipantEvent.MetadataChanged` event will be emitted on the local participant.
47
47
  * Note: this requires `canUpdateOwnMetadata` permission.
48
+ * method will throw if the user doesn't have the required permissions
48
49
  * @param metadata
49
50
  */
50
- setMetadata(metadata: string): void;
51
+ setMetadata(metadata: string): Promise<void>;
51
52
  /**
52
53
  * Sets and updates the name of the local participant.
53
- * The change does not take immediate effect.
54
- * If successful, a `ParticipantEvent.ParticipantNameChanged` event will be emitted on the local participant.
55
54
  * Note: this requires `canUpdateOwnMetadata` permission.
55
+ * method will throw if the user doesn't have the required permissions
56
56
  * @param metadata
57
57
  */
58
- setName(name: string): void;
58
+ setName(name: string): Promise<void>;
59
+ /**
60
+ * Set or update participant attributes. It will make updates only to keys that
61
+ * are present in `attributes`, and will not override others.
62
+ * Note: this requires `canUpdateOwnMetadata` permission.
63
+ * @param attributes attributes to update
64
+ */
65
+ setAttributes(attributes: Record<string, string>): Promise<void>;
66
+ private requestMetadataUpdate;
59
67
  /**
60
68
  * Enable or disable a participant's camera track.
61
69
  *
@@ -39,6 +39,7 @@ export default class Participant extends Participant_base {
39
39
  name?: string;
40
40
  /** client metadata, opaque to livekit */
41
41
  metadata?: string;
42
+ private _attributes;
42
43
  lastSpokeAt?: Date | undefined;
43
44
  permissions?: ParticipantPermission;
44
45
  protected _kind: ParticipantKind;
@@ -52,6 +53,8 @@ export default class Participant extends Participant_base {
52
53
  get isEncrypted(): boolean;
53
54
  get isAgent(): boolean;
54
55
  get kind(): ParticipantKind;
56
+ /** participant attributes, similar to metadata, but as a key/value map */
57
+ get attributes(): Readonly<Record<string, string>>;
55
58
  /** @internal */
56
59
  constructor(sid: string, identity: string, name?: string, metadata?: string, loggerOptions?: LoggerOptions, kind?: ParticipantKind);
57
60
  getTrackPublications(): TrackPublication[];
@@ -78,6 +81,10 @@ export default class Participant extends Participant_base {
78
81
  **/
79
82
  private _setMetadata;
80
83
  private _setName;
84
+ /**
85
+ * Updates metadata from server
86
+ **/
87
+ private _setAttributes;
81
88
  /** @internal */
82
89
  setPermissions(permissions: ParticipantPermission): boolean;
83
90
  /** @internal */
@@ -113,5 +120,6 @@ export type ParticipantEventCallbacks = {
113
120
  audioStreamAcquired: () => void;
114
121
  participantPermissionsChanged: (prevPermissions?: ParticipantPermission) => void;
115
122
  trackSubscriptionStatusChanged: (publication: RemoteTrackPublication, status: TrackPublication.SubscriptionStatus) => void;
123
+ attributesChanged: (changedAttributes: Record<string, string>) => void;
116
124
  };
117
125
  //# sourceMappingURL=Participant.d.ts.map
@@ -4,9 +4,9 @@
4
4
  * that the timer fires on time.
5
5
  */
6
6
  export default class CriticalTimers {
7
- static setTimeout: (handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]) => number;
8
- static setInterval: (handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]) => number;
9
- static clearTimeout: (id: number | undefined) => void;
10
- static clearInterval: (id: number | undefined) => void;
7
+ static setTimeout: (callback: (args: void) => void, ms?: number | undefined) => NodeJS.Timeout;
8
+ static setInterval: (callback: (args: void) => void, ms?: number | undefined) => NodeJS.Timeout;
9
+ static clearTimeout: (timeoutId: string | number | NodeJS.Timeout | undefined) => void;
10
+ static clearInterval: (intervalId: string | number | NodeJS.Timeout | undefined) => void;
11
11
  }
12
12
  //# sourceMappingURL=timers.d.ts.map
@@ -30,4 +30,5 @@ export declare function mimeTypeToVideoCodecString(mimeType: string): "vp8" | "h
30
30
  export declare function getTrackPublicationInfo<T extends TrackPublication>(tracks: T[]): TrackPublishedResponse[];
31
31
  export declare function getLogContextFromTrack(track: Track | TrackPublication): Record<string, unknown>;
32
32
  export declare function supportsSynchronizationSources(): boolean;
33
+ export declare function diffAttributes(oldValues: Record<string, string> | undefined, newValues: Record<string, string> | undefined): Record<string, string>;
33
34
  //# sourceMappingURL=utils.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -36,7 +36,7 @@
36
36
  "author": "David Zhao <david@davidzhao.com>",
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
- "@livekit/protocol": "1.19.0",
39
+ "@livekit/protocol": "1.19.1",
40
40
  "events": "^3.3.0",
41
41
  "loglevel": "^1.8.0",
42
42
  "sdp-transform": "^2.14.1",
@@ -4,6 +4,7 @@ import {
4
4
  ClientInfo,
5
5
  ConnectionQualityUpdate,
6
6
  DisconnectReason,
7
+ ErrorResponse,
7
8
  JoinResponse,
8
9
  LeaveRequest,
9
10
  LeaveRequest_Action,
@@ -141,6 +142,8 @@ export class SignalClient {
141
142
 
142
143
  onLeave?: (leave: LeaveRequest) => void;
143
144
 
145
+ onErrorResponse?: (error: ErrorResponse) => void;
146
+
144
147
  connectOptions?: ConnectOpts;
145
148
 
146
149
  ws?: WebSocket;
@@ -163,6 +166,11 @@ export class SignalClient {
163
166
  );
164
167
  }
165
168
 
169
+ private getNextRequestId() {
170
+ this._requestId += 1;
171
+ return this._requestId;
172
+ }
173
+
166
174
  private options?: SignalOptions;
167
175
 
168
176
  private pingTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -183,6 +191,8 @@ export class SignalClient {
183
191
 
184
192
  private loggerContextCb?: LoggerOptions['loggerContextCb'];
185
193
 
194
+ private _requestId = 0;
195
+
186
196
  constructor(useJSON: boolean = false, loggerOptions: LoggerOptions = {}) {
187
197
  this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.Signal);
188
198
  this.loggerContextCb = loggerOptions.loggerContextCb;
@@ -511,14 +521,22 @@ export class SignalClient {
511
521
  });
512
522
  }
513
523
 
514
- sendUpdateLocalMetadata(metadata: string, name: string) {
515
- return this.sendRequest({
524
+ async sendUpdateLocalMetadata(
525
+ metadata: string,
526
+ name: string,
527
+ attributes: Record<string, string> = {},
528
+ ) {
529
+ const requestId = this.getNextRequestId();
530
+ await this.sendRequest({
516
531
  case: 'updateMetadata',
517
532
  value: new UpdateParticipantMetadata({
533
+ requestId,
518
534
  metadata,
519
535
  name,
536
+ attributes,
520
537
  }),
521
538
  });
539
+ return requestId;
522
540
  }
523
541
 
524
542
  sendUpdateTrackSettings(settings: UpdateTrackSettings) {
@@ -721,6 +739,10 @@ export class SignalClient {
721
739
  this.rtt = Date.now() - Number.parseInt(msg.value.lastPingTimestamp.toString());
722
740
  this.resetPingTimeout();
723
741
  pingHandled = true;
742
+ } else if (msg.case === 'errorResponse') {
743
+ if (this.onErrorResponse) {
744
+ this.onErrorResponse(msg.value);
745
+ }
724
746
  } else {
725
747
  this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
726
748
  }
@@ -18,10 +18,13 @@ export class ReconnectCheck extends Checker {
18
18
  reconnectResolver = resolve;
19
19
  });
20
20
 
21
+ const handleReconnecting = () => {
22
+ reconnectingTriggered = true;
23
+ };
24
+
21
25
  room
22
- .on(RoomEvent.Reconnecting, () => {
23
- reconnectingTriggered = true;
24
- })
26
+ .on(RoomEvent.SignalReconnecting, handleReconnecting)
27
+ .on(RoomEvent.Reconnecting, handleReconnecting)
25
28
  .on(RoomEvent.Reconnected, () => {
26
29
  reconnected = true;
27
30
  reconnectResolver(true);
@@ -9,8 +9,15 @@ export enum CryptorErrorReason {
9
9
  export class CryptorError extends LivekitError {
10
10
  reason: CryptorErrorReason;
11
11
 
12
- constructor(message?: string, reason: CryptorErrorReason = CryptorErrorReason.InternalError) {
12
+ participantIdentity?: string;
13
+
14
+ constructor(
15
+ message?: string,
16
+ reason: CryptorErrorReason = CryptorErrorReason.InternalError,
17
+ participantIdentity?: string,
18
+ ) {
13
19
  super(40, message);
14
20
  this.reason = reason;
21
+ this.participantIdentity = participantIdentity;
15
22
  }
16
23
  }
package/src/e2ee/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './KeyProvider';
2
2
  export * from './utils';
3
3
  export * from './types';
4
4
  export * from './events';
5
+ export * from './errors';
@@ -183,7 +183,12 @@ export class FrameCryptor extends BaseFrameCryptor {
183
183
  .pipeTo(writable)
184
184
  .catch((e) => {
185
185
  workerLogger.warn(e);
186
- this.emit(CryptorEvent.Error, e instanceof CryptorError ? e : new CryptorError(e.message));
186
+ this.emit(
187
+ CryptorEvent.Error,
188
+ e instanceof CryptorError
189
+ ? e
190
+ : new CryptorError(e.message, undefined, this.participantIdentity),
191
+ );
187
192
  });
188
193
  this.trackId = trackId;
189
194
  }
@@ -294,10 +299,14 @@ export class FrameCryptor extends BaseFrameCryptor {
294
299
  workerLogger.error(e);
295
300
  }
296
301
  } else {
297
- workerLogger.debug('failed to decrypt, emitting error', this.logContext);
302
+ workerLogger.debug('failed to encrypt, emitting error', this.logContext);
298
303
  this.emit(
299
304
  CryptorEvent.Error,
300
- new CryptorError(`encryption key missing for encoding`, CryptorErrorReason.MissingKey),
305
+ new CryptorError(
306
+ `encryption key missing for encoding`,
307
+ CryptorErrorReason.MissingKey,
308
+ this.participantIdentity,
309
+ ),
301
310
  );
302
311
  }
303
312
  }
@@ -367,6 +376,7 @@ export class FrameCryptor extends BaseFrameCryptor {
367
376
  new CryptorError(
368
377
  `missing key at index ${keyIndex} for participant ${this.participantIdentity}`,
369
378
  CryptorErrorReason.MissingKey,
379
+ this.participantIdentity,
370
380
  ),
371
381
  );
372
382
  }
@@ -487,12 +497,14 @@ export class FrameCryptor extends BaseFrameCryptor {
487
497
  throw new CryptorError(
488
498
  `valid key missing for participant ${this.participantIdentity}`,
489
499
  CryptorErrorReason.InvalidKey,
500
+ this.participantIdentity,
490
501
  );
491
502
  }
492
503
  } else {
493
504
  throw new CryptorError(
494
505
  `Decryption failed: ${error.message}`,
495
506
  CryptorErrorReason.InvalidKey,
507
+ this.participantIdentity,
496
508
  );
497
509
  }
498
510
  }
package/src/logger.ts CHANGED
@@ -54,9 +54,10 @@ export function getLogger(name: string) {
54
54
  export function setLogLevel(level: LogLevel | LogLevelString, loggerName?: LoggerNames) {
55
55
  if (loggerName) {
56
56
  log.getLogger(loggerName).setLevel(level);
57
- }
58
- for (const logger of livekitLoggers) {
59
- logger.setLevel(level);
57
+ } else {
58
+ for (const logger of livekitLoggers) {
59
+ logger.setLevel(level);
60
+ }
60
61
  }
61
62
  }
62
63
 
@@ -41,7 +41,7 @@ export default class DeviceManager {
41
41
  !(isSafari() && this.hasDeviceInUse(kind))
42
42
  ) {
43
43
  const isDummyDeviceOrEmpty =
44
- devices.length === 0 ||
44
+ devices.filter((d) => d.kind === kind).length === 0 ||
45
45
  devices.some((device) => {
46
46
  const noLabel = device.label === '';
47
47
  const isRelevant = kind ? device.kind === kind : true;
@@ -7,6 +7,7 @@ import {
7
7
  DataPacket,
8
8
  DataPacket_Kind,
9
9
  DisconnectReason,
10
+ ErrorResponse,
10
11
  type JoinResponse,
11
12
  type LeaveRequest,
12
13
  LeaveRequest_Action,
@@ -193,6 +194,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
193
194
  this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
194
195
  this.client.onSpeakersChanged = (update) => this.emit(EngineEvent.SpeakersChanged, update);
195
196
  this.client.onStreamStateUpdate = (update) => this.emit(EngineEvent.StreamStateChanged, update);
197
+ this.client.onErrorResponse = (error) => this.emit(EngineEvent.SignalRequestError, error);
196
198
  }
197
199
 
198
200
  /** @internal */
@@ -1412,6 +1414,7 @@ export type EngineEventCallbacks = {
1412
1414
  localTrackUnpublished: (unpublishedResponse: TrackUnpublishedResponse) => void;
1413
1415
  remoteMute: (trackSid: string, muted: boolean) => void;
1414
1416
  offline: () => void;
1417
+ signalRequestError: (error: ErrorResponse) => void;
1415
1418
  };
1416
1419
 
1417
1420
  function supportOptionalDatachannel(protocol: number | undefined): boolean {
package/src/room/Room.ts CHANGED
@@ -468,9 +468,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
468
468
  // trigger the first fetch without waiting for a response
469
469
  // if initial connection fails, this will speed up picking regional url
470
470
  // on subsequent runs
471
- this.regionUrlProvider.fetchRegionSettings().catch((e) => {
472
- this.log.warn('could not fetch region settings', { ...this.logContext, error: e });
473
- });
471
+ this.regionUrlProvider
472
+ .fetchRegionSettings()
473
+ .then((settings) => {
474
+ this.regionUrlProvider?.setServerReportedRegions(settings);
475
+ })
476
+ .catch((e) => {
477
+ this.log.warn('could not fetch region settings', { ...this.logContext, error: e });
478
+ });
474
479
  }
475
480
 
476
481
  const connectFn = async (
@@ -1116,6 +1121,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1116
1121
  this.localParticipant
1117
1122
  .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
1118
1123
  .on(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged)
1124
+ .on(ParticipantEvent.AttributesChanged, this.onLocalAttributesChanged)
1119
1125
  .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
1120
1126
  .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
1121
1127
  .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
@@ -1294,6 +1300,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1294
1300
  this.localParticipant
1295
1301
  .off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
1296
1302
  .off(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged)
1303
+ .off(ParticipantEvent.AttributesChanged, this.onLocalAttributesChanged)
1297
1304
  .off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
1298
1305
  .off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
1299
1306
  .off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
@@ -1717,6 +1724,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1717
1724
  .on(ParticipantEvent.ParticipantNameChanged, (name) => {
1718
1725
  this.emitWhenConnected(RoomEvent.ParticipantNameChanged, name, participant);
1719
1726
  })
1727
+ .on(ParticipantEvent.AttributesChanged, (changedAttributes: Record<string, string>) => {
1728
+ this.emitWhenConnected(
1729
+ RoomEvent.ParticipantAttributesChanged,
1730
+ changedAttributes,
1731
+ participant,
1732
+ );
1733
+ })
1720
1734
  .on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
1721
1735
  this.emitWhenConnected(RoomEvent.ConnectionQualityChanged, quality, participant);
1722
1736
  })
@@ -1865,6 +1879,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1865
1879
  this.emit(RoomEvent.ParticipantNameChanged, name, this.localParticipant);
1866
1880
  };
1867
1881
 
1882
+ private onLocalAttributesChanged = (changedAttributes: Record<string, string>) => {
1883
+ this.emit(RoomEvent.ParticipantAttributesChanged, changedAttributes, this.localParticipant);
1884
+ };
1885
+
1868
1886
  private onLocalTrackMuted = (pub: TrackPublication) => {
1869
1887
  this.emit(RoomEvent.TrackMuted, pub, this.localParticipant);
1870
1888
  };
@@ -2131,6 +2149,10 @@ export type RoomEventCallbacks = {
2131
2149
  prevPermissions: ParticipantPermission | undefined,
2132
2150
  participant: RemoteParticipant | LocalParticipant,
2133
2151
  ) => void;
2152
+ participantAttributesChanged: (
2153
+ changedAttributes: Record<string, string>,
2154
+ participant: RemoteParticipant | LocalParticipant,
2155
+ ) => void;
2134
2156
  activeSpeakersChanged: (speakers: Array<Participant>) => void;
2135
2157
  roomMetadataChanged: (metadata: string) => void;
2136
2158
  dataReceived: (
@@ -1,3 +1,5 @@
1
+ import { ErrorResponse_Reason } from '@livekit/protocol';
2
+
1
3
  export class LivekitError extends Error {
2
4
  code: number;
3
5
 
@@ -63,6 +65,15 @@ export class PublishDataError extends LivekitError {
63
65
  }
64
66
  }
65
67
 
68
+ export class SignalRequestError extends LivekitError {
69
+ reason: ErrorResponse_Reason;
70
+
71
+ constructor(message: string, reason: ErrorResponse_Reason = ErrorResponse_Reason.UNKNOWN) {
72
+ super(15, message);
73
+ this.reason = reason;
74
+ }
75
+ }
76
+
66
77
  export enum MediaDeviceFailure {
67
78
  // user rejected permissions
68
79
  PermissionDenied = 'PermissionDenied',
@@ -185,6 +185,13 @@ export enum RoomEvent {
185
185
  */
186
186
  ParticipantNameChanged = 'participantNameChanged',
187
187
 
188
+ /**
189
+ * Participant attributes is an app-specific key value state to be pushed to
190
+ * all users.
191
+ * When a participant's attributes changed, this event will be emitted with the changed attributes and the participant
192
+ */
193
+ ParticipantAttributesChanged = 'participantAttributesChanged',
194
+
188
195
  /**
189
196
  * Room metadata is a simple way for app-specific state to be pushed to
190
197
  * all users.
@@ -495,6 +502,13 @@ export enum ParticipantEvent {
495
502
 
496
503
  /** @internal */
497
504
  PCTrackAdded = 'pcTrackAdded',
505
+
506
+ /**
507
+ * Participant attributes is an app-specific key value state to be pushed to
508
+ * all users.
509
+ * When a participant's attributes changed, this event will be emitted with the changed attributes
510
+ */
511
+ AttributesChanged = 'attributesChanged',
498
512
  }
499
513
 
500
514
  /** @internal */
@@ -525,6 +539,7 @@ export enum EngineEvent {
525
539
  SubscribedQualityUpdate = 'subscribedQualityUpdate',
526
540
  LocalTrackUnpublished = 'localTrackUnpublished',
527
541
  Offline = 'offline',
542
+ SignalRequestError = 'signalRequestError',
528
543
  }
529
544
 
530
545
  export enum TrackEvent {
@@ -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
  /**