livekit-client 2.18.10 → 2.19.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.
Files changed (63) hide show
  1. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  2. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  3. package/dist/livekit-client.esm.mjs +749 -438
  4. package/dist/livekit-client.esm.mjs.map +1 -1
  5. package/dist/livekit-client.pt.worker.js.map +1 -1
  6. package/dist/livekit-client.pt.worker.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/room/RTCEngine.d.ts +0 -3
  10. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  11. package/dist/src/room/Room.d.ts +4 -2
  12. package/dist/src/room/Room.d.ts.map +1 -1
  13. package/dist/src/room/participant/LocalParticipant.d.ts +5 -13
  14. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  15. package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
  16. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  17. package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
  18. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
  19. package/dist/src/room/rpc/client/events.d.ts +8 -0
  20. package/dist/src/room/rpc/client/events.d.ts.map +1 -0
  21. package/dist/src/room/rpc/index.d.ts +6 -0
  22. package/dist/src/room/rpc/index.d.ts.map +1 -0
  23. package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
  24. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
  25. package/dist/src/room/rpc/server/events.d.ts +8 -0
  26. package/dist/src/room/rpc/server/events.d.ts.map +1 -0
  27. package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  28. package/dist/src/room/rpc/utils.d.ts.map +1 -0
  29. package/dist/src/room/utils.d.ts +1 -0
  30. package/dist/src/room/utils.d.ts.map +1 -1
  31. package/dist/src/version.d.ts +8 -0
  32. package/dist/src/version.d.ts.map +1 -1
  33. package/dist/ts4.2/room/RTCEngine.d.ts +0 -3
  34. package/dist/ts4.2/room/Room.d.ts +4 -2
  35. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +5 -13
  36. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
  37. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
  38. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
  39. package/dist/ts4.2/room/rpc/index.d.ts +7 -0
  40. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
  41. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
  42. package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  43. package/dist/ts4.2/room/utils.d.ts +1 -0
  44. package/dist/ts4.2/version.d.ts +8 -0
  45. package/package.json +4 -2
  46. package/src/api/SignalClient.ts +1 -0
  47. package/src/room/RTCEngine.ts +4 -30
  48. package/src/room/Room.test.ts +99 -1
  49. package/src/room/Room.ts +107 -88
  50. package/src/room/participant/LocalParticipant.ts +16 -180
  51. package/src/room/participant/RemoteParticipant.ts +9 -0
  52. package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
  53. package/src/room/rpc/client/RpcClientManager.ts +269 -0
  54. package/src/room/rpc/client/events.ts +9 -0
  55. package/src/room/rpc/index.ts +14 -0
  56. package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
  57. package/src/room/rpc/server/RpcServerManager.ts +293 -0
  58. package/src/room/rpc/server/events.ts +9 -0
  59. package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
  60. package/src/room/utils.ts +7 -1
  61. package/src/version.ts +10 -0
  62. package/dist/src/room/rpc.d.ts.map +0 -1
  63. package/src/room/rpc.test.ts +0 -301
@@ -8,7 +8,7 @@ import LocalDataTrack from '../data-track/LocalDataTrack';
8
8
  import type OutgoingDataTrackManager from '../data-track/outgoing/OutgoingDataTrackManager';
9
9
  import type { DataTrackOptions } from '../data-track/outgoing/types';
10
10
  import type { PerformRpcParams, RpcInvocationData } from '../rpc';
11
- import { RpcError } from '../rpc';
11
+ import { RpcClientManager, RpcError, RpcServerManager } from '../rpc';
12
12
  import LocalTrack from '../track/LocalTrack';
13
13
  import LocalTrackPublication from '../track/LocalTrackPublication';
14
14
  import { Track } from '../track/Track';
@@ -40,15 +40,14 @@ export default class LocalParticipant extends Participant {
40
40
  private signalConnectedFuture?;
41
41
  private activeAgentFuture?;
42
42
  private firstActiveAgent?;
43
- private rpcHandlers;
44
43
  private roomOutgoingDataStreamManager;
45
44
  private roomOutgoingDataTrackManager;
45
+ private rpcClientManager;
46
+ private rpcServerManager;
46
47
  private pendingSignalRequests;
47
48
  private enabledPublishVideoCodecs;
48
- private pendingAcks;
49
- private pendingResponses;
50
49
  /** @internal */
51
- constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions, roomRpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>, roomOutgoingDataStreamManager: OutgoingDataStreamManager, roomOutgoingDataTrackManager: OutgoingDataTrackManager);
50
+ constructor(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions, roomOutgoingDataStreamManager: OutgoingDataStreamManager, roomOutgoingDataTrackManager: OutgoingDataTrackManager, rpcClientManager: RpcClientManager, rpcServerManager: RpcServerManager);
52
51
  get lastCameraError(): Error | undefined;
53
52
  get lastMicrophoneError(): Error | undefined;
54
53
  get isE2EEEnabled(): boolean;
@@ -63,7 +62,6 @@ export default class LocalParticipant extends Participant {
63
62
  private handleClosing;
64
63
  private handleSignalConnected;
65
64
  private handleSignalRequestResponse;
66
- private handleDataPacket;
67
65
  /**
68
66
  * Sets and updates the metadata of the local participant.
69
67
  * Note: this requires `canUpdateOwnMetadata` permission.
@@ -222,7 +220,7 @@ export default class LocalParticipant extends Participant {
222
220
  * @returns A promise that resolves with the response payload or rejects with an error.
223
221
  * @throws Error on failure. Details in `message`.
224
222
  */
225
- performRpc({ destinationIdentity, method, payload, responseTimeout, }: PerformRpcParams): TypedPromise<string, RpcError>;
223
+ performRpc(params: PerformRpcParams): TypedPromise<string, RpcError>;
226
224
  /**
227
225
  * @deprecated use `room.registerRpcMethod` instead
228
226
  */
@@ -249,12 +247,6 @@ export default class LocalParticipant extends Participant {
249
247
  * participant/track. Any omitted participants will not receive any permissions.
250
248
  */
251
249
  setTrackSubscriptionPermissions(allParticipantsAllowed: boolean, participantTrackPermissions?: ParticipantTrackPermission[]): void;
252
- private handleIncomingRpcAck;
253
- private handleIncomingRpcResponse;
254
- /** @internal */
255
- private publishRpcRequest;
256
- /** @internal */
257
- handleParticipantDisconnected(participantIdentity: string): void;
258
250
  /** @internal */
259
251
  setEnabledPublishCodecs(codecs: Codec[]): void;
260
252
  /** @internal */
@@ -22,6 +22,10 @@ export default class RemoteParticipant extends Participant {
22
22
  * const track = await remoteParticipant.dataTracks.getDeferred("data track name"); */
23
23
  dataTracks: DeferrableMap<RemoteDataTrack['info']['name'], RemoteDataTrack>;
24
24
  signalClient: SignalClient;
25
+ /** A version number indicating the set of features that the report participant's client supports.
26
+ * @internal
27
+ **/
28
+ clientProtocol: number;
25
29
  private volumeMap;
26
30
  private audioOutput?;
27
31
  /** @internal */
@@ -31,7 +35,7 @@ export default class RemoteParticipant extends Participant {
31
35
  remoteParticipant: string;
32
36
  };
33
37
  /** @internal */
34
- constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, attributes?: Record<string, string>, loggerOptions?: LoggerOptions, kind?: ParticipantKind, remoteDataTracks?: Array<RemoteDataTrack>);
38
+ constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, attributes?: Record<string, string>, loggerOptions?: LoggerOptions, kind?: ParticipantKind, remoteDataTracks?: Array<RemoteDataTrack>, clientProtocol?: number);
35
39
  protected addTrackPublication(publication: RemoteTrackPublication): void;
36
40
  getTrackPublication(source: Track.Source): RemoteTrackPublication | undefined;
37
41
  getTrackPublicationByName(name: string): RemoteTrackPublication | undefined;
@@ -0,0 +1,43 @@
1
+ import type TypedEmitter from 'typed-emitter';
2
+ import type { StructuredLogger } from '../../../logger';
3
+ import type { TextStreamReader } from '../../data-stream/incoming/StreamReader';
4
+ import type OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
5
+ import type Participant from '../../participant/Participant';
6
+ import type { PerformRpcParams } from '../utils';
7
+ import { RpcError } from '../utils';
8
+ import type { RpcClientManagerCallbacks } from './events';
9
+ declare const RpcClientManager_base: new () => TypedEmitter<RpcClientManagerCallbacks>;
10
+ /**
11
+ * Manages the client (caller) side of RPC: sending requests, tracking pending
12
+ * ack/response state, and handling incoming ack/response packets.
13
+ * @internal
14
+ */
15
+ export default class RpcClientManager extends RpcClientManager_base {
16
+ private log;
17
+ private outgoingDataStreamManager;
18
+ private getRemoteParticipantClientProtocol;
19
+ private getServerVersion;
20
+ private pendingAcks;
21
+ private pendingResponses;
22
+ constructor(log: StructuredLogger, outgoingDataStreamManager: OutgoingDataStreamManager, getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number, getServerVersion: () => string | undefined);
23
+ performRpc({ destinationIdentity, method, payload, responseTimeout: responseTimeoutMs, }: PerformRpcParams): Promise<[
24
+ id: string,
25
+ completionPromise: Promise<string>
26
+ ]>;
27
+ private publishRpcRequest;
28
+ /**
29
+ * Handle an incoming data stream containing an RPC response payload.
30
+ * @internal
31
+ */
32
+ handleIncomingDataStream(reader: TextStreamReader, senderIdentity: Participant['identity'], attributes: Record<string, string>): Promise<void>;
33
+ /** @internal */
34
+ handleIncomingRpcResponseSuccess(requestId: string, payload: string): void;
35
+ /** @internal */
36
+ handleIncomingRpcResponseFailure(requestId: string, error: RpcError): void;
37
+ /** @internal */
38
+ handleIncomingRpcAck(requestId: string): void;
39
+ /** @internal */
40
+ handleParticipantDisconnected(participantIdentity: string): void;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=RpcClientManager.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { DataPacket } from '@livekit/protocol';
2
+ export type EventSendDataPacket = {
3
+ packet: DataPacket;
4
+ };
5
+ export type RpcClientManagerCallbacks = {
6
+ sendDataPacket: (event: EventSendDataPacket) => void;
7
+ };
8
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { default as RpcClientManager } from './client/RpcClientManager';
2
+ export type { RpcClientManagerCallbacks } from './client/events';
3
+ export { default as RpcServerManager } from './server/RpcServerManager';
4
+ export type { RpcServerManagerCallbacks } from './server/events';
5
+ export type { PerformRpcParams, RpcInvocationData } from './utils';
6
+ export { RPC_REQUEST_DATA_STREAM_TOPIC, RPC_RESPONSE_DATA_STREAM_TOPIC, RpcRequestAttrs, RpcError, byteLength, truncateBytes } from './utils';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { RpcRequest } from '@livekit/protocol';
2
+ import type TypedEmitter from 'typed-emitter';
3
+ import type { StructuredLogger } from '../../../logger';
4
+ import type { TextStreamReader } from '../../data-stream/incoming/StreamReader';
5
+ import type OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
6
+ import type Participant from '../../participant/Participant';
7
+ import type { RpcInvocationData } from '../utils';
8
+ import type { RpcServerManagerCallbacks } from './events';
9
+ declare const RpcServerManager_base: new () => TypedEmitter<RpcServerManagerCallbacks>;
10
+ /**
11
+ * Manages the server (handler) side of RPC: processing incoming requests,
12
+ * managing registered method handlers, and sending responses.
13
+ * @internal
14
+ */
15
+ export default class RpcServerManager extends RpcServerManager_base {
16
+ private log;
17
+ private outgoingDataStreamManager;
18
+ private getRemoteParticipantClientProtocol;
19
+ private rpcHandlers;
20
+ constructor(log: StructuredLogger, outgoingDataStreamManager: OutgoingDataStreamManager, getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number);
21
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>): void;
22
+ unregisterRpcMethod(method: string): void;
23
+ /**
24
+ * Handle an incoming RPCRequest message containing a payload.
25
+ * This handles "version 1" of rpc requests.
26
+ * @internal
27
+ */
28
+ handleIncomingRpcRequest(callerIdentity: string, rpcRequest: RpcRequest): Promise<void>;
29
+ /**
30
+ * Handle an incoming data stream containing a RPC request payload.
31
+ * This handles "version 2" of rpc requests.
32
+ * @internal
33
+ */
34
+ handleIncomingDataStream(reader: TextStreamReader, callerIdentity: Participant['identity'], dataStreamAttrs: Record<string, string>): Promise<void>;
35
+ private publishRpcAck;
36
+ private publishRpcResponsePacket;
37
+ /**
38
+ * Send a successful RPC response payload, choosing the transport based on
39
+ * the caller's client protocol version.
40
+ */
41
+ private publishRpcResponse;
42
+ }
43
+ export {};
44
+ //# sourceMappingURL=RpcServerManager.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { DataPacket } from '@livekit/protocol';
2
+ export type EventSendDataPacket = {
3
+ packet: DataPacket;
4
+ };
5
+ export type RpcServerManagerCallbacks = {
6
+ sendDataPacket: (event: EventSendDataPacket) => void;
7
+ };
8
+ //# sourceMappingURL=events.d.ts.map
@@ -49,6 +49,7 @@ export declare class RpcError extends Error {
49
49
  static MAX_DATA_BYTES: number;
50
50
  code: number;
51
51
  data?: string;
52
+ cause?: unknown;
52
53
  /**
53
54
  * Creates an error object with the given code and message, plus an optional data payload.
54
55
  *
@@ -56,7 +57,9 @@ export declare class RpcError extends Error {
56
57
  *
57
58
  * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
58
59
  */
59
- constructor(code: number, message: string, data?: string);
60
+ constructor(code: number, message: string, data?: string, options?: {
61
+ cause?: unknown;
62
+ });
60
63
  /**
61
64
  * @internal
62
65
  */
@@ -87,9 +90,36 @@ export declare class RpcError extends Error {
87
90
  *
88
91
  * @internal
89
92
  */
90
- static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError;
93
+ static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string, options?: {
94
+ cause?: unknown;
95
+ }): RpcError;
91
96
  }
92
- export declare const MAX_PAYLOAD_BYTES = 15360;
97
+ export declare const MAX_V1_PAYLOAD_BYTES = 15360;
98
+ /**
99
+ * Topic used for v2 RPC request data streams.
100
+ * @internal
101
+ */
102
+ export declare const RPC_REQUEST_DATA_STREAM_TOPIC = "lk.rpc_request";
103
+ /**
104
+ * Topic used for v2 RPC response data streams.
105
+ * @internal
106
+ */
107
+ export declare const RPC_RESPONSE_DATA_STREAM_TOPIC = "lk.rpc_response";
108
+ /** @internal */
109
+ export declare enum RpcRequestAttrs {
110
+ RPC_REQUEST_ID = "lk.rpc_request_id",
111
+ RPC_REQUEST_METHOD = "lk.rpc_request_method",
112
+ RPC_REQUEST_RESPONSE_TIMEOUT_MS = "lk.rpc_request_response_timeout_ms",
113
+ RPC_REQUEST_VERSION = "lk.rpc_request_version"
114
+ }
115
+ /** Initial version of rpc which uses RpcRequest / RpcResponse messages.
116
+ * @internal
117
+ **/
118
+ export declare const RPC_VERSION_V1 = 1;
119
+ /** Rpc version backed by data streams instead of RpcRequest / RpcResponse.
120
+ * @internal
121
+ **/
122
+ export declare const RPC_VERSION_V2 = 2;
93
123
  /**
94
124
  * @internal
95
125
  */
@@ -98,4 +128,4 @@ export declare function byteLength(str: string): number;
98
128
  * @internal
99
129
  */
100
130
  export declare function truncateBytes(str: string, maxBytes: number): string;
101
- //# sourceMappingURL=rpc.d.ts.map
131
+ //# sourceMappingURL=utils.d.ts.map
@@ -147,4 +147,5 @@ export declare function isRemoteParticipant(p: Participant): p is RemoteParticip
147
147
  export declare function splitUtf8(s: string, n: number): NonSharedUint8Array[];
148
148
  export declare function extractMaxAgeFromRequestHeaders(headers: Headers): number | undefined;
149
149
  export declare function isCompressionStreamSupported(): boolean;
150
+ export declare function isPublisherOfferWithJoinSupported(): boolean;
150
151
  //# sourceMappingURL=utils.d.ts.map
@@ -1,3 +1,11 @@
1
1
  export declare const version: string;
2
2
  export declare const protocolVersion = 17;
3
+ /** Initial client protocol. */
4
+ export declare const CLIENT_PROTOCOL_DEFAULT = 0;
5
+ /** Replaces RPC v1 protocol with a v2 data streams based one to support unlimited request /
6
+ * response payload length. */
7
+ export declare const CLIENT_PROTOCOL_DATA_STREAM_RPC = 1;
8
+ /** The client protocol version indicates what level of support that the client has for
9
+ * client <-> client api interactions. */
10
+ export declare const clientProtocol = 1;
3
11
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.18.10",
3
+ "version": "2.19.1",
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",
@@ -91,6 +91,7 @@
91
91
  "happy-dom": "^20.0.0",
92
92
  "jsdom": "^26.1.0",
93
93
  "prettier": "^3.4.2",
94
+ "publint": "^0.3.21",
94
95
  "rollup": "4.60.2",
95
96
  "rollup-plugin-delete": "^2.1.0",
96
97
  "rollup-plugin-typescript2": "0.37.0",
@@ -118,7 +119,8 @@
118
119
  "format": "prettier --write src examples/**/*.ts",
119
120
  "format:check": "prettier --check src examples/**/*.ts",
120
121
  "throws:check": "pnpm --package=@livekit/throws-transformer dlx throws-check 'src/!(*.test).ts' 'src/**/!(*.test).ts'",
121
- "ci:publish": "pnpm build:clean && pnpm compat && changeset publish",
122
+ "type:check": "tsc --noEmit",
123
+ "ci:publish": "pnpm type:check && pnpm build && pnpm compat && changeset publish",
122
124
  "downlevel-dts": "downlevel-dts ./dist/src ./dist/ts4.2 --to=4.2",
123
125
  "compat": "eslint --config ./eslint.config.dist.mjs --no-inline-config ./dist/livekit-client.esm.mjs",
124
126
  "size-limit": "size-limit"
@@ -1157,6 +1157,7 @@ function createConnectionParams(
1157
1157
  params.set('sdk', isReactNative() ? 'reactnative' : 'js');
1158
1158
  params.set('version', info.version!);
1159
1159
  params.set('protocol', info.protocol!.toString());
1160
+ params.set('client_protocol', info.clientProtocol!.toString());
1160
1161
  if (info.deviceModel) {
1161
1162
  params.set('device_model', info.deviceModel);
1162
1163
  }
@@ -26,7 +26,6 @@ import {
26
26
  Room as RoomModel,
27
27
  RoomMovedResponse,
28
28
  RpcAck,
29
- RpcResponse,
30
29
  ServerInfo,
31
30
  SessionDescription,
32
31
  SignalTarget,
@@ -79,7 +78,6 @@ import {
79
78
  UnexpectedConnectionState,
80
79
  } from './errors';
81
80
  import { EngineEvent } from './events';
82
- import { RpcError } from './rpc';
83
81
  import CriticalTimers from './timers';
84
82
  import type LocalTrack from './track/LocalTrack';
85
83
  import type LocalTrackPublication from './track/LocalTrackPublication';
@@ -92,7 +90,7 @@ import { getTrackPublicationInfo } from './track/utils';
92
90
  import type { LoggerOptions } from './types';
93
91
  import {
94
92
  Future,
95
- isCompressionStreamSupported,
93
+ isPublisherOfferWithJoinSupported,
96
94
  isVideoCodec,
97
95
  isVideoTrack,
98
96
  isWeb,
@@ -124,7 +122,7 @@ enum PCState {
124
122
  export enum DataChannelKind {
125
123
  RELIABLE = DataPacket_Kind.RELIABLE,
126
124
  LOSSY = DataPacket_Kind.LOSSY,
127
- DATA_TRACK_LOSSY,
125
+ DATA_TRACK_LOSSY = 2,
128
126
  }
129
127
 
130
128
  /** @internal */
@@ -327,7 +325,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
327
325
 
328
326
  this.setupSignalClientCallbacks();
329
327
  let offerProto: SessionDescription | undefined;
330
- if (!useV0Path && isCompressionStreamSupported()) {
328
+ if (!useV0Path && isPublisherOfferWithJoinSupported()) {
331
329
  if (!this.pcManager) {
332
330
  await this.configure();
333
331
  this.createDataChannels();
@@ -360,7 +358,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
360
358
  this.participantSid = joinResponse.participant?.sid;
361
359
 
362
360
  this.subscriberPrimary = joinResponse.subscriberPrimary;
363
- if (!useV0Path && isCompressionStreamSupported()) {
361
+ if (!useV0Path && isPublisherOfferWithJoinSupported()) {
364
362
  this.pcManager?.updateConfiguration(this.makeRTCConfiguration(joinResponse));
365
363
  } else {
366
364
  if (!this.pcManager) {
@@ -1464,30 +1462,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1464
1462
  });
1465
1463
  };
1466
1464
 
1467
- /** @internal */
1468
- async publishRpcResponse(
1469
- destinationIdentity: string,
1470
- requestId: string,
1471
- payload: string | null,
1472
- error: RpcError | null,
1473
- ) {
1474
- const packet = new DataPacket({
1475
- destinationIdentities: [destinationIdentity],
1476
- kind: DataPacket_Kind.RELIABLE,
1477
- value: {
1478
- case: 'rpcResponse',
1479
- value: new RpcResponse({
1480
- requestId,
1481
- value: error
1482
- ? { case: 'error', value: error.toProto() }
1483
- : { case: 'payload', value: payload ?? '' },
1484
- }),
1485
- },
1486
- });
1487
-
1488
- await this.sendDataPacket(packet, DataChannelKind.RELIABLE);
1489
- }
1490
-
1491
1465
  /** @internal */
1492
1466
  async publishRpcAck(destinationIdentity: string, requestId: string) {
1493
1467
  const packet = new DataPacket({
@@ -1,5 +1,5 @@
1
1
  import { ClientInfo_Capability, JoinResponse } from '@livekit/protocol';
2
- import { describe, expect, it, vi } from 'vitest';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
3
  import Room from './Room';
4
4
  import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
5
5
  import { RoomEvent } from './events';
@@ -89,3 +89,101 @@ describe('Room signaling options', () => {
89
89
  );
90
90
  });
91
91
  });
92
+
93
+ describe('Room lifecycle', () => {
94
+ afterEach(() => {
95
+ vi.restoreAllMocks();
96
+ // Tear down the mocked mediaDevices so other tests see the env they
97
+ // expected (happy-dom does not provide navigator.mediaDevices by default).
98
+ if ((navigator as { mediaDevices?: unknown }).mediaDevices) {
99
+ Object.defineProperty(navigator, 'mediaDevices', {
100
+ configurable: true,
101
+ value: undefined,
102
+ });
103
+ }
104
+ });
105
+
106
+ it('wraps the constructor-registered devicechange listener in a WeakRef so the Room is GC-eligible (#1940)', async () => {
107
+ // happy-dom does not provide navigator.mediaDevices. Install a minimal
108
+ // EventTarget stand-in so the constructor takes the listener-registration
109
+ // branch and we can observe the registered listener.
110
+ const mediaDevices = new EventTarget() as EventTarget & {
111
+ addEventListener: EventTarget['addEventListener'];
112
+ removeEventListener: EventTarget['removeEventListener'];
113
+ };
114
+ Object.defineProperty(navigator, 'mediaDevices', {
115
+ configurable: true,
116
+ value: mediaDevices,
117
+ });
118
+
119
+ const addSpy = vi.spyOn(mediaDevices, 'addEventListener');
120
+ const derefSpy = vi.spyOn(WeakRef.prototype, 'deref');
121
+ const cleanupRegistrySpy = Room.cleanupRegistry
122
+ ? vi.spyOn(Room.cleanupRegistry, 'register')
123
+ : undefined;
124
+
125
+ const room = new Room();
126
+ const handleDeviceChangeSpy = vi.spyOn(
127
+ room as unknown as { handleDeviceChange: (ev: Event) => void },
128
+ 'handleDeviceChange',
129
+ );
130
+
131
+ // Constructor must register exactly one devicechange listener with AbortSignal teardown.
132
+ const deviceChangeAdds = addSpy.mock.calls.filter(([type]) => type === 'devicechange');
133
+ expect(deviceChangeAdds).toHaveLength(1);
134
+ const listener = deviceChangeAdds[0][1] as EventListener;
135
+ const addOptions = deviceChangeAdds[0][2] as AddEventListenerOptions | undefined;
136
+ expect(addOptions?.signal).toBeInstanceOf(AbortSignal);
137
+
138
+ // FinalizationRegistry must be registered with the Room as the target so the
139
+ // cleanup callback fires when the user drops their Room reference.
140
+ if (Room.cleanupRegistry) {
141
+ expect(cleanupRegistrySpy).toHaveBeenCalledWith(room, expect.any(Function));
142
+ }
143
+
144
+ // While the WeakRef still derefs to the Room, the listener forwards to handleDeviceChange.
145
+ listener.call(null, new Event('devicechange'));
146
+ expect(handleDeviceChangeSpy).toHaveBeenCalledTimes(1);
147
+
148
+ // Simulate the Room being GC'd by forcing deref to return undefined; the
149
+ // listener must short-circuit instead of calling handleDeviceChange.
150
+ derefSpy.mockReturnValue(undefined);
151
+ listener.call(null, new Event('devicechange'));
152
+ expect(handleDeviceChangeSpy).toHaveBeenCalledTimes(1);
153
+ });
154
+
155
+ it('falls back to a direct devicechange listener when WeakRef/FinalizationRegistry are unavailable (#1944)', async () => {
156
+ const mediaDevices = new EventTarget() as EventTarget & {
157
+ addEventListener: EventTarget['addEventListener'];
158
+ removeEventListener: EventTarget['removeEventListener'];
159
+ };
160
+ Object.defineProperty(navigator, 'mediaDevices', {
161
+ configurable: true,
162
+ value: mediaDevices,
163
+ });
164
+
165
+ // Simulate a legacy browser by stubbing out cleanupRegistry.
166
+ const originalRegistry = Room.cleanupRegistry;
167
+ Object.defineProperty(Room, 'cleanupRegistry', {
168
+ configurable: true,
169
+ value: false,
170
+ });
171
+
172
+ try {
173
+ const addSpy = vi.spyOn(mediaDevices, 'addEventListener');
174
+ const room = new Room();
175
+ const handleDeviceChange = (room as unknown as { handleDeviceChange: () => void })
176
+ .handleDeviceChange;
177
+
178
+ const deviceChangeAdds = addSpy.mock.calls.filter(([type]) => type === 'devicechange');
179
+ expect(deviceChangeAdds).toHaveLength(1);
180
+ // The registered listener is the bare handleDeviceChange method (no WeakRef closure).
181
+ expect(deviceChangeAdds[0][1]).toBe(handleDeviceChange);
182
+ } finally {
183
+ Object.defineProperty(Room, 'cleanupRegistry', {
184
+ configurable: true,
185
+ value: originalRegistry,
186
+ });
187
+ }
188
+ });
189
+ });