livekit-client 2.5.10 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });