livekit-client 2.5.10 → 2.6.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/README.md +54 -0
- package/dist/livekit-client.esm.mjs +418 -32
- 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/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +56 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc.d.ts +96 -0
- package/dist/src/room/rpc.d.ts.map +1 -0
- package/dist/src/room/track/utils.d.ts +2 -2
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +2 -0
- package/dist/ts4.2/src/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +56 -0
- package/dist/ts4.2/src/room/rpc.d.ts +96 -0
- package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/room/PCTransport.ts +42 -29
- package/src/room/Room.ts +1 -0
- package/src/room/participant/LocalParticipant.test.ts +304 -0
- package/src/room/participant/LocalParticipant.ts +340 -1
- package/src/room/rpc.ts +172 -0
- package/src/room/track/utils.ts +1 -6
package/src/room/PCTransport.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { EventEmitter } from 'events';
|
2
|
-
import type { MediaDescription } from 'sdp-transform';
|
2
|
+
import type { MediaDescription, SessionDescription } from 'sdp-transform';
|
3
3
|
import { parse, write } from 'sdp-transform';
|
4
4
|
import { debounce } from 'ts-debounce';
|
5
5
|
import log, { LoggerNames, getLogger } from '../logger';
|
@@ -48,6 +48,8 @@ export default class PCTransport extends EventEmitter {
|
|
48
48
|
|
49
49
|
private loggerOptions: LoggerOptions;
|
50
50
|
|
51
|
+
private ddExtID = 0;
|
52
|
+
|
51
53
|
pendingCandidates: RTCIceCandidateInit[] = [];
|
52
54
|
|
53
55
|
restartingIce: boolean = false;
|
@@ -288,7 +290,7 @@ export default class PCTransport extends EventEmitter {
|
|
288
290
|
}
|
289
291
|
|
290
292
|
if (isSVCCodec(trackbr.codec)) {
|
291
|
-
ensureVideoDDExtensionForSVC(media);
|
293
|
+
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
|
292
294
|
}
|
293
295
|
|
294
296
|
// TODO: av1 slow starting issue already fixed in chrome 124, clean this after some versions
|
@@ -503,6 +505,44 @@ export default class PCTransport extends EventEmitter {
|
|
503
505
|
throw new NegotiationError(msg);
|
504
506
|
}
|
505
507
|
}
|
508
|
+
|
509
|
+
private ensureVideoDDExtensionForSVC(
|
510
|
+
media: {
|
511
|
+
type: string;
|
512
|
+
port: number;
|
513
|
+
protocol: string;
|
514
|
+
payloads?: string | undefined;
|
515
|
+
} & MediaDescription,
|
516
|
+
sdp: SessionDescription,
|
517
|
+
) {
|
518
|
+
const ddFound = media.ext?.some((ext): boolean => {
|
519
|
+
if (ext.uri === ddExtensionURI) {
|
520
|
+
return true;
|
521
|
+
}
|
522
|
+
return false;
|
523
|
+
});
|
524
|
+
|
525
|
+
if (!ddFound) {
|
526
|
+
if (this.ddExtID === 0) {
|
527
|
+
let maxID = 0;
|
528
|
+
sdp.media.forEach((m) => {
|
529
|
+
if (m.type !== 'video') {
|
530
|
+
return;
|
531
|
+
}
|
532
|
+
m.ext?.forEach((ext) => {
|
533
|
+
if (ext.value > maxID) {
|
534
|
+
maxID = ext.value;
|
535
|
+
}
|
536
|
+
});
|
537
|
+
});
|
538
|
+
this.ddExtID = maxID + 1;
|
539
|
+
}
|
540
|
+
media.ext?.push({
|
541
|
+
value: this.ddExtID,
|
542
|
+
uri: ddExtensionURI,
|
543
|
+
});
|
544
|
+
}
|
545
|
+
}
|
506
546
|
}
|
507
547
|
|
508
548
|
function ensureAudioNackAndStereo(
|
@@ -555,33 +595,6 @@ function ensureAudioNackAndStereo(
|
|
555
595
|
}
|
556
596
|
}
|
557
597
|
|
558
|
-
function ensureVideoDDExtensionForSVC(
|
559
|
-
media: {
|
560
|
-
type: string;
|
561
|
-
port: number;
|
562
|
-
protocol: string;
|
563
|
-
payloads?: string | undefined;
|
564
|
-
} & MediaDescription,
|
565
|
-
) {
|
566
|
-
let maxID = 0;
|
567
|
-
const ddFound = media.ext?.some((ext): boolean => {
|
568
|
-
if (ext.uri === ddExtensionURI) {
|
569
|
-
return true;
|
570
|
-
}
|
571
|
-
if (ext.value > maxID) {
|
572
|
-
maxID = ext.value;
|
573
|
-
}
|
574
|
-
return false;
|
575
|
-
});
|
576
|
-
|
577
|
-
if (!ddFound) {
|
578
|
-
media.ext?.push({
|
579
|
-
value: maxID + 1,
|
580
|
-
uri: ddExtensionURI,
|
581
|
-
});
|
582
|
-
}
|
583
|
-
}
|
584
|
-
|
585
598
|
function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
586
599
|
stereoMids: string[];
|
587
600
|
nackMids: string[];
|
package/src/room/Room.ts
CHANGED
@@ -1414,6 +1414,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1414
1414
|
participant.unpublishTrack(publication.trackSid, true);
|
1415
1415
|
});
|
1416
1416
|
this.emit(RoomEvent.ParticipantDisconnected, participant);
|
1417
|
+
this.localParticipant?.handleParticipantDisconnected(participant.identity);
|
1417
1418
|
}
|
1418
1419
|
|
1419
1420
|
// updates are sent only when there's a change to speaker ordering
|
@@ -0,0 +1,304 @@
|
|
1
|
+
import { DataPacket, DataPacket_Kind } from '@livekit/protocol';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
import type { InternalRoomOptions } from '../../options';
|
4
|
+
import type RTCEngine from '../RTCEngine';
|
5
|
+
import { RpcError } from '../rpc';
|
6
|
+
import LocalParticipant from './LocalParticipant';
|
7
|
+
import { ParticipantKind } from './Participant';
|
8
|
+
import RemoteParticipant from './RemoteParticipant';
|
9
|
+
|
10
|
+
describe('LocalParticipant', () => {
|
11
|
+
describe('registerRpcMethod', () => {
|
12
|
+
let localParticipant: LocalParticipant;
|
13
|
+
let mockEngine: RTCEngine;
|
14
|
+
let mockRoomOptions: InternalRoomOptions;
|
15
|
+
let mockSendDataPacket: ReturnType<typeof vi.fn>;
|
16
|
+
|
17
|
+
beforeEach(() => {
|
18
|
+
mockSendDataPacket = vi.fn();
|
19
|
+
mockEngine = {
|
20
|
+
client: {
|
21
|
+
sendUpdateLocalMetadata: vi.fn(),
|
22
|
+
},
|
23
|
+
on: vi.fn().mockReturnThis(),
|
24
|
+
sendDataPacket: mockSendDataPacket,
|
25
|
+
} as unknown as RTCEngine;
|
26
|
+
|
27
|
+
mockRoomOptions = {} as InternalRoomOptions;
|
28
|
+
|
29
|
+
localParticipant = new LocalParticipant(
|
30
|
+
'test-sid',
|
31
|
+
'test-identity',
|
32
|
+
mockEngine,
|
33
|
+
mockRoomOptions,
|
34
|
+
);
|
35
|
+
});
|
36
|
+
|
37
|
+
it('should register an RPC method handler', async () => {
|
38
|
+
const methodName = 'testMethod';
|
39
|
+
const handler = vi.fn().mockResolvedValue('test response');
|
40
|
+
|
41
|
+
localParticipant.registerRpcMethod(methodName, handler);
|
42
|
+
|
43
|
+
const mockCaller = new RemoteParticipant(
|
44
|
+
{} as any,
|
45
|
+
'remote-sid',
|
46
|
+
'remote-identity',
|
47
|
+
'Remote Participant',
|
48
|
+
'',
|
49
|
+
undefined,
|
50
|
+
ParticipantKind.STANDARD,
|
51
|
+
);
|
52
|
+
|
53
|
+
await localParticipant.handleIncomingRpcRequest(
|
54
|
+
mockCaller.identity,
|
55
|
+
'test-request-id',
|
56
|
+
methodName,
|
57
|
+
'test payload',
|
58
|
+
5000,
|
59
|
+
1,
|
60
|
+
);
|
61
|
+
|
62
|
+
expect(handler).toHaveBeenCalledWith({
|
63
|
+
requestId: 'test-request-id',
|
64
|
+
callerIdentity: mockCaller.identity,
|
65
|
+
payload: 'test payload',
|
66
|
+
responseTimeout: 5000,
|
67
|
+
});
|
68
|
+
|
69
|
+
// Check if sendDataPacket was called twice (once for ACK and once for response)
|
70
|
+
expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
|
71
|
+
|
72
|
+
// Check if the first call was for ACK
|
73
|
+
expect(mockSendDataPacket.mock.calls[0][0].value.case).toBe('rpcAck');
|
74
|
+
expect(mockSendDataPacket.mock.calls[0][1]).toBe(DataPacket_Kind.RELIABLE);
|
75
|
+
|
76
|
+
// Check if the second call was for response
|
77
|
+
expect(mockSendDataPacket.mock.calls[1][0].value.case).toBe('rpcResponse');
|
78
|
+
expect(mockSendDataPacket.mock.calls[1][1]).toBe(DataPacket_Kind.RELIABLE);
|
79
|
+
});
|
80
|
+
|
81
|
+
it('should catch and transform unhandled errors in the RPC method handler', async () => {
|
82
|
+
const methodName = 'errorMethod';
|
83
|
+
const errorMessage = 'Test error';
|
84
|
+
const handler = vi.fn().mockRejectedValue(new Error(errorMessage));
|
85
|
+
|
86
|
+
localParticipant.registerRpcMethod(methodName, handler);
|
87
|
+
|
88
|
+
const mockCaller = new RemoteParticipant(
|
89
|
+
{} as any,
|
90
|
+
'remote-sid',
|
91
|
+
'remote-identity',
|
92
|
+
'Remote Participant',
|
93
|
+
'',
|
94
|
+
undefined,
|
95
|
+
ParticipantKind.STANDARD,
|
96
|
+
);
|
97
|
+
|
98
|
+
await localParticipant.handleIncomingRpcRequest(
|
99
|
+
mockCaller.identity,
|
100
|
+
'test-error-request-id',
|
101
|
+
methodName,
|
102
|
+
'test payload',
|
103
|
+
5000,
|
104
|
+
1,
|
105
|
+
);
|
106
|
+
|
107
|
+
expect(handler).toHaveBeenCalledWith({
|
108
|
+
requestId: 'test-error-request-id',
|
109
|
+
callerIdentity: mockCaller.identity,
|
110
|
+
payload: 'test payload',
|
111
|
+
responseTimeout: 5000,
|
112
|
+
});
|
113
|
+
|
114
|
+
// Check if sendDataPacket was called twice (once for ACK and once for error response)
|
115
|
+
expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
|
116
|
+
|
117
|
+
// Check if the second call was for error response
|
118
|
+
const errorResponse = mockSendDataPacket.mock.calls[1][0].value.value.value.value;
|
119
|
+
expect(errorResponse.code).toBe(RpcError.ErrorCode.APPLICATION_ERROR);
|
120
|
+
});
|
121
|
+
|
122
|
+
it('should pass through RpcError thrown by the RPC method handler', async () => {
|
123
|
+
const methodName = 'rpcErrorMethod';
|
124
|
+
const errorCode = 101;
|
125
|
+
const errorMessage = 'some-error-message';
|
126
|
+
const handler = vi.fn().mockRejectedValue(new RpcError(errorCode, errorMessage));
|
127
|
+
|
128
|
+
localParticipant.registerRpcMethod(methodName, handler);
|
129
|
+
|
130
|
+
const mockCaller = new RemoteParticipant(
|
131
|
+
{} as any,
|
132
|
+
'remote-sid',
|
133
|
+
'remote-identity',
|
134
|
+
'Remote Participant',
|
135
|
+
'',
|
136
|
+
undefined,
|
137
|
+
ParticipantKind.STANDARD,
|
138
|
+
);
|
139
|
+
|
140
|
+
await localParticipant.handleIncomingRpcRequest(
|
141
|
+
mockCaller.identity,
|
142
|
+
'test-rpc-error-request-id',
|
143
|
+
methodName,
|
144
|
+
'test payload',
|
145
|
+
5000,
|
146
|
+
1,
|
147
|
+
);
|
148
|
+
|
149
|
+
expect(handler).toHaveBeenCalledWith({
|
150
|
+
requestId: 'test-rpc-error-request-id',
|
151
|
+
callerIdentity: mockCaller.identity,
|
152
|
+
payload: 'test payload',
|
153
|
+
responseTimeout: 5000,
|
154
|
+
});
|
155
|
+
|
156
|
+
// Check if sendDataPacket was called twice (once for ACK and once for error response)
|
157
|
+
expect(mockSendDataPacket).toHaveBeenCalledTimes(2);
|
158
|
+
|
159
|
+
// Check if the second call was for error response
|
160
|
+
const errorResponse = mockSendDataPacket.mock.calls[1][0].value.value.value.value;
|
161
|
+
expect(errorResponse.code).toBe(errorCode);
|
162
|
+
expect(errorResponse.message).toBe(errorMessage);
|
163
|
+
});
|
164
|
+
});
|
165
|
+
|
166
|
+
describe('performRpc', () => {
|
167
|
+
let localParticipant: LocalParticipant;
|
168
|
+
let mockRemoteParticipant: RemoteParticipant;
|
169
|
+
let mockEngine: RTCEngine;
|
170
|
+
let mockRoomOptions: InternalRoomOptions;
|
171
|
+
let mockSendDataPacket: ReturnType<typeof vi.fn>;
|
172
|
+
|
173
|
+
beforeEach(() => {
|
174
|
+
mockSendDataPacket = vi.fn();
|
175
|
+
mockEngine = {
|
176
|
+
client: {
|
177
|
+
sendUpdateLocalMetadata: vi.fn(),
|
178
|
+
},
|
179
|
+
on: vi.fn().mockReturnThis(),
|
180
|
+
sendDataPacket: mockSendDataPacket,
|
181
|
+
} as unknown as RTCEngine;
|
182
|
+
|
183
|
+
mockRoomOptions = {} as InternalRoomOptions;
|
184
|
+
|
185
|
+
localParticipant = new LocalParticipant(
|
186
|
+
'local-sid',
|
187
|
+
'local-identity',
|
188
|
+
mockEngine,
|
189
|
+
mockRoomOptions,
|
190
|
+
);
|
191
|
+
|
192
|
+
mockRemoteParticipant = new RemoteParticipant(
|
193
|
+
{} as any,
|
194
|
+
'remote-sid',
|
195
|
+
'remote-identity',
|
196
|
+
'Remote Participant',
|
197
|
+
'',
|
198
|
+
undefined,
|
199
|
+
ParticipantKind.STANDARD,
|
200
|
+
);
|
201
|
+
});
|
202
|
+
|
203
|
+
it('should send RPC request and receive successful response', async () => {
|
204
|
+
const method = 'testMethod';
|
205
|
+
const payload = 'testPayload';
|
206
|
+
const responsePayload = 'responsePayload';
|
207
|
+
|
208
|
+
mockSendDataPacket.mockImplementationOnce((packet: DataPacket) => {
|
209
|
+
const requestId = packet.value.value.id;
|
210
|
+
setTimeout(() => {
|
211
|
+
localParticipant.handleIncomingRpcAck(requestId);
|
212
|
+
setTimeout(() => {
|
213
|
+
localParticipant.handleIncomingRpcResponse(requestId, responsePayload, null);
|
214
|
+
}, 10);
|
215
|
+
}, 10);
|
216
|
+
});
|
217
|
+
|
218
|
+
const result = await localParticipant.performRpc({
|
219
|
+
destinationIdentity: mockRemoteParticipant.identity,
|
220
|
+
method,
|
221
|
+
payload,
|
222
|
+
});
|
223
|
+
|
224
|
+
expect(mockSendDataPacket).toHaveBeenCalledTimes(1);
|
225
|
+
expect(result).toBe(responsePayload);
|
226
|
+
});
|
227
|
+
|
228
|
+
it('should handle RPC request timeout', async () => {
|
229
|
+
const method = 'timeoutMethod';
|
230
|
+
const payload = 'timeoutPayload';
|
231
|
+
|
232
|
+
const timeout = 50;
|
233
|
+
|
234
|
+
const resultPromise = localParticipant.performRpc({
|
235
|
+
destinationIdentity: mockRemoteParticipant.identity,
|
236
|
+
method,
|
237
|
+
payload,
|
238
|
+
responseTimeout: timeout,
|
239
|
+
});
|
240
|
+
|
241
|
+
mockSendDataPacket.mockImplementationOnce(() => {
|
242
|
+
return new Promise((resolve) => {
|
243
|
+
setTimeout(resolve, timeout + 10);
|
244
|
+
});
|
245
|
+
});
|
246
|
+
|
247
|
+
const startTime = Date.now();
|
248
|
+
|
249
|
+
await expect(resultPromise).rejects.toThrow('Response timeout');
|
250
|
+
|
251
|
+
const elapsedTime = Date.now() - startTime;
|
252
|
+
expect(elapsedTime).toBeGreaterThanOrEqual(timeout);
|
253
|
+
expect(elapsedTime).toBeLessThan(timeout + 50); // Allow some margin for test execution
|
254
|
+
|
255
|
+
expect(mockSendDataPacket).toHaveBeenCalledTimes(1);
|
256
|
+
});
|
257
|
+
|
258
|
+
it('should handle RPC error response', async () => {
|
259
|
+
const method = 'errorMethod';
|
260
|
+
const payload = 'errorPayload';
|
261
|
+
const errorCode = 101;
|
262
|
+
const errorMessage = 'Test error message';
|
263
|
+
|
264
|
+
mockSendDataPacket.mockImplementationOnce((packet: DataPacket) => {
|
265
|
+
const requestId = packet.value.value.id;
|
266
|
+
setTimeout(() => {
|
267
|
+
localParticipant.handleIncomingRpcAck(requestId);
|
268
|
+
localParticipant.handleIncomingRpcResponse(
|
269
|
+
requestId,
|
270
|
+
null,
|
271
|
+
new RpcError(errorCode, errorMessage),
|
272
|
+
);
|
273
|
+
}, 10);
|
274
|
+
});
|
275
|
+
|
276
|
+
await expect(
|
277
|
+
localParticipant.performRpc({
|
278
|
+
destinationIdentity: mockRemoteParticipant.identity,
|
279
|
+
method,
|
280
|
+
payload,
|
281
|
+
}),
|
282
|
+
).rejects.toThrow(errorMessage);
|
283
|
+
});
|
284
|
+
|
285
|
+
it('should handle participant disconnection during RPC request', async () => {
|
286
|
+
const method = 'disconnectMethod';
|
287
|
+
const payload = 'disconnectPayload';
|
288
|
+
|
289
|
+
mockSendDataPacket.mockImplementationOnce(() => Promise.resolve());
|
290
|
+
|
291
|
+
const resultPromise = localParticipant.performRpc({
|
292
|
+
destinationIdentity: mockRemoteParticipant.identity,
|
293
|
+
method,
|
294
|
+
payload,
|
295
|
+
});
|
296
|
+
|
297
|
+
// Simulate a small delay before disconnection
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
299
|
+
localParticipant.handleParticipantDisconnected(mockRemoteParticipant.identity);
|
300
|
+
|
301
|
+
await expect(resultPromise).rejects.toThrow('Recipient disconnected');
|
302
|
+
});
|
303
|
+
});
|
304
|
+
});
|