livekit-client 2.5.10 → 2.6.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.
- package/README.md +54 -0
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +431 -45
- 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.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/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -2
- package/dist/src/room/errors.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/RemoteAudioTrack.d.ts +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- 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/errors.d.ts +2 -2
- 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/RemoteAudioTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
- package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
- package/package.json +2 -2
- package/src/api/SignalClient.ts +19 -3
- package/src/index.ts +2 -0
- package/src/room/PCTransport.ts +42 -29
- package/src/room/PCTransportManager.ts +6 -1
- package/src/room/RTCEngine.ts +13 -3
- package/src/room/RegionUrlProvider.ts +3 -1
- package/src/room/Room.ts +9 -3
- package/src/room/errors.ts +2 -2
- 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/RemoteAudioTrack.ts +1 -1
- package/src/room/track/RemoteVideoTrack.ts +1 -1
- package/src/room/track/utils.ts +1 -6
@@ -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
|
+
});
|
@@ -9,6 +9,9 @@ import {
|
|
9
9
|
ParticipantPermission,
|
10
10
|
RequestResponse,
|
11
11
|
RequestResponse_Reason,
|
12
|
+
RpcAck,
|
13
|
+
RpcRequest,
|
14
|
+
RpcResponse,
|
12
15
|
SimulcastCodec,
|
13
16
|
SipDTMF,
|
14
17
|
SubscribedQualityUpdate,
|
@@ -29,6 +32,13 @@ import {
|
|
29
32
|
UnexpectedConnectionState,
|
30
33
|
} from '../errors';
|
31
34
|
import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
|
35
|
+
import {
|
36
|
+
MAX_PAYLOAD_BYTES,
|
37
|
+
type PerformRpcParams,
|
38
|
+
RpcError,
|
39
|
+
type RpcInvocationData,
|
40
|
+
byteLength,
|
41
|
+
} from '../rpc';
|
32
42
|
import LocalAudioTrack from '../track/LocalAudioTrack';
|
33
43
|
import LocalTrack from '../track/LocalTrack';
|
34
44
|
import LocalTrackPublication from '../track/LocalTrackPublication';
|
@@ -54,6 +64,7 @@ import {
|
|
54
64
|
import type { ChatMessage, DataPublishOptions } from '../types';
|
55
65
|
import {
|
56
66
|
Future,
|
67
|
+
compareVersions,
|
57
68
|
isE2EESimulcastSupported,
|
58
69
|
isFireFox,
|
59
70
|
isSVCCodec,
|
@@ -119,6 +130,18 @@ export default class LocalParticipant extends Participant {
|
|
119
130
|
|
120
131
|
private enabledPublishVideoCodecs: Codec[] = [];
|
121
132
|
|
133
|
+
private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
|
134
|
+
|
135
|
+
private pendingAcks = new Map<string, { resolve: () => void; participantIdentity: string }>();
|
136
|
+
|
137
|
+
private pendingResponses = new Map<
|
138
|
+
string,
|
139
|
+
{
|
140
|
+
resolve: (payload: string | null, error: RpcError | null) => void;
|
141
|
+
participantIdentity: string;
|
142
|
+
}
|
143
|
+
>();
|
144
|
+
|
122
145
|
/** @internal */
|
123
146
|
constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
|
124
147
|
super(sid, identity, undefined, undefined, {
|
@@ -187,7 +210,8 @@ export default class LocalParticipant extends Participant {
|
|
187
210
|
.on(EngineEvent.LocalTrackUnpublished, this.handleLocalTrackUnpublished)
|
188
211
|
.on(EngineEvent.SubscribedQualityUpdate, this.handleSubscribedQualityUpdate)
|
189
212
|
.on(EngineEvent.Disconnected, this.handleDisconnected)
|
190
|
-
.on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse)
|
213
|
+
.on(EngineEvent.SignalRequestResponse, this.handleSignalRequestResponse)
|
214
|
+
.on(EngineEvent.DataPacketReceived, this.handleDataPacket);
|
191
215
|
}
|
192
216
|
|
193
217
|
private handleReconnecting = () => {
|
@@ -221,6 +245,38 @@ export default class LocalParticipant extends Participant {
|
|
221
245
|
}
|
222
246
|
};
|
223
247
|
|
248
|
+
private handleDataPacket = (packet: DataPacket) => {
|
249
|
+
switch (packet.value.case) {
|
250
|
+
case 'rpcRequest':
|
251
|
+
let rpcRequest = packet.value.value as RpcRequest;
|
252
|
+
this.handleIncomingRpcRequest(
|
253
|
+
packet.participantIdentity,
|
254
|
+
rpcRequest.id,
|
255
|
+
rpcRequest.method,
|
256
|
+
rpcRequest.payload,
|
257
|
+
rpcRequest.responseTimeoutMs,
|
258
|
+
rpcRequest.version,
|
259
|
+
);
|
260
|
+
break;
|
261
|
+
case 'rpcResponse':
|
262
|
+
let rpcResponse = packet.value.value as RpcResponse;
|
263
|
+
let payload: string | null = null;
|
264
|
+
let error: RpcError | null = null;
|
265
|
+
|
266
|
+
if (rpcResponse.value.case === 'payload') {
|
267
|
+
payload = rpcResponse.value.value;
|
268
|
+
} else if (rpcResponse.value.case === 'error') {
|
269
|
+
error = RpcError.fromProto(rpcResponse.value.value);
|
270
|
+
}
|
271
|
+
this.handleIncomingRpcResponse(rpcResponse.requestId, payload, error);
|
272
|
+
break;
|
273
|
+
case 'rpcAck':
|
274
|
+
let rpcAck = packet.value.value as RpcAck;
|
275
|
+
this.handleIncomingRpcAck(rpcAck.requestId);
|
276
|
+
break;
|
277
|
+
}
|
278
|
+
};
|
279
|
+
|
224
280
|
/**
|
225
281
|
* Sets and updates the metadata of the local participant.
|
226
282
|
* Note: this requires `canUpdateOwnMetadata` permission.
|
@@ -1415,6 +1471,121 @@ export default class LocalParticipant extends Participant {
|
|
1415
1471
|
return msg;
|
1416
1472
|
}
|
1417
1473
|
|
1474
|
+
/**
|
1475
|
+
* Initiate an RPC call to a remote participant
|
1476
|
+
* @param params - Parameters for initiating the RPC call, see {@link PerformRpcParams}
|
1477
|
+
* @returns A promise that resolves with the response payload or rejects with an error.
|
1478
|
+
* @throws Error on failure. Details in `message`.
|
1479
|
+
*/
|
1480
|
+
async performRpc({
|
1481
|
+
destinationIdentity,
|
1482
|
+
method,
|
1483
|
+
payload,
|
1484
|
+
responseTimeout = 10000,
|
1485
|
+
}: PerformRpcParams): Promise<string> {
|
1486
|
+
const maxRoundTripLatency = 2000;
|
1487
|
+
|
1488
|
+
return new Promise(async (resolve, reject) => {
|
1489
|
+
if (byteLength(payload) > MAX_PAYLOAD_BYTES) {
|
1490
|
+
reject(RpcError.builtIn('REQUEST_PAYLOAD_TOO_LARGE'));
|
1491
|
+
return;
|
1492
|
+
}
|
1493
|
+
|
1494
|
+
if (
|
1495
|
+
this.engine.latestJoinResponse?.serverInfo?.version &&
|
1496
|
+
compareVersions(this.engine.latestJoinResponse?.serverInfo?.version, '1.8.0') < 0
|
1497
|
+
) {
|
1498
|
+
reject(RpcError.builtIn('UNSUPPORTED_SERVER'));
|
1499
|
+
return;
|
1500
|
+
}
|
1501
|
+
|
1502
|
+
const id = crypto.randomUUID();
|
1503
|
+
await this.publishRpcRequest(
|
1504
|
+
destinationIdentity,
|
1505
|
+
id,
|
1506
|
+
method,
|
1507
|
+
payload,
|
1508
|
+
responseTimeout - maxRoundTripLatency,
|
1509
|
+
);
|
1510
|
+
|
1511
|
+
const ackTimeoutId = setTimeout(() => {
|
1512
|
+
this.pendingAcks.delete(id);
|
1513
|
+
reject(RpcError.builtIn('CONNECTION_TIMEOUT'));
|
1514
|
+
this.pendingResponses.delete(id);
|
1515
|
+
clearTimeout(responseTimeoutId);
|
1516
|
+
}, maxRoundTripLatency);
|
1517
|
+
|
1518
|
+
this.pendingAcks.set(id, {
|
1519
|
+
resolve: () => {
|
1520
|
+
clearTimeout(ackTimeoutId);
|
1521
|
+
},
|
1522
|
+
participantIdentity: destinationIdentity,
|
1523
|
+
});
|
1524
|
+
|
1525
|
+
const responseTimeoutId = setTimeout(() => {
|
1526
|
+
this.pendingResponses.delete(id);
|
1527
|
+
reject(RpcError.builtIn('RESPONSE_TIMEOUT'));
|
1528
|
+
}, responseTimeout);
|
1529
|
+
|
1530
|
+
this.pendingResponses.set(id, {
|
1531
|
+
resolve: (responsePayload: string | null, responseError: RpcError | null) => {
|
1532
|
+
clearTimeout(responseTimeoutId);
|
1533
|
+
if (this.pendingAcks.has(id)) {
|
1534
|
+
console.warn('RPC response received before ack', id);
|
1535
|
+
this.pendingAcks.delete(id);
|
1536
|
+
clearTimeout(ackTimeoutId);
|
1537
|
+
}
|
1538
|
+
|
1539
|
+
if (responseError) {
|
1540
|
+
reject(responseError);
|
1541
|
+
} else {
|
1542
|
+
resolve(responsePayload ?? '');
|
1543
|
+
}
|
1544
|
+
},
|
1545
|
+
participantIdentity: destinationIdentity,
|
1546
|
+
});
|
1547
|
+
});
|
1548
|
+
}
|
1549
|
+
|
1550
|
+
/**
|
1551
|
+
* Establishes the participant as a receiver for calls of the specified RPC method.
|
1552
|
+
* Will overwrite any existing callback for the same method.
|
1553
|
+
*
|
1554
|
+
* @param method - The name of the indicated RPC method
|
1555
|
+
* @param handler - Will be invoked when an RPC request for this method is received
|
1556
|
+
* @returns A promise that resolves when the method is successfully registered
|
1557
|
+
*
|
1558
|
+
* @example
|
1559
|
+
* ```typescript
|
1560
|
+
* room.localParticipant?.registerRpcMethod(
|
1561
|
+
* 'greet',
|
1562
|
+
* async (data: RpcInvocationData) => {
|
1563
|
+
* console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
|
1564
|
+
* return `Hello, ${data.callerIdentity}!`;
|
1565
|
+
* }
|
1566
|
+
* );
|
1567
|
+
* ```
|
1568
|
+
*
|
1569
|
+
* The handler should return a Promise that resolves to a string.
|
1570
|
+
* If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
|
1571
|
+
*
|
1572
|
+
* You may throw errors of type `RpcError` with a string `message` in the handler,
|
1573
|
+
* and they will be received on the caller's side with the message intact.
|
1574
|
+
* Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
|
1575
|
+
*/
|
1576
|
+
registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
|
1577
|
+
this.rpcHandlers.set(method, handler);
|
1578
|
+
}
|
1579
|
+
|
1580
|
+
/**
|
1581
|
+
* Unregisters a previously registered RPC method.
|
1582
|
+
*
|
1583
|
+
* @param method - The name of the RPC method to unregister
|
1584
|
+
*/
|
1585
|
+
unregisterRpcMethod(method: string) {
|
1586
|
+
this.rpcHandlers.delete(method);
|
1587
|
+
}
|
1588
|
+
|
1418
1589
|
/**
|
1419
1590
|
* Control who can subscribe to LocalParticipant's published tracks.
|
1420
1591
|
*
|
@@ -1443,6 +1614,174 @@ export default class LocalParticipant extends Participant {
|
|
1443
1614
|
}
|
1444
1615
|
}
|
1445
1616
|
|
1617
|
+
private handleIncomingRpcAck(requestId: string) {
|
1618
|
+
const handler = this.pendingAcks.get(requestId);
|
1619
|
+
if (handler) {
|
1620
|
+
handler.resolve();
|
1621
|
+
this.pendingAcks.delete(requestId);
|
1622
|
+
} else {
|
1623
|
+
console.error('Ack received for unexpected RPC request', requestId);
|
1624
|
+
}
|
1625
|
+
}
|
1626
|
+
|
1627
|
+
private handleIncomingRpcResponse(
|
1628
|
+
requestId: string,
|
1629
|
+
payload: string | null,
|
1630
|
+
error: RpcError | null,
|
1631
|
+
) {
|
1632
|
+
const handler = this.pendingResponses.get(requestId);
|
1633
|
+
if (handler) {
|
1634
|
+
handler.resolve(payload, error);
|
1635
|
+
this.pendingResponses.delete(requestId);
|
1636
|
+
} else {
|
1637
|
+
console.error('Response received for unexpected RPC request', requestId);
|
1638
|
+
}
|
1639
|
+
}
|
1640
|
+
|
1641
|
+
private async handleIncomingRpcRequest(
|
1642
|
+
callerIdentity: string,
|
1643
|
+
requestId: string,
|
1644
|
+
method: string,
|
1645
|
+
payload: string,
|
1646
|
+
responseTimeout: number,
|
1647
|
+
version: number,
|
1648
|
+
) {
|
1649
|
+
await this.publishRpcAck(callerIdentity, requestId);
|
1650
|
+
|
1651
|
+
if (version !== 1) {
|
1652
|
+
await this.publishRpcResponse(
|
1653
|
+
callerIdentity,
|
1654
|
+
requestId,
|
1655
|
+
null,
|
1656
|
+
RpcError.builtIn('UNSUPPORTED_VERSION'),
|
1657
|
+
);
|
1658
|
+
return;
|
1659
|
+
}
|
1660
|
+
|
1661
|
+
const handler = this.rpcHandlers.get(method);
|
1662
|
+
|
1663
|
+
if (!handler) {
|
1664
|
+
await this.publishRpcResponse(
|
1665
|
+
callerIdentity,
|
1666
|
+
requestId,
|
1667
|
+
null,
|
1668
|
+
RpcError.builtIn('UNSUPPORTED_METHOD'),
|
1669
|
+
);
|
1670
|
+
return;
|
1671
|
+
}
|
1672
|
+
|
1673
|
+
let responseError: RpcError | null = null;
|
1674
|
+
let responsePayload: string | null = null;
|
1675
|
+
|
1676
|
+
try {
|
1677
|
+
const response = await handler({
|
1678
|
+
requestId,
|
1679
|
+
callerIdentity,
|
1680
|
+
payload,
|
1681
|
+
responseTimeout,
|
1682
|
+
});
|
1683
|
+
if (byteLength(response) > MAX_PAYLOAD_BYTES) {
|
1684
|
+
responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
|
1685
|
+
console.warn(`RPC Response payload too large for ${method}`);
|
1686
|
+
} else {
|
1687
|
+
responsePayload = response;
|
1688
|
+
}
|
1689
|
+
} catch (error) {
|
1690
|
+
if (error instanceof RpcError) {
|
1691
|
+
responseError = error;
|
1692
|
+
} else {
|
1693
|
+
console.warn(
|
1694
|
+
`Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
|
1695
|
+
error,
|
1696
|
+
);
|
1697
|
+
responseError = RpcError.builtIn('APPLICATION_ERROR');
|
1698
|
+
}
|
1699
|
+
}
|
1700
|
+
await this.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
|
1701
|
+
}
|
1702
|
+
|
1703
|
+
/** @internal */
|
1704
|
+
private async publishRpcRequest(
|
1705
|
+
destinationIdentity: string,
|
1706
|
+
requestId: string,
|
1707
|
+
method: string,
|
1708
|
+
payload: string,
|
1709
|
+
responseTimeout: number,
|
1710
|
+
) {
|
1711
|
+
const packet = new DataPacket({
|
1712
|
+
destinationIdentities: [destinationIdentity],
|
1713
|
+
kind: DataPacket_Kind.RELIABLE,
|
1714
|
+
value: {
|
1715
|
+
case: 'rpcRequest',
|
1716
|
+
value: new RpcRequest({
|
1717
|
+
id: requestId,
|
1718
|
+
method,
|
1719
|
+
payload,
|
1720
|
+
responseTimeoutMs: responseTimeout,
|
1721
|
+
version: 1,
|
1722
|
+
}),
|
1723
|
+
},
|
1724
|
+
});
|
1725
|
+
|
1726
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1727
|
+
}
|
1728
|
+
|
1729
|
+
/** @internal */
|
1730
|
+
private async publishRpcResponse(
|
1731
|
+
destinationIdentity: string,
|
1732
|
+
requestId: string,
|
1733
|
+
payload: string | null,
|
1734
|
+
error: RpcError | null,
|
1735
|
+
) {
|
1736
|
+
const packet = new DataPacket({
|
1737
|
+
destinationIdentities: [destinationIdentity],
|
1738
|
+
kind: DataPacket_Kind.RELIABLE,
|
1739
|
+
value: {
|
1740
|
+
case: 'rpcResponse',
|
1741
|
+
value: new RpcResponse({
|
1742
|
+
requestId,
|
1743
|
+
value: error
|
1744
|
+
? { case: 'error', value: error.toProto() }
|
1745
|
+
: { case: 'payload', value: payload ?? '' },
|
1746
|
+
}),
|
1747
|
+
},
|
1748
|
+
});
|
1749
|
+
|
1750
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1751
|
+
}
|
1752
|
+
|
1753
|
+
/** @internal */
|
1754
|
+
private async publishRpcAck(destinationIdentity: string, requestId: string) {
|
1755
|
+
const packet = new DataPacket({
|
1756
|
+
destinationIdentities: [destinationIdentity],
|
1757
|
+
kind: DataPacket_Kind.RELIABLE,
|
1758
|
+
value: {
|
1759
|
+
case: 'rpcAck',
|
1760
|
+
value: new RpcAck({
|
1761
|
+
requestId,
|
1762
|
+
}),
|
1763
|
+
},
|
1764
|
+
});
|
1765
|
+
|
1766
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1767
|
+
}
|
1768
|
+
|
1769
|
+
/** @internal */
|
1770
|
+
handleParticipantDisconnected(participantIdentity: string) {
|
1771
|
+
for (const [id, { participantIdentity: pendingIdentity }] of this.pendingAcks) {
|
1772
|
+
if (pendingIdentity === participantIdentity) {
|
1773
|
+
this.pendingAcks.delete(id);
|
1774
|
+
}
|
1775
|
+
}
|
1776
|
+
|
1777
|
+
for (const [id, { participantIdentity: pendingIdentity, resolve }] of this.pendingResponses) {
|
1778
|
+
if (pendingIdentity === participantIdentity) {
|
1779
|
+
resolve(null, RpcError.builtIn('RECIPIENT_DISCONNECTED'));
|
1780
|
+
this.pendingResponses.delete(id);
|
1781
|
+
}
|
1782
|
+
}
|
1783
|
+
}
|
1784
|
+
|
1446
1785
|
/** @internal */
|
1447
1786
|
setEnabledPublishCodecs(codecs: Codec[]) {
|
1448
1787
|
this.enabledPublishVideoCodecs = codecs.filter(
|