livekit-client 2.3.2 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
  /**