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.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +14 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +325 -175
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +5 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/e2ee/errors.d.ts +2 -1
- package/dist/src/e2ee/errors.d.ts.map +1 -1
- package/dist/src/e2ee/index.d.ts +1 -0
- package/dist/src/e2ee/index.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +2 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +5 -0
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +15 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +14 -6
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +8 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +4 -4
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +1 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +5 -2
- package/dist/ts4.2/src/e2ee/errors.d.ts +2 -1
- package/dist/ts4.2/src/e2ee/index.d.ts +1 -0
- package/dist/ts4.2/src/room/PCTransport.d.ts +1 -2
- package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -1
- package/dist/ts4.2/src/room/Room.d.ts +1 -0
- package/dist/ts4.2/src/room/errors.d.ts +5 -0
- package/dist/ts4.2/src/room/events.d.ts +15 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +14 -6
- package/dist/ts4.2/src/room/participant/Participant.d.ts +8 -0
- package/dist/ts4.2/src/room/timers.d.ts +4 -4
- package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
- package/package.json +12 -12
- package/src/api/SignalClient.ts +24 -2
- package/src/e2ee/errors.ts +8 -1
- package/src/e2ee/index.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +18 -4
- package/src/e2ee/worker/e2ee.worker.ts +5 -1
- package/src/logger.ts +4 -3
- package/src/room/DeviceManager.ts +1 -1
- package/src/room/RTCEngine.ts +3 -0
- package/src/room/Room.ts +11 -3
- package/src/room/errors.ts +11 -0
- package/src/room/events.ts +15 -0
- package/src/room/participant/LocalParticipant.ts +102 -10
- package/src/room/participant/Participant.ts +23 -0
- package/src/room/track/Track.ts +1 -1
- package/src/room/track/utils.test.ts +35 -1
- 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 {
|
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.
|
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.
|
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
|
};
|
package/src/room/track/Track.ts
CHANGED
@@ -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
|
+
});
|
package/src/room/track/utils.ts
CHANGED
@@ -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
|
+
}
|