livekit-client 2.18.9 → 2.19.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.
Files changed (217) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +5609 -644
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +3553 -2813
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.pt.worker.js +2 -0
  8. package/dist/livekit-client.pt.worker.js.map +1 -0
  9. package/dist/livekit-client.pt.worker.mjs +5834 -0
  10. package/dist/livekit-client.pt.worker.mjs.map +1 -0
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts +2 -1
  14. package/dist/src/api/SignalClient.d.ts.map +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts +8 -7
  16. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  17. package/dist/src/e2ee/types.d.ts +35 -8
  18. package/dist/src/e2ee/types.d.ts.map +1 -1
  19. package/dist/src/e2ee/utils.d.ts +5 -5
  20. package/dist/src/e2ee/utils.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
  22. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  25. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
  26. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  27. package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
  28. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  29. package/dist/src/index.d.ts +4 -1
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/options.d.ts +7 -0
  32. package/dist/src/options.d.ts.map +1 -1
  33. package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
  34. package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
  35. package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
  36. package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
  37. package/dist/src/packetTrailer/types.d.ts +57 -0
  38. package/dist/src/packetTrailer/types.d.ts.map +1 -0
  39. package/dist/src/packetTrailer/utils.d.ts +9 -0
  40. package/dist/src/packetTrailer/utils.d.ts.map +1 -0
  41. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  42. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
  43. package/dist/src/room/RTCEngine.d.ts +2 -4
  44. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  45. package/dist/src/room/Room.d.ts +7 -3
  46. package/dist/src/room/Room.d.ts.map +1 -1
  47. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
  48. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  49. package/dist/src/room/data-track/depacketizer.d.ts +12 -4
  50. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  51. package/dist/src/room/data-track/frame.d.ts +3 -3
  52. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  53. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  54. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  55. package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
  56. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  57. package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
  58. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  59. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  60. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  61. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  62. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  63. package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
  64. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  65. package/dist/src/room/data-track/types.d.ts +7 -0
  66. package/dist/src/room/data-track/types.d.ts.map +1 -1
  67. package/dist/src/room/events.d.ts +2 -2
  68. package/dist/src/room/participant/LocalParticipant.d.ts +8 -14
  69. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  70. package/dist/src/room/participant/Participant.d.ts +1 -1
  71. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  72. package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
  73. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  74. package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
  75. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
  76. package/dist/src/room/rpc/client/events.d.ts +8 -0
  77. package/dist/src/room/rpc/client/events.d.ts.map +1 -0
  78. package/dist/src/room/rpc/index.d.ts +6 -0
  79. package/dist/src/room/rpc/index.d.ts.map +1 -0
  80. package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
  81. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
  82. package/dist/src/room/rpc/server/events.d.ts +8 -0
  83. package/dist/src/room/rpc/server/events.d.ts.map +1 -0
  84. package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  85. package/dist/src/room/rpc/utils.d.ts.map +1 -0
  86. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  87. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  88. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  89. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  90. package/dist/src/room/track/Track.d.ts +1 -1
  91. package/dist/src/room/track/Track.d.ts.map +1 -1
  92. package/dist/src/room/track/create.d.ts.map +1 -1
  93. package/dist/src/room/track/options.d.ts +10 -0
  94. package/dist/src/room/track/options.d.ts.map +1 -1
  95. package/dist/src/room/track/utils.d.ts.map +1 -1
  96. package/dist/src/room/utils.d.ts +4 -3
  97. package/dist/src/room/utils.d.ts.map +1 -1
  98. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  99. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  100. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  101. package/dist/src/version.d.ts +9 -1
  102. package/dist/src/version.d.ts.map +1 -1
  103. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  104. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  105. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  106. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  107. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  108. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  109. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  110. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  111. package/dist/ts4.2/index.d.ts +5 -1
  112. package/dist/ts4.2/options.d.ts +7 -0
  113. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  114. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  115. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  116. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  117. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  118. package/dist/ts4.2/room/RTCEngine.d.ts +2 -4
  119. package/dist/ts4.2/room/Room.d.ts +7 -3
  120. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  121. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  122. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  123. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  125. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
  126. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  127. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  128. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  129. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  130. package/dist/ts4.2/room/events.d.ts +2 -2
  131. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -14
  132. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  133. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
  134. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
  135. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
  136. package/dist/ts4.2/room/rpc/index.d.ts +7 -0
  137. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
  138. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
  139. package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  140. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  141. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  142. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  143. package/dist/ts4.2/room/track/options.d.ts +10 -0
  144. package/dist/ts4.2/room/utils.d.ts +4 -3
  145. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  146. package/dist/ts4.2/version.d.ts +9 -1
  147. package/package.json +24 -16
  148. package/src/api/SignalClient.test.ts +102 -10
  149. package/src/api/SignalClient.ts +4 -2
  150. package/src/api/WebSocketStream.test.ts +0 -1
  151. package/src/e2ee/E2eeManager.ts +82 -30
  152. package/src/e2ee/types.ts +37 -8
  153. package/src/e2ee/utils.ts +7 -6
  154. package/src/e2ee/worker/DataCryptor.ts +6 -6
  155. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  156. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  157. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  158. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  159. package/src/e2ee/worker/naluUtils.ts +4 -4
  160. package/src/e2ee/worker/sifPayload.ts +10 -8
  161. package/src/index.ts +7 -0
  162. package/src/options.ts +8 -0
  163. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  164. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  165. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  166. package/src/packetTrailer/packetTrailer.ts +276 -0
  167. package/src/packetTrailer/types.ts +75 -0
  168. package/src/packetTrailer/utils.test.ts +105 -0
  169. package/src/packetTrailer/utils.ts +50 -0
  170. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  171. package/src/packetTrailer/worker/tsconfig.json +14 -0
  172. package/src/room/BackOffStrategy.test.ts +1 -1
  173. package/src/room/RTCEngine.test.ts +219 -0
  174. package/src/room/RTCEngine.ts +86 -46
  175. package/src/room/Room.test.ts +62 -1
  176. package/src/room/Room.ts +111 -86
  177. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  178. package/src/room/data-track/depacketizer.test.ts +433 -1
  179. package/src/room/data-track/depacketizer.ts +79 -61
  180. package/src/room/data-track/frame.ts +2 -2
  181. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  182. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  183. package/src/room/data-track/incoming/pipeline.ts +13 -2
  184. package/src/room/data-track/outgoing/types.ts +3 -2
  185. package/src/room/data-track/packet/extensions.ts +2 -2
  186. package/src/room/data-track/packet/index.ts +6 -6
  187. package/src/room/data-track/packet/serializable.ts +1 -1
  188. package/src/room/data-track/types.ts +8 -0
  189. package/src/room/events.ts +2 -2
  190. package/src/room/participant/LocalParticipant.test.ts +81 -0
  191. package/src/room/participant/LocalParticipant.ts +64 -187
  192. package/src/room/participant/Participant.ts +1 -1
  193. package/src/room/participant/RemoteParticipant.ts +9 -0
  194. package/src/room/participant/publishUtils.ts +1 -1
  195. package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
  196. package/src/room/rpc/client/RpcClientManager.ts +269 -0
  197. package/src/room/rpc/client/events.ts +9 -0
  198. package/src/room/rpc/index.ts +14 -0
  199. package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
  200. package/src/room/rpc/server/RpcServerManager.ts +293 -0
  201. package/src/room/rpc/server/events.ts +9 -0
  202. package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
  203. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  204. package/src/room/track/RemoteVideoTrack.ts +23 -2
  205. package/src/room/track/Track.ts +1 -1
  206. package/src/room/track/create.ts +0 -4
  207. package/src/room/track/options.ts +11 -0
  208. package/src/room/track/record.ts +1 -1
  209. package/src/room/track/utils.ts +4 -1
  210. package/src/room/utils.test.ts +14 -1
  211. package/src/room/utils.ts +19 -4
  212. package/src/test/MockMediaStreamTrack.ts +0 -1
  213. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  214. package/src/utils/dataPacketBuffer.ts +1 -1
  215. package/src/version.ts +11 -1
  216. package/dist/src/room/rpc.d.ts.map +0 -1
  217. package/src/room/rpc.test.ts +0 -301
@@ -0,0 +1,293 @@
1
+ import { DataPacket, DataPacket_Kind, RpcAck, RpcRequest, RpcResponse } from '@livekit/protocol';
2
+ import EventEmitter from 'events';
3
+ import type TypedEmitter from 'typed-emitter';
4
+ import { type StructuredLogger } from '../../../logger';
5
+ import { CLIENT_PROTOCOL_DATA_STREAM_RPC } from '../../../version';
6
+ import { type TextStreamReader } from '../../data-stream/incoming/StreamReader';
7
+ import type OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
8
+ import type Participant from '../../participant/Participant';
9
+ import {
10
+ MAX_V1_PAYLOAD_BYTES,
11
+ RPC_RESPONSE_DATA_STREAM_TOPIC,
12
+ RPC_VERSION_V2,
13
+ RpcError,
14
+ type RpcInvocationData,
15
+ RpcRequestAttrs,
16
+ byteLength,
17
+ } from '../utils';
18
+ import type { RpcServerManagerCallbacks } from './events';
19
+
20
+ /**
21
+ * Manages the server (handler) side of RPC: processing incoming requests,
22
+ * managing registered method handlers, and sending responses.
23
+ * @internal
24
+ */
25
+ export default class RpcServerManager extends (EventEmitter as new () => TypedEmitter<RpcServerManagerCallbacks>) {
26
+ private log: StructuredLogger;
27
+
28
+ private outgoingDataStreamManager: OutgoingDataStreamManager;
29
+
30
+ private getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number;
31
+
32
+ private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
33
+
34
+ constructor(
35
+ log: StructuredLogger,
36
+ outgoingDataStreamManager: OutgoingDataStreamManager,
37
+ getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number,
38
+ ) {
39
+ super();
40
+ this.log = log;
41
+ this.outgoingDataStreamManager = outgoingDataStreamManager;
42
+ this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
43
+ }
44
+
45
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
46
+ if (this.rpcHandlers.has(method)) {
47
+ throw Error(
48
+ `RPC handler already registered for method ${method}, unregisterRpcMethod before trying to register again`,
49
+ );
50
+ }
51
+ this.rpcHandlers.set(method, handler);
52
+ }
53
+
54
+ unregisterRpcMethod(method: string) {
55
+ this.rpcHandlers.delete(method);
56
+ }
57
+
58
+ /**
59
+ * Handle an incoming RPCRequest message containing a payload.
60
+ * This handles "version 1" of rpc requests.
61
+ * @internal
62
+ */
63
+ async handleIncomingRpcRequest(callerIdentity: string, rpcRequest: RpcRequest) {
64
+ this.publishRpcAck(callerIdentity, rpcRequest.id);
65
+
66
+ if (rpcRequest.version !== 1) {
67
+ this.publishRpcResponsePacket(
68
+ callerIdentity,
69
+ rpcRequest.id,
70
+ null,
71
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
72
+ );
73
+ return;
74
+ }
75
+
76
+ const handler = this.rpcHandlers.get(rpcRequest.method);
77
+
78
+ if (!handler) {
79
+ this.publishRpcResponsePacket(
80
+ callerIdentity,
81
+ rpcRequest.id,
82
+ null,
83
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
84
+ );
85
+ return;
86
+ }
87
+
88
+ let response;
89
+ try {
90
+ response = await handler({
91
+ requestId: rpcRequest.id,
92
+ callerIdentity,
93
+ payload: rpcRequest.payload,
94
+ responseTimeout: rpcRequest.responseTimeoutMs,
95
+ });
96
+ } catch (error) {
97
+ let responseError;
98
+ if (error instanceof RpcError) {
99
+ responseError = error;
100
+ } else {
101
+ this.log.warn(
102
+ `Uncaught error returned by RPC handler for ${rpcRequest.method}. Returning APPLICATION_ERROR instead.`,
103
+ error,
104
+ );
105
+ responseError = RpcError.builtIn(
106
+ 'APPLICATION_ERROR',
107
+ `Uncaught error: ${(error as Error)?.message ?? error}`,
108
+ { cause: error },
109
+ );
110
+ }
111
+
112
+ this.publishRpcResponsePacket(callerIdentity, rpcRequest.id, null, responseError);
113
+ return;
114
+ }
115
+
116
+ await this.publishRpcResponse(callerIdentity, rpcRequest.id, response ?? '');
117
+ }
118
+
119
+ /**
120
+ * Handle an incoming data stream containing a RPC request payload.
121
+ * This handles "version 2" of rpc requests.
122
+ * @internal
123
+ */
124
+ async handleIncomingDataStream(
125
+ reader: TextStreamReader,
126
+ callerIdentity: Participant['identity'],
127
+ dataStreamAttrs: Record<string, string>,
128
+ ) {
129
+ const requestId = dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_ID];
130
+ const method = dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_METHOD];
131
+ const responseTimeout = parseInt(
132
+ dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS],
133
+ 10,
134
+ );
135
+ const version = parseInt(dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_VERSION], 10);
136
+
137
+ if (!requestId || !method || Number.isNaN(responseTimeout) || Number.isNaN(version)) {
138
+ this.log.warn(
139
+ `RPC data stream malformed: ${RpcRequestAttrs.RPC_REQUEST_ID} / ${RpcRequestAttrs.RPC_REQUEST_METHOD} / ${RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS} / ${RpcRequestAttrs.RPC_REQUEST_VERSION} not set.`,
140
+ );
141
+ this.publishRpcResponsePacket(
142
+ callerIdentity,
143
+ requestId,
144
+ null,
145
+ RpcError.builtIn('APPLICATION_ERROR', 'RPC data stream malformed'),
146
+ );
147
+ return;
148
+ }
149
+
150
+ this.publishRpcAck(callerIdentity, requestId);
151
+
152
+ if (version !== RPC_VERSION_V2) {
153
+ this.publishRpcResponsePacket(
154
+ callerIdentity,
155
+ requestId,
156
+ null,
157
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
158
+ );
159
+ return;
160
+ }
161
+
162
+ let payload: string;
163
+ try {
164
+ payload = await reader.readAll();
165
+ } catch (e) {
166
+ this.log.warn(`Error reading RPC request payload: ${e}`);
167
+ this.publishRpcResponsePacket(
168
+ callerIdentity,
169
+ requestId,
170
+ null,
171
+ RpcError.builtIn('APPLICATION_ERROR', 'Error reading RPC request payload', { cause: e }),
172
+ );
173
+ return;
174
+ }
175
+
176
+ const handler = this.rpcHandlers.get(method);
177
+
178
+ if (!handler) {
179
+ this.publishRpcResponsePacket(
180
+ callerIdentity,
181
+ requestId,
182
+ null,
183
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
184
+ );
185
+ return;
186
+ }
187
+
188
+ let response;
189
+ try {
190
+ response = await handler({
191
+ requestId,
192
+ callerIdentity,
193
+ payload,
194
+ responseTimeout,
195
+ });
196
+ } catch (error) {
197
+ let responseError;
198
+ if (error instanceof RpcError) {
199
+ responseError = error;
200
+ } else {
201
+ this.log.warn(
202
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
203
+ error,
204
+ );
205
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
206
+ }
207
+
208
+ this.publishRpcResponsePacket(callerIdentity, requestId, null, responseError);
209
+ return;
210
+ }
211
+
212
+ await this.publishRpcResponse(callerIdentity, requestId, response ?? '');
213
+ }
214
+
215
+ private publishRpcAck(destinationIdentity: string, requestId: string) {
216
+ this.emit('sendDataPacket', {
217
+ packet: new DataPacket({
218
+ destinationIdentities: [destinationIdentity],
219
+ kind: DataPacket_Kind.RELIABLE,
220
+ value: {
221
+ case: 'rpcAck',
222
+ value: new RpcAck({
223
+ requestId,
224
+ }),
225
+ },
226
+ }),
227
+ });
228
+ }
229
+
230
+ private publishRpcResponsePacket(
231
+ destinationIdentity: string,
232
+ requestId: string,
233
+ payload: string | null,
234
+ error: RpcError | null,
235
+ ) {
236
+ this.emit('sendDataPacket', {
237
+ packet: new DataPacket({
238
+ destinationIdentities: [destinationIdentity],
239
+ kind: DataPacket_Kind.RELIABLE,
240
+ value: {
241
+ case: 'rpcResponse',
242
+ value: new RpcResponse({
243
+ requestId,
244
+ value: error
245
+ ? { case: 'error', value: error.toProto() }
246
+ : { case: 'payload', value: payload ?? '' },
247
+ }),
248
+ },
249
+ }),
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Send a successful RPC response payload, choosing the transport based on
255
+ * the caller's client protocol version.
256
+ */
257
+ private async publishRpcResponse(
258
+ destinationIdentity: string,
259
+ requestId: string,
260
+ payload: string,
261
+ ) {
262
+ const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
263
+
264
+ if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
265
+ // Send response as a data stream
266
+ const writer = await this.outgoingDataStreamManager.streamText({
267
+ topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
268
+ destinationIdentities: [destinationIdentity],
269
+ attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
270
+ });
271
+ await writer.write(payload);
272
+ await writer.close();
273
+ return;
274
+ }
275
+
276
+ // Legacy client: enforce size limit and send uncompressed payload inline
277
+ const responseBytes = byteLength(payload);
278
+ if (responseBytes > MAX_V1_PAYLOAD_BYTES) {
279
+ this.log.warn(
280
+ `RPC Response payload too large for request ${requestId}. To send larger responses, consider updating the sending client.`,
281
+ );
282
+ this.publishRpcResponsePacket(
283
+ destinationIdentity,
284
+ requestId,
285
+ null,
286
+ RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE'),
287
+ );
288
+ return;
289
+ }
290
+
291
+ this.publishRpcResponsePacket(destinationIdentity, requestId, payload, null);
292
+ }
293
+ }
@@ -0,0 +1,9 @@
1
+ import type { DataPacket } from '@livekit/protocol';
2
+
3
+ export type EventSendDataPacket = {
4
+ packet: DataPacket;
5
+ };
6
+
7
+ export type RpcServerManagerCallbacks = {
8
+ sendDataPacket: (event: EventSendDataPacket) => void;
9
+ };
@@ -1,6 +1,3 @@
1
- // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
- //
3
- // SPDX-License-Identifier: Apache-2.0
4
1
  import { RpcError as RpcError_Proto } from '@livekit/protocol';
5
2
 
6
3
  /** Parameters for initiating an RPC call */
@@ -63,6 +60,9 @@ export class RpcError extends Error {
63
60
 
64
61
  data?: string;
65
62
 
63
+ // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
64
+ cause?: unknown;
65
+
66
66
  /**
67
67
  * Creates an error object with the given code and message, plus an optional data payload.
68
68
  *
@@ -70,11 +70,15 @@ export class RpcError extends Error {
70
70
  *
71
71
  * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
72
72
  */
73
- constructor(code: number, message: string, data?: string) {
73
+ constructor(code: number, message: string, data?: string, options?: { cause?: unknown }) {
74
74
  super(message);
75
75
  this.code = code;
76
76
  this.message = truncateBytes(message, RpcError.MAX_MESSAGE_BYTES);
77
77
  this.data = data ? truncateBytes(data, RpcError.MAX_DATA_BYTES) : undefined;
78
+
79
+ if (typeof options?.cause !== 'undefined') {
80
+ this.cause = options?.cause;
81
+ }
78
82
  }
79
83
 
80
84
  /**
@@ -133,16 +137,53 @@ export class RpcError extends Error {
133
137
  *
134
138
  * @internal
135
139
  */
136
- static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError {
137
- return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data);
140
+ static builtIn(
141
+ key: keyof typeof RpcError.ErrorCode,
142
+ data?: string,
143
+ options?: { cause?: unknown },
144
+ ): RpcError {
145
+ return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data, options);
138
146
  }
139
147
  }
140
148
 
141
149
  /*
142
- * Maximum payload size for RPC requests and responses. If a payload exceeds this size,
150
+ * Maximum payload size for RPC requests and responses for clients with a clientProtocol of less
151
+ * than CLIENT_PROTOCOL_DATA_STREAM_RPC.
152
+ *
153
+ * If a payload exceeds this size and the remote client does not support compression,
143
154
  * the RPC call will fail with a REQUEST_PAYLOAD_TOO_LARGE(1402) or RESPONSE_PAYLOAD_TOO_LARGE(1504) error.
144
155
  */
145
- export const MAX_PAYLOAD_BYTES = 15360; // 15 KB
156
+ export const MAX_V1_PAYLOAD_BYTES = 15360; // 15 KB
157
+
158
+ /**
159
+ * Topic used for v2 RPC request data streams.
160
+ * @internal
161
+ */
162
+ export const RPC_REQUEST_DATA_STREAM_TOPIC = 'lk.rpc_request';
163
+
164
+ /**
165
+ * Topic used for v2 RPC response data streams.
166
+ * @internal
167
+ */
168
+ export const RPC_RESPONSE_DATA_STREAM_TOPIC = 'lk.rpc_response';
169
+
170
+ /** @internal */
171
+ export enum RpcRequestAttrs {
172
+ RPC_REQUEST_ID = 'lk.rpc_request_id',
173
+ RPC_REQUEST_METHOD = 'lk.rpc_request_method',
174
+ RPC_REQUEST_RESPONSE_TIMEOUT_MS = 'lk.rpc_request_response_timeout_ms',
175
+ RPC_REQUEST_VERSION = 'lk.rpc_request_version',
176
+ }
177
+
178
+ /** Initial version of rpc which uses RpcRequest / RpcResponse messages.
179
+ * @internal
180
+ **/
181
+ export const RPC_VERSION_V1 = 1;
182
+
183
+ /** Rpc version backed by data streams instead of RpcRequest / RpcResponse.
184
+ * @internal
185
+ **/
186
+ export const RPC_VERSION_V2 = 2;
146
187
 
147
188
  /**
148
189
  * @internal
@@ -0,0 +1,43 @@
1
+ import type { PacketTrailerMetadata } from '../../packetTrailer/types';
2
+
3
+ const MAX_ENTRIES = 300;
4
+
5
+ /**
6
+ * Caches packet trailer metadata extracted from received video frames,
7
+ * keyed by RTP timestamp so it can be looked up when the frame is displayed.
8
+ *
9
+ * Metadata is populated either by the packet trailer worker managed by
10
+ * `PacketTrailerManager` (non-E2EE) or by the E2EE FrameCryptor worker
11
+ * after decryption (E2EE).
12
+ *
13
+ * @experimental
14
+ */
15
+ export class PacketTrailerExtractor {
16
+ private metadataMap = new Map<number, PacketTrailerMetadata>();
17
+
18
+ private activeSsrc: number = 0;
19
+
20
+ storeMetadata(rtpTimestamp: number, ssrc: number, metadata: PacketTrailerMetadata) {
21
+ // Simulcast layer switch: SSRC changed, flush stale entries from old layer.
22
+ if (this.activeSsrc !== 0 && this.activeSsrc !== ssrc) {
23
+ this.metadataMap.clear();
24
+ }
25
+ this.activeSsrc = ssrc;
26
+
27
+ while (this.metadataMap.size >= MAX_ENTRIES) {
28
+ const evicted = this.metadataMap.keys().next().value!;
29
+ this.metadataMap.delete(evicted);
30
+ }
31
+
32
+ this.metadataMap.set(rtpTimestamp, metadata);
33
+ }
34
+
35
+ lookupMetadata(rtpTimestamp: number): PacketTrailerMetadata | undefined {
36
+ return this.metadataMap.get(rtpTimestamp);
37
+ }
38
+
39
+ dispose() {
40
+ this.metadataMap.clear();
41
+ this.activeSsrc = 0;
42
+ }
43
+ }
@@ -1,3 +1,4 @@
1
+ import type { PacketTrailerMetadata } from '../../packetTrailer/types';
1
2
  import { debounce } from '../debounce';
2
3
  import { TrackEvent } from '../events';
3
4
  import type { VideoReceiverStats } from '../stats';
@@ -6,6 +7,7 @@ import CriticalTimers from '../timers';
6
7
  import type { LoggerOptions } from '../types';
7
8
  import type { ObservableMediaElement } from '../utils';
8
9
  import { getDevicePixelRatio, getIntersectionObserver, getResizeObserver, isWeb } from '../utils';
10
+ import type { PacketTrailerExtractor } from './PacketTrailerExtractor';
9
11
  import RemoteTrack from './RemoteTrack';
10
12
  import { Track, attachToElement, detachTrack } from './Track';
11
13
  import type { AdaptiveStreamSettings } from './types';
@@ -23,6 +25,9 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
23
25
 
24
26
  private lastDimensions?: Track.Dimensions;
25
27
 
28
+ /** @internal */
29
+ packetTrailerExtractor?: PacketTrailerExtractor;
30
+
26
31
  constructor(
27
32
  mediaTrack: MediaStreamTrack,
28
33
  sid: string,
@@ -38,6 +43,23 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
38
43
  return this.adaptiveStreamSettings !== undefined;
39
44
  }
40
45
 
46
+ /**
47
+ * Look up frame-level metadata for a given RTP timestamp.
48
+ * Use with the `TrackEvent.TimeSyncUpdate` event to correlate displayed frames
49
+ * with their capture-time metadata.
50
+ *
51
+ * Requires the room to be configured with the `packetTrailer` worker option
52
+ * and the publishing track to have packet trailer features enabled.
53
+ *
54
+ */
55
+ lookupFrameMetadata({
56
+ rtpTimestamp,
57
+ }: {
58
+ rtpTimestamp: number;
59
+ }): PacketTrailerMetadata | undefined {
60
+ return this.packetTrailerExtractor?.lookupMetadata(rtpTimestamp);
61
+ }
62
+
41
63
  override setStreamState(value: Track.StreamState) {
42
64
  super.setStreamState(value);
43
65
  this.log.debug('setStreamState', value);
@@ -140,12 +162,11 @@ export default class RemoteVideoTrack extends RemoteTrack<Track.Kind.Video> {
140
162
  detach(): HTMLMediaElement[];
141
163
  detach(element: HTMLMediaElement): HTMLMediaElement;
142
164
  detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[] {
143
- let detachedElements: HTMLMediaElement[] = [];
144
165
  if (element) {
145
166
  this.stopObservingElement(element);
146
167
  return super.detach(element);
147
168
  }
148
- detachedElements = super.detach();
169
+ const detachedElements = super.detach();
149
170
 
150
171
  for (const e of detachedElements) {
151
172
  this.stopObservingElement(e);
@@ -537,6 +537,6 @@ export type TrackEventCallbacks = {
537
537
  trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
538
538
  audioTrackFeatureUpdate: (track: any, feature: AudioTrackFeature, enabled: boolean) => void;
539
539
  timeSyncUpdate: (update: { timestamp: number; rtpTimestamp: number }) => void;
540
- preConnectBufferFlushed: (buffer: Uint8Array[]) => void;
540
+ preConnectBufferFlushed: (buffer: NonSharedUint8Array[]) => void;
541
541
  cpuConstrained: () => void;
542
542
  };
@@ -111,10 +111,6 @@ export async function createLocalTracks(
111
111
  return await Promise.all(
112
112
  stream.getTracks().map(async (mediaStreamTrack) => {
113
113
  const isAudio = mediaStreamTrack.kind === 'audio';
114
- let trackOptions = isAudio ? opts!.audio : opts!.video;
115
- if (typeof trackOptions === 'boolean' || !trackOptions) {
116
- trackOptions = {};
117
- }
118
114
  let trackConstraints: MediaTrackConstraints | undefined;
119
115
  const conOrBool = isAudio ? constraints.audio : constraints.video;
120
116
  if (typeof conOrBool !== 'boolean') {
@@ -1,3 +1,4 @@
1
+ import type { PacketTrailerPublishOptions } from '../../packetTrailer/types';
1
2
  import type { Track } from './Track';
2
3
  import type {
3
4
  AudioProcessorOptions,
@@ -128,6 +129,16 @@ export interface TrackPublishDefaults {
128
129
  * Defaults to false.
129
130
  */
130
131
  preConnectBuffer?: boolean;
132
+
133
+ /**
134
+ * Packet trailer metadata to append to published video frames.
135
+ *
136
+ * Requires either room-level packet trailer worker configuration or E2EE,
137
+ * because encoded frame transforms are used to write the trailer.
138
+ *
139
+ * @experimental
140
+ */
141
+ packetTrailer?: PacketTrailerPublishOptions;
131
142
  }
132
143
 
133
144
  /**
@@ -51,7 +51,7 @@ export class LocalTrackRecorder<T extends LocalTrack> extends RecorderBase {
51
51
  start: (controller) => {
52
52
  streamController = controller;
53
53
  dataListener = async (event: BlobEvent) => {
54
- let data: Uint8Array;
54
+ let data: NonSharedUint8Array;
55
55
 
56
56
  if (event.data.arrayBuffer) {
57
57
  const arrayBuffer = await event.data.arrayBuffer();
@@ -280,7 +280,10 @@ export function getLogContextFromTrack(track: Track | TrackPublication): Record<
280
280
  }
281
281
 
282
282
  export function supportsSynchronizationSources(): boolean {
283
- return typeof RTCRtpReceiver !== 'undefined' && 'getSynchronizationSources' in RTCRtpReceiver;
283
+ return (
284
+ typeof RTCRtpReceiver !== 'undefined' &&
285
+ typeof RTCRtpReceiver.prototype.getSynchronizationSources === 'function'
286
+ );
284
287
  }
285
288
 
286
289
  export function diffAttributes(
@@ -1,5 +1,6 @@
1
+ import { ClientInfo_Capability } from '@livekit/protocol';
1
2
  import { describe, expect, it } from 'vitest';
2
- import { extractMaxAgeFromRequestHeaders, splitUtf8, toWebsocketUrl } from './utils';
3
+ import { extractMaxAgeFromRequestHeaders, getClientInfo, splitUtf8, toWebsocketUrl } from './utils';
3
4
 
4
5
  describe('toWebsocketUrl', () => {
5
6
  it('leaves wss urls alone', () => {
@@ -15,6 +16,18 @@ describe('toWebsocketUrl', () => {
15
16
  });
16
17
  });
17
18
 
19
+ describe('getClientInfo', () => {
20
+ it('does not advertise packet trailer capability by default', () => {
21
+ expect(getClientInfo().capabilities).toEqual([]);
22
+ });
23
+
24
+ it('advertises packet trailer capability when provided', () => {
25
+ expect(getClientInfo([ClientInfo_Capability.CAP_PACKET_TRAILER]).capabilities).toEqual([
26
+ ClientInfo_Capability.CAP_PACKET_TRAILER,
27
+ ]);
28
+ });
29
+ });
30
+
18
31
  describe('splitUtf8', () => {
19
32
  it('splits a string into chunks of the given size', () => {
20
33
  expect(splitUtf8('hello world', 5)).toEqual([
package/src/room/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ChatMessage as ChatMessageModel,
3
3
  ClientInfo,
4
+ ClientInfo_Capability,
4
5
  ClientInfo_SDK,
5
6
  DisconnectReason,
6
7
  Transcription as TranscriptionModel,
@@ -9,7 +10,7 @@ import { type Throws } from '@livekit/throws-transformer/throws';
9
10
  import TypedPromise from '../utils/TypedPromise';
10
11
  import { getBrowser } from '../utils/browserParser';
11
12
  import type { BrowserDetails } from '../utils/browserParser';
12
- import { protocolVersion, version } from '../version';
13
+ import { clientProtocol, protocolVersion, version } from '../version';
13
14
  import { type ConnectionError, ConnectionErrorReason } from './errors';
14
15
  import type LocalParticipant from './participant/LocalParticipant';
15
16
  import type Participant from './participant/Participant';
@@ -179,6 +180,18 @@ export function isChromiumBased(): boolean {
179
180
  return !!browser && browser.name === 'Chrome' && browser.os !== 'iOS';
180
181
  }
181
182
 
183
+ export function isScriptTransformSupportedForWorker(): boolean {
184
+ // Chrome occasionally throws an `InvalidState` error when using script transforms directly after introducing this API in 141.
185
+ // Disabling it for Chrome based browsers until the API has stabilized.
186
+ // @ts-ignore
187
+ return (
188
+ typeof window !== 'undefined' &&
189
+ // @ts-ignore
190
+ typeof window.RTCRtpScriptTransform !== 'undefined' &&
191
+ !isChromiumBased()
192
+ );
193
+ }
194
+
182
195
  export function isSafari(): boolean {
183
196
  return getBrowser()?.name === 'Safari';
184
197
  }
@@ -364,10 +377,12 @@ export interface ObservableMediaElement extends HTMLMediaElement {
364
377
  handleVisibilityChanged: (entry: IntersectionObserverEntry) => void;
365
378
  }
366
379
 
367
- export function getClientInfo(): ClientInfo {
380
+ export function getClientInfo(capabilities?: ClientInfo_Capability[]): ClientInfo {
368
381
  const info = new ClientInfo({
382
+ capabilities,
369
383
  sdk: ClientInfo_SDK.JS,
370
384
  protocol: protocolVersion,
385
+ clientProtocol,
371
386
  version,
372
387
  });
373
388
 
@@ -740,12 +755,12 @@ export function isRemoteParticipant(p: Participant): p is RemoteParticipant {
740
755
  return !p.isLocal;
741
756
  }
742
757
 
743
- export function splitUtf8(s: string, n: number): Uint8Array[] {
758
+ export function splitUtf8(s: string, n: number): NonSharedUint8Array[] {
744
759
  if (n < 4) {
745
760
  throw new Error('n must be at least 4 due to utf8 encoding rules');
746
761
  }
747
762
  // adapted from https://stackoverflow.com/a/6043797
748
- const result: Uint8Array[] = [];
763
+ const result: NonSharedUint8Array[] = [];
749
764
  let encoded = new TextEncoder().encode(s);
750
765
  while (encoded.length > n) {
751
766
  let k = n;
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  // @ts-ignore
3
2
  export default class MockMediaStreamTrack implements MediaStreamTrack {
4
3
  contentHint: string = '';
@@ -0,0 +1,6 @@
1
+ // As of TS 5.7, `Uint8Array` is generic over its backing buffer (`Uint8Array<ArrayBufferLike>`),
2
+ // which includes `SharedArrayBuffer`. Many Web APIs (WebCrypto, structured clone, RTCDataChannel)
3
+ // only accept the non-shared variant `Uint8Array<ArrayBuffer>`. Using `ReturnType<typeof Uint8Array.from>`
4
+ // resolves to that non-shared variant on TS versions that support the generic, while remaining
5
+ // equivalent to plain `Uint8Array` on older versions — so this alias works across the range we support.
6
+ type NonSharedUint8Array = ReturnType<typeof Uint8Array.from>;
@@ -1,5 +1,5 @@
1
1
  export interface DataPacketItem {
2
- data: Uint8Array;
2
+ data: NonSharedUint8Array;
3
3
  sequence: number;
4
4
  }
5
5