livekit-client 2.5.9 → 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.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +500 -5114
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +519 -127
- 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 +3 -1
- 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/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/LocalTrack.d.ts +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.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/src/room/utils.d.ts +0 -10
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +4 -1
- 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/LocalTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/utils.d.ts +2 -2
- package/dist/ts4.2/src/room/utils.d.ts +0 -10
- package/package.json +3 -2
- package/src/api/SignalClient.ts +2 -1
- package/src/index.ts +3 -1
- package/src/room/PCTransport.ts +42 -29
- package/src/room/PCTransportManager.ts +2 -1
- package/src/room/RTCEngine.ts +3 -1
- package/src/room/Room.ts +2 -1
- 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/LocalTrack.ts +2 -1
- package/src/room/track/LocalVideoTrack.ts +2 -1
- package/src/room/track/options.ts +5 -5
- package/src/room/track/utils.ts +1 -6
- package/src/room/utils.ts +0 -38
- package/src/utils/AsyncQueue.test.ts +2 -2
- package/src/utils/AsyncQueue.ts +1 -1
@@ -0,0 +1,96 @@
|
|
1
|
+
import { RpcError as RpcError_Proto } from '@livekit/protocol';
|
2
|
+
/** Parameters for initiating an RPC call */
|
3
|
+
export interface PerformRpcParams {
|
4
|
+
/** The `identity` of the destination participant */
|
5
|
+
destinationIdentity: string;
|
6
|
+
/** The method name to call */
|
7
|
+
method: string;
|
8
|
+
/** The method payload */
|
9
|
+
payload: string;
|
10
|
+
/** Timeout for receiving a response after initial connection (milliseconds). Default: 10000 */
|
11
|
+
responseTimeout?: number;
|
12
|
+
}
|
13
|
+
/**
|
14
|
+
* Data passed to method handler for incoming RPC invocations
|
15
|
+
*/
|
16
|
+
export interface RpcInvocationData {
|
17
|
+
/**
|
18
|
+
* The unique request ID. Will match at both sides of the call, useful for debugging or logging.
|
19
|
+
*/
|
20
|
+
requestId: string;
|
21
|
+
/**
|
22
|
+
* The unique participant identity of the caller.
|
23
|
+
*/
|
24
|
+
callerIdentity: string;
|
25
|
+
/**
|
26
|
+
* The payload of the request. User-definable format, typically JSON.
|
27
|
+
*/
|
28
|
+
payload: string;
|
29
|
+
/**
|
30
|
+
* The maximum time the caller will wait for a response.
|
31
|
+
*/
|
32
|
+
responseTimeout: number;
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Specialized error handling for RPC methods.
|
36
|
+
*
|
37
|
+
* Instances of this type, when thrown in a method handler, will have their `message`
|
38
|
+
* serialized and sent across the wire. The sender will receive an equivalent error on the other side.
|
39
|
+
*
|
40
|
+
* Built-in types are included but developers may use any string, with a max length of 256 bytes.
|
41
|
+
*/
|
42
|
+
export declare class RpcError extends Error {
|
43
|
+
static MAX_MESSAGE_BYTES: number;
|
44
|
+
static MAX_DATA_BYTES: number;
|
45
|
+
code: number;
|
46
|
+
data?: string;
|
47
|
+
/**
|
48
|
+
* Creates an error object with the given code and message, plus an optional data payload.
|
49
|
+
*
|
50
|
+
* If thrown in an RPC method handler, the error will be sent back to the caller.
|
51
|
+
*
|
52
|
+
* Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
|
53
|
+
*/
|
54
|
+
constructor(code: number, message: string, data?: string);
|
55
|
+
/**
|
56
|
+
* @internal
|
57
|
+
*/
|
58
|
+
static fromProto(proto: RpcError_Proto): RpcError;
|
59
|
+
/**
|
60
|
+
* @internal
|
61
|
+
*/
|
62
|
+
toProto(): RpcError_Proto;
|
63
|
+
static ErrorCode: {
|
64
|
+
readonly APPLICATION_ERROR: 1500;
|
65
|
+
readonly CONNECTION_TIMEOUT: 1501;
|
66
|
+
readonly RESPONSE_TIMEOUT: 1502;
|
67
|
+
readonly RECIPIENT_DISCONNECTED: 1503;
|
68
|
+
readonly RESPONSE_PAYLOAD_TOO_LARGE: 1504;
|
69
|
+
readonly SEND_FAILED: 1505;
|
70
|
+
readonly UNSUPPORTED_METHOD: 1400;
|
71
|
+
readonly RECIPIENT_NOT_FOUND: 1401;
|
72
|
+
readonly REQUEST_PAYLOAD_TOO_LARGE: 1402;
|
73
|
+
readonly UNSUPPORTED_SERVER: 1403;
|
74
|
+
readonly UNSUPPORTED_VERSION: 1404;
|
75
|
+
};
|
76
|
+
/**
|
77
|
+
* @internal
|
78
|
+
*/
|
79
|
+
static ErrorMessage: Record<keyof typeof RpcError.ErrorCode, string>;
|
80
|
+
/**
|
81
|
+
* Creates an error object from the code, with an auto-populated message.
|
82
|
+
*
|
83
|
+
* @internal
|
84
|
+
*/
|
85
|
+
static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError;
|
86
|
+
}
|
87
|
+
export declare const MAX_PAYLOAD_BYTES = 15360;
|
88
|
+
/**
|
89
|
+
* @internal
|
90
|
+
*/
|
91
|
+
export declare function byteLength(str: string): number;
|
92
|
+
/**
|
93
|
+
* @internal
|
94
|
+
*/
|
95
|
+
export declare function truncateBytes(str: string, maxBytes: number): string;
|
96
|
+
//# sourceMappingURL=rpc.d.ts.map
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { TrackPublishedResponse } from '@livekit/protocol';
|
2
2
|
import { Track } from './Track';
|
3
3
|
import type { TrackPublication } from './TrackPublication';
|
4
|
-
import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions } from './options';
|
4
|
+
import type { AudioCaptureOptions, CreateLocalTracksOptions, ScreenShareCaptureOptions, VideoCaptureOptions, VideoCodec } from './options';
|
5
5
|
import type { AudioTrack } from './types';
|
6
6
|
export declare function mergeDefaultOptions(options?: CreateLocalTracksOptions, audioDefaults?: AudioCaptureOptions, videoDefaults?: VideoCaptureOptions): CreateLocalTracksOptions;
|
7
7
|
export declare function constraintsForOptions(options: CreateLocalTracksOptions): MediaStreamConstraints;
|
@@ -26,7 +26,7 @@ export declare function sourceToKind(source: Track.Source): MediaDeviceKind | un
|
|
26
26
|
* @internal
|
27
27
|
*/
|
28
28
|
export declare function screenCaptureToDisplayMediaStreamOptions(options: ScreenShareCaptureOptions): DisplayMediaStreamOptions;
|
29
|
-
export declare function mimeTypeToVideoCodecString(mimeType: string):
|
29
|
+
export declare function mimeTypeToVideoCodecString(mimeType: string): VideoCodec;
|
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;
|
@@ -80,16 +80,6 @@ export declare function createAudioAnalyser(track: LocalAudioTrack | RemoteAudio
|
|
80
80
|
analyser: AnalyserNode;
|
81
81
|
cleanup: () => Promise<void>;
|
82
82
|
};
|
83
|
-
/**
|
84
|
-
* @internal
|
85
|
-
*/
|
86
|
-
export declare class Mutex {
|
87
|
-
private _locking;
|
88
|
-
private _locks;
|
89
|
-
constructor();
|
90
|
-
isLocked(): boolean;
|
91
|
-
lock(): Promise<() => void>;
|
92
|
-
}
|
93
83
|
export declare function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec;
|
94
84
|
export declare function unwrapConstraint(constraint: ConstrainDOMString): string;
|
95
85
|
export declare function unwrapConstraint(constraint: ConstrainULong): number;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "livekit-client",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.6.0",
|
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,6 +36,7 @@
|
|
36
36
|
"author": "David Zhao <david@davidzhao.com>",
|
37
37
|
"license": "Apache-2.0",
|
38
38
|
"dependencies": {
|
39
|
+
"@livekit/mutex": "1.0.0",
|
39
40
|
"@livekit/protocol": "1.24.0",
|
40
41
|
"events": "^3.3.0",
|
41
42
|
"loglevel": "^1.8.0",
|
@@ -87,7 +88,7 @@
|
|
87
88
|
"build": "rollup --config --bundleConfigAsCjs && rollup --config rollup.config.worker.js --bundleConfigAsCjs && pnpm downlevel-dts",
|
88
89
|
"build:watch": "rollup --watch --config --bundleConfigAsCjs",
|
89
90
|
"build:worker:watch": "rollup --watch --config rollup.config.worker.js --bundleConfigAsCjs",
|
90
|
-
"build-docs": "typedoc",
|
91
|
+
"build-docs": "typedoc && mkdir -p docs/assets/github && cp .github/*.png docs/assets/github/ && find docs -name '*.html' -type f -exec sed -i.bak 's|=\"/.github/|=\"assets/github/|g' {} + && find docs -name '*.bak' -delete",
|
91
92
|
"proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto",
|
92
93
|
"examples:demo": "vite examples/demo -c vite.config.mjs",
|
93
94
|
"lint": "eslint src",
|
package/src/api/SignalClient.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import {
|
2
3
|
AddTrackRequest,
|
3
4
|
AudioTrackFeature,
|
@@ -42,7 +43,7 @@ import log, { LoggerNames, getLogger } from '../logger';
|
|
42
43
|
import { ConnectionError, ConnectionErrorReason } from '../room/errors';
|
43
44
|
import CriticalTimers from '../room/timers';
|
44
45
|
import type { LoggerOptions } from '../room/types';
|
45
|
-
import {
|
46
|
+
import { getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../room/utils';
|
46
47
|
import { AsyncQueue } from '../utils/AsyncQueue';
|
47
48
|
|
48
49
|
// internal options
|
package/src/index.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import { DataPacket_Kind, DisconnectReason, SubscriptionError } from '@livekit/protocol';
|
2
3
|
import { LogLevel, LoggerNames, getLogger, setLogExtension, setLogLevel } from './logger';
|
3
4
|
import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
|
@@ -26,7 +27,6 @@ import { TrackPublication } from './room/track/TrackPublication';
|
|
26
27
|
import type { LiveKitReactNativeInfo } from './room/types';
|
27
28
|
import type { AudioAnalyserOptions } from './room/utils';
|
28
29
|
import {
|
29
|
-
Mutex,
|
30
30
|
compareVersions,
|
31
31
|
createAudioAnalyser,
|
32
32
|
getEmptyAudioStreamTrack,
|
@@ -39,6 +39,8 @@ import {
|
|
39
39
|
} from './room/utils';
|
40
40
|
import { getBrowser } from './utils/browserParser';
|
41
41
|
|
42
|
+
export { RpcError, type RpcInvocationData, type PerformRpcParams } from './room/rpc';
|
43
|
+
|
42
44
|
export * from './connectionHelper/ConnectionCheck';
|
43
45
|
export * from './connectionHelper/checks/Checker';
|
44
46
|
export * from './e2ee';
|
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[];
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import { SignalTarget } from '@livekit/protocol';
|
2
3
|
import log, { LoggerNames, getLogger } from '../logger';
|
3
4
|
import PCTransport, { PCEvents } from './PCTransport';
|
@@ -5,7 +6,7 @@ import { roomConnectOptionDefaults } from './defaults';
|
|
5
6
|
import { ConnectionError, ConnectionErrorReason } from './errors';
|
6
7
|
import CriticalTimers from './timers';
|
7
8
|
import type { LoggerOptions } from './types';
|
8
|
-
import {
|
9
|
+
import { sleep } from './utils';
|
9
10
|
|
10
11
|
export enum PCTransportState {
|
11
12
|
NEW,
|
package/src/room/RTCEngine.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import {
|
2
3
|
type AddTrackRequest,
|
3
4
|
ClientConfigSetting,
|
@@ -63,7 +64,7 @@ import type { Track } from './track/Track';
|
|
63
64
|
import type { TrackPublishOptions, VideoCodec } from './track/options';
|
64
65
|
import { getTrackPublicationInfo } from './track/utils';
|
65
66
|
import type { LoggerOptions } from './types';
|
66
|
-
import {
|
67
|
+
import { isVideoCodec, isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';
|
67
68
|
|
68
69
|
const lossyDataChannel = '_lossy';
|
69
70
|
const reliableDataChannel = '_reliable';
|
@@ -262,6 +263,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
262
263
|
}
|
263
264
|
try {
|
264
265
|
this._isClosed = true;
|
266
|
+
this.joinAttempts = 0;
|
265
267
|
this.emit(EngineEvent.Closing);
|
266
268
|
this.removeAllListeners();
|
267
269
|
this.deregisterOnLineListener();
|
package/src/room/Room.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Mutex } from '@livekit/mutex';
|
1
2
|
import {
|
2
3
|
ChatMessage as ChatMessageModel,
|
3
4
|
ConnectionQualityUpdate,
|
@@ -77,7 +78,6 @@ import type {
|
|
77
78
|
} from './types';
|
78
79
|
import {
|
79
80
|
Future,
|
80
|
-
Mutex,
|
81
81
|
createDummyVideoStreamTrack,
|
82
82
|
extractChatMessage,
|
83
83
|
extractTranscriptionSegments,
|
@@ -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
|
+
});
|