livekit-client 2.20.2 → 2.22.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 (142) hide show
  1. package/README.md +10 -10
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +6 -1
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2588 -1056
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +6 -1
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/room/PCTransport.d.ts +49 -2
  17. package/dist/src/room/PCTransport.d.ts.map +1 -1
  18. package/dist/src/room/RTCEngine.d.ts +48 -65
  19. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  20. package/dist/src/room/Room.d.ts +6 -0
  21. package/dist/src/room/Room.d.ts.map +1 -1
  22. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  23. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  24. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  25. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  26. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  27. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  28. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  29. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  30. package/dist/src/room/data-channel/types.d.ts +12 -0
  31. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  32. package/dist/src/room/data-stream/compression.d.ts +49 -0
  33. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  34. package/dist/src/room/data-stream/constants.d.ts +18 -0
  35. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  36. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  37. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  39. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  40. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  41. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  42. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  43. package/dist/src/room/errors.d.ts +3 -1
  44. package/dist/src/room/errors.d.ts.map +1 -1
  45. package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
  46. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  47. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  48. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  49. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  50. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  51. package/dist/src/room/token-source/TokenSource.d.ts +19 -10
  52. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  53. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  54. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/RemoteTrack.d.ts +3 -0
  56. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/Track.d.ts.map +1 -1
  58. package/dist/src/room/types.d.ts +17 -0
  59. package/dist/src/room/types.d.ts.map +1 -1
  60. package/dist/src/room/utils.d.ts +28 -0
  61. package/dist/src/room/utils.d.ts.map +1 -1
  62. package/dist/src/test/promiseState.d.ts +12 -0
  63. package/dist/src/test/promiseState.d.ts.map +1 -0
  64. package/dist/src/test/signalToken.d.ts.map +1 -1
  65. package/dist/src/utils/AsyncQueue.d.ts +3 -3
  66. package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
  67. package/dist/src/version.d.ts +5 -1
  68. package/dist/src/version.d.ts.map +1 -1
  69. package/dist/ts4.2/index.d.ts +1 -1
  70. package/dist/ts4.2/room/PCTransport.d.ts +49 -2
  71. package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
  72. package/dist/ts4.2/room/Room.d.ts +6 -0
  73. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  74. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  75. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  76. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  77. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  78. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  79. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  80. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  81. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  82. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  83. package/dist/ts4.2/room/errors.d.ts +3 -1
  84. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
  85. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  86. package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
  87. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  88. package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
  89. package/dist/ts4.2/room/types.d.ts +17 -0
  90. package/dist/ts4.2/room/utils.d.ts +28 -0
  91. package/dist/ts4.2/test/promiseState.d.ts +12 -0
  92. package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
  93. package/dist/ts4.2/version.d.ts +5 -1
  94. package/package.json +14 -13
  95. package/src/api/SignalClient.e2e.test.ts +16 -9
  96. package/src/e2ee/utils.ts +1 -1
  97. package/src/index.ts +2 -0
  98. package/src/room/PCTransport.test.ts +243 -1
  99. package/src/room/PCTransport.ts +181 -80
  100. package/src/room/RTCEngine.test.ts +397 -101
  101. package/src/room/RTCEngine.ts +244 -545
  102. package/src/room/Room.test.ts +138 -4
  103. package/src/room/Room.ts +92 -25
  104. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  105. package/src/room/data-channel/DataChannelManager.ts +237 -0
  106. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  107. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  108. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  109. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  110. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  111. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  112. package/src/room/data-channel/types.ts +30 -0
  113. package/src/room/data-stream/compression.ts +117 -0
  114. package/src/room/data-stream/constants.ts +18 -0
  115. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  116. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  117. package/src/room/data-stream/incoming/StreamReader.ts +9 -3
  118. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  119. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  120. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  121. package/src/room/errors.ts +7 -2
  122. package/src/room/participant/LocalParticipant.ts +13 -0
  123. package/src/room/participant/RemoteParticipant.ts +19 -5
  124. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  125. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  126. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  127. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  128. package/src/room/token-source/TokenSource.ts +25 -12
  129. package/src/room/track/LocalVideoTrack.test.ts +105 -2
  130. package/src/room/track/LocalVideoTrack.ts +36 -10
  131. package/src/room/track/RemoteTrack.test.ts +144 -0
  132. package/src/room/track/RemoteTrack.ts +39 -12
  133. package/src/room/track/Track.ts +2 -1
  134. package/src/room/types.ts +18 -0
  135. package/src/room/utils.test.ts +71 -2
  136. package/src/room/utils.ts +119 -0
  137. package/src/test/promiseState.ts +23 -0
  138. package/src/test/signalServerSetup.ts +2 -1
  139. package/src/test/signalToken.ts +17 -13
  140. package/src/type-polyfills/header-extensions.d.ts +13 -0
  141. package/src/utils/AsyncQueue.ts +3 -3
  142. package/src/version.ts +5 -1
@@ -0,0 +1,87 @@
1
+ import {
2
+ DataPacket,
3
+ DataStream_ByteHeader,
4
+ DataStream_CompressionType,
5
+ DataStream_Header,
6
+ DataStream_OperationType,
7
+ DataStream_TextHeader,
8
+ } from '@livekit/protocol';
9
+ import type { ByteStreamInfo, StreamTextOptions, TextStreamInfo } from '../../types';
10
+ import { numberToBigInt } from '../../utils';
11
+
12
+ /** The data-streams-v2 wire signals carried directly on the header: the compression flag and the
13
+ * inline single-packet payload. Both used to live in reserved header attributes; they are now
14
+ * first-class protobuf fields on `DataStream.Header`. */
15
+ export interface StreamHeaderV2Fields {
16
+ /** Compression applied to the inline/chunked payload. Defaults to `NONE` when omitted. */
17
+ compression?: DataStream_CompressionType;
18
+ /** The full payload smuggled into the header for single-packet (inline) sends. */
19
+ inlineContent?: Uint8Array;
20
+ }
21
+
22
+ /** Builds the `DataStream_Header` for a text stream from its info and stream options. */
23
+ export function buildTextStreamHeader(
24
+ info: TextStreamInfo,
25
+ options?: Pick<StreamTextOptions, 'version' | 'replyToStreamId' | 'type'>,
26
+ v2?: StreamHeaderV2Fields,
27
+ ): DataStream_Header {
28
+ return new DataStream_Header({
29
+ streamId: info.id,
30
+ mimeType: info.mimeType,
31
+ topic: info.topic,
32
+ timestamp: numberToBigInt(info.timestamp),
33
+ totalLength: numberToBigInt(info.size),
34
+ attributes: info.attributes,
35
+ compression: v2?.compression ?? DataStream_CompressionType.NONE,
36
+ inlineContent: v2?.inlineContent,
37
+ contentHeader: {
38
+ case: 'textHeader',
39
+ value: new DataStream_TextHeader({
40
+ version: options?.version,
41
+ attachedStreamIds: info.attachedStreamIds,
42
+ replyToStreamId: options?.replyToStreamId,
43
+ operationType:
44
+ options?.type === 'update'
45
+ ? DataStream_OperationType.UPDATE
46
+ : DataStream_OperationType.CREATE,
47
+ }),
48
+ },
49
+ });
50
+ }
51
+
52
+ /** Builds the `DataStream_Header` for a byte stream from its info. */
53
+ export function buildByteStreamHeader(
54
+ info: ByteStreamInfo,
55
+ v2?: StreamHeaderV2Fields,
56
+ ): DataStream_Header {
57
+ return new DataStream_Header({
58
+ streamId: info.id,
59
+ mimeType: info.mimeType,
60
+ topic: info.topic,
61
+ timestamp: numberToBigInt(info.timestamp),
62
+ totalLength: numberToBigInt(info.size),
63
+ attributes: info.attributes,
64
+ compression: v2?.compression ?? DataStream_CompressionType.NONE,
65
+ inlineContent: v2?.inlineContent,
66
+ contentHeader: {
67
+ case: 'byteHeader',
68
+ value: new DataStream_ByteHeader({
69
+ name: info.name,
70
+ }),
71
+ },
72
+ });
73
+ }
74
+
75
+ /** Wraps a `DataStream_Header` in a `DataPacket` ready to be sent over a data channel. */
76
+ export function createStreamHeaderPacket(
77
+ header: DataStream_Header,
78
+ destinationIdentities?: Array<string>,
79
+ ): DataPacket {
80
+ return new DataPacket({
81
+ destinationIdentities,
82
+ value: {
83
+ case: 'streamHeader',
84
+ value: header,
85
+ },
86
+ });
87
+ }
@@ -248,8 +248,7 @@ export class PublishTrackError extends LivekitError {
248
248
  }
249
249
 
250
250
  export type RequestErrorReason =
251
- | Exclude<RequestResponse_Reason, RequestResponse_Reason.OK>
252
- | 'TimeoutError';
251
+ Exclude<RequestResponse_Reason, RequestResponse_Reason.OK> | 'TimeoutError';
253
252
 
254
253
  export class SignalRequestError extends LivekitReasonedError<RequestErrorReason> {
255
254
  readonly name = 'SignalRequestError';
@@ -287,6 +286,12 @@ export enum DataStreamErrorReason {
287
286
 
288
287
  // Encryption type mismatch.
289
288
  EncryptionTypeMismatch = 8,
289
+
290
+ // The serialized stream header packet (driven mainly by attributes) exceeds the MTU budget.
291
+ HeaderTooLarge = 9,
292
+
293
+ // The stream's (decompressed) payload exceeds the maximum allowed size.
294
+ PayloadTooLarge = 10,
290
295
  }
291
296
 
292
297
  export class DataStreamError extends LivekitReasonedError<DataStreamErrorReason> {
@@ -82,8 +82,10 @@ import {
82
82
  sourceToKind,
83
83
  } from '../track/utils';
84
84
  import {
85
+ type ByteStreamInfo,
85
86
  type ChatMessage,
86
87
  type DataPublishOptions,
88
+ type SendBytesOptions,
87
89
  type SendFileOptions,
88
90
  type SendTextOptions,
89
91
  type StreamBytesOptions,
@@ -1851,6 +1853,17 @@ export default class LocalParticipant extends Participant {
1851
1853
  return this.roomOutgoingDataStreamManager.sendFile(file, options);
1852
1854
  }
1853
1855
 
1856
+ /**
1857
+ * Sends the given bytes to participants in the room via the data channel.
1858
+ * For files, consider using {@link sendFile}; for longer/incremental payloads, {@link streamBytes}.
1859
+ *
1860
+ * @param bytes The byte payload
1861
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1862
+ */
1863
+ async sendBytes(bytes: Uint8Array, options?: SendBytesOptions): Promise<ByteStreamInfo> {
1864
+ return this.roomOutgoingDataStreamManager.sendBytes(bytes, options);
1865
+ }
1866
+
1854
1867
  /**
1855
1868
  * Stream bytes incrementally to participants in the room via the data channel.
1856
1869
  * For sending files, consider using {@link sendFile} instead.
@@ -1,8 +1,9 @@
1
- import type {
2
- ParticipantInfo,
3
- SubscriptionError,
4
- UpdateSubscription,
5
- UpdateTrackSettings,
1
+ import {
2
+ ClientInfo_Capability,
3
+ type ParticipantInfo,
4
+ type SubscriptionError,
5
+ type UpdateSubscription,
6
+ type UpdateTrackSettings,
6
7
  } from '@livekit/protocol';
7
8
  import type { SignalClient } from '../../api/SignalClient';
8
9
  import { DeferrableMap } from '../../utils/deferrable-map';
@@ -47,6 +48,16 @@ export default class RemoteParticipant extends Participant {
47
48
  **/
48
49
  clientProtocol: number;
49
50
 
51
+ /** The client capabilities the remote participant advertises (e.g. deflate-raw compression
52
+ * support). Used to decide which peer-to-peer features can be used when sending to them.
53
+ *
54
+ * Differs from clientProtocol in that these are truely optional "additions" which can be used
55
+ * or not depending on client specific attributes rather than protocol level invariants.
56
+ *
57
+ * @internal
58
+ **/
59
+ capabilities: Array<ClientInfo_Capability>;
60
+
50
61
  private volumeMap: Map<Track.Source, number>;
51
62
 
52
63
  private audioOutput?: AudioOutputOptions;
@@ -72,6 +83,7 @@ export default class RemoteParticipant extends Participant {
72
83
  return new RemoteDataTrack(info, manager, { publisherIdentity: pi.identity });
73
84
  }),
74
85
  pi.clientProtocol,
86
+ pi.capabilities,
75
87
  );
76
88
  }
77
89
 
@@ -95,6 +107,7 @@ export default class RemoteParticipant extends Participant {
95
107
  kind: ParticipantKind = ParticipantKind.STANDARD,
96
108
  remoteDataTracks: Array<RemoteDataTrack> = [],
97
109
  clientProtocol: number = CLIENT_PROTOCOL_DEFAULT,
110
+ capabilities: Array<ClientInfo_Capability> = [],
98
111
  ) {
99
112
  super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
100
113
  this.signalClient = signalClient;
@@ -108,6 +121,7 @@ export default class RemoteParticipant extends Participant {
108
121
  );
109
122
  this.volumeMap = new Map();
110
123
  this.clientProtocol = clientProtocol;
124
+ this.capabilities = capabilities;
111
125
  }
112
126
 
113
127
  protected addTrackPublication(publication: RemoteTrackPublication) {
@@ -16,6 +16,8 @@ describe('RpcClientManager', () => {
16
16
  const outgoingDataStreamManager = new OutgoingDataStreamManager(
17
17
  {} as unknown as RTCEngine,
18
18
  log,
19
+ (_identity) => CLIENT_PROTOCOL_DEFAULT,
20
+ () => [],
19
21
  );
20
22
 
21
23
  rpcClientManager = new RpcClientManager(
@@ -132,19 +134,13 @@ describe('RpcClientManager', () => {
132
134
 
133
135
  describe('v2 -> v2', () => {
134
136
  let rpcClientManager: RpcClientManager;
135
- let mockStreamTextWriter: {
136
- write: ReturnType<typeof vi.fn>;
137
- close: ReturnType<typeof vi.fn>;
138
- };
137
+ let sendTextMock: ReturnType<typeof vi.fn>;
139
138
  let mockOutgoingDataStreamManager: OutgoingDataStreamManager;
140
139
 
141
140
  beforeEach(() => {
142
- mockStreamTextWriter = {
143
- write: vi.fn().mockResolvedValue(undefined),
144
- close: vi.fn().mockResolvedValue(undefined),
145
- };
141
+ sendTextMock = vi.fn().mockResolvedValue(undefined);
146
142
  mockOutgoingDataStreamManager = {
147
- streamText: vi.fn().mockResolvedValue(mockStreamTextWriter),
143
+ sendText: sendTextMock,
148
144
  } as unknown as OutgoingDataStreamManager;
149
145
 
150
146
  rpcClientManager = new RpcClientManager(
@@ -171,7 +167,8 @@ describe('RpcClientManager', () => {
171
167
  });
172
168
 
173
169
  // Verify the data stream was used with correct attributes
174
- expect(mockOutgoingDataStreamManager.streamText).toHaveBeenCalledWith(
170
+ expect(mockOutgoingDataStreamManager.sendText).toHaveBeenCalledWith(
171
+ 'request-payload',
175
172
  expect.objectContaining({
176
173
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
177
174
  destinationIdentities: ['destination-identity'],
@@ -182,8 +179,6 @@ describe('RpcClientManager', () => {
182
179
  }),
183
180
  }),
184
181
  );
185
- expect(mockStreamTextWriter.write).toHaveBeenCalledWith('request-payload');
186
- expect(mockStreamTextWriter.close).toHaveBeenCalled();
187
182
 
188
183
  // No packet should have been emitted
189
184
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
@@ -213,7 +208,8 @@ describe('RpcClientManager', () => {
213
208
  });
214
209
 
215
210
  // Verify the data stream was used with correct attributes
216
- expect(mockOutgoingDataStreamManager.streamText).toHaveBeenCalledWith(
211
+ expect(mockOutgoingDataStreamManager.sendText).toHaveBeenCalledWith(
212
+ longPayload,
217
213
  expect.objectContaining({
218
214
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
219
215
  destinationIdentities: ['destination-identity'],
@@ -224,8 +220,6 @@ describe('RpcClientManager', () => {
224
220
  }),
225
221
  }),
226
222
  );
227
- expect(mockStreamTextWriter.write).toHaveBeenCalledWith(longPayload);
228
- expect(mockStreamTextWriter.close).toHaveBeenCalled();
229
223
 
230
224
  // No packet should have been emitted
231
225
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
@@ -337,14 +331,14 @@ describe('RpcClientManager', () => {
337
331
  });
338
332
 
339
333
  it('should not drop ack and response that arrive before publish completes', async () => {
340
- // Hold the publish path open by blocking writer.close() until we explicitly resolve it.
341
- let resolveClose!: () => void;
342
- const closeBlocked = new Promise<void>((resolve) => {
343
- resolveClose = resolve;
334
+ // Hold the publish path open by blocking sendText() until we explicitly resolve it.
335
+ let resolveSend!: () => void;
336
+ const sendBlocked = new Promise<void>((resolve) => {
337
+ resolveSend = resolve;
344
338
  });
345
- mockStreamTextWriter.close = vi.fn().mockReturnValue(closeBlocked);
339
+ sendTextMock.mockReturnValue(sendBlocked);
346
340
 
347
- // Start performRpc but don't await its return yet. The synchronous prefix runs streamText.
341
+ // Start performRpc but don't await its return yet. The synchronous prefix runs sendText.
348
342
  const performRpcPromise = rpcClientManager.performRpc({
349
343
  destinationIdentity: 'destination-identity',
350
344
  method: 'test-method',
@@ -352,11 +346,10 @@ describe('RpcClientManager', () => {
352
346
  responseTimeout: 200,
353
347
  });
354
348
 
355
- // streamText was called synchronously; pull the request id out of the attributes.
356
- const streamTextCalls = (mockOutgoingDataStreamManager.streamText as ReturnType<typeof vi.fn>)
357
- .mock.calls;
358
- expect(streamTextCalls.length).toBe(1);
359
- const requestId = streamTextCalls[0][0].attributes[RpcRequestAttrs.RPC_REQUEST_ID];
349
+ // sendText was called synchronously; pull the request id out of the attributes.
350
+ const sendTextCalls = sendTextMock.mock.calls;
351
+ expect(sendTextCalls.length).toBe(1);
352
+ const requestId = sendTextCalls[0][1].attributes[RpcRequestAttrs.RPC_REQUEST_ID];
360
353
 
361
354
  // Deliver ack and response BEFORE close() unblocks - the publish has not yet returned.
362
355
  rpcClientManager.handleIncomingRpcAck(requestId);
@@ -367,7 +360,7 @@ describe('RpcClientManager', () => {
367
360
  );
368
361
 
369
362
  // Now allow the publish path to complete.
370
- resolveClose();
363
+ resolveSend();
371
364
 
372
365
  const [, completionPromise] = await performRpcPromise;
373
366
  await expect(completionPromise).resolves.toStrictEqual('response-payload');
@@ -145,7 +145,7 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
145
145
  ) {
146
146
  if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
147
147
  // Send payload as a data stream - a "version 2" rpc request.
148
- const writer = await this.outgoingDataStreamManager.streamText({
148
+ await this.outgoingDataStreamManager.sendText(payload, {
149
149
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
150
150
  destinationIdentities: [destinationIdentity],
151
151
  attributes: {
@@ -155,9 +155,6 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
155
155
  [RpcRequestAttrs.RPC_REQUEST_VERSION]: `${RPC_VERSION_V2}`,
156
156
  },
157
157
  });
158
-
159
- await writer.write(payload);
160
- await writer.close();
161
158
  return;
162
159
  }
163
160
 
@@ -2,7 +2,11 @@ import { RpcRequest } from '@livekit/protocol';
2
2
  import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import log from '../../../logger';
4
4
  import { subscribeToEvents } from '../../../utils/subscribeToEvents';
5
- import { CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT } from '../../../version';
5
+ import {
6
+ CLIENT_PROTOCOL_DATA_STREAM_RPC,
7
+ CLIENT_PROTOCOL_DATA_STREAM_V2,
8
+ CLIENT_PROTOCOL_DEFAULT,
9
+ } from '../../../version';
6
10
  import type RTCEngine from '../../RTCEngine';
7
11
  import OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
8
12
  import { RPC_RESPONSE_DATA_STREAM_TOPIC, RpcError, RpcRequestAttrs } from '../utils';
@@ -17,6 +21,8 @@ describe('RpcServerManager', () => {
17
21
  const outgoingDataStreamManager = new OutgoingDataStreamManager(
18
22
  {} as unknown as RTCEngine,
19
23
  log,
24
+ (_identity) => CLIENT_PROTOCOL_DEFAULT,
25
+ () => [],
20
26
  );
21
27
 
22
28
  rpcServerManager = new RpcServerManager(
@@ -184,22 +190,17 @@ describe('RpcServerManager', () => {
184
190
  describe('v2 -> v2', () => {
185
191
  let rpcServerManager: RpcServerManager;
186
192
  let outgoingDataStreamManager: OutgoingDataStreamManager;
187
- let mockStreamTextWriter: {
188
- write: ReturnType<typeof vi.fn>;
189
- close: ReturnType<typeof vi.fn>;
190
- };
191
193
 
192
194
  beforeEach(() => {
193
- outgoingDataStreamManager = new OutgoingDataStreamManager({} as unknown as RTCEngine, log);
194
-
195
- mockStreamTextWriter = {
196
- write: vi.fn().mockResolvedValue(undefined),
197
- close: vi.fn().mockResolvedValue(undefined),
198
- };
199
- vi.spyOn(outgoingDataStreamManager, 'streamText').mockResolvedValue(
200
- mockStreamTextWriter as any,
195
+ outgoingDataStreamManager = new OutgoingDataStreamManager(
196
+ {} as unknown as RTCEngine,
197
+ log,
198
+ (_identity) => CLIENT_PROTOCOL_DATA_STREAM_V2,
199
+ () => [],
201
200
  );
202
201
 
202
+ vi.spyOn(outgoingDataStreamManager, 'sendText').mockResolvedValue(undefined as any);
203
+
203
204
  rpcServerManager = new RpcServerManager(
204
205
  log,
205
206
  outgoingDataStreamManager,
@@ -243,15 +244,14 @@ describe('RpcServerManager', () => {
243
244
 
244
245
  // The response should have been sent via data stream, not packet
245
246
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
246
- expect(outgoingDataStreamManager.streamText).toHaveBeenCalledWith(
247
+ expect(outgoingDataStreamManager.sendText).toHaveBeenCalledWith(
248
+ 'response payload',
247
249
  expect.objectContaining({
248
250
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
249
251
  destinationIdentities: ['caller-identity'],
250
252
  attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
251
253
  }),
252
254
  );
253
- expect(mockStreamTextWriter.write).toHaveBeenCalledWith('response payload');
254
- expect(mockStreamTextWriter.close).toHaveBeenCalled();
255
255
  });
256
256
 
257
257
  it('should receive a large rpc request (> 15kb) and send a large response via data stream from a participant', async () => {
@@ -277,15 +277,14 @@ describe('RpcServerManager', () => {
277
277
 
278
278
  // The response should have been sent via data stream, not packet
279
279
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
280
- expect(outgoingDataStreamManager.streamText).toHaveBeenCalledWith(
280
+ expect(outgoingDataStreamManager.sendText).toHaveBeenCalledWith(
281
+ new Array(20_000).fill('B').join(''),
281
282
  expect.objectContaining({
282
283
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
283
284
  destinationIdentities: ['caller-identity'],
284
285
  attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
285
286
  }),
286
287
  );
287
- expect(mockStreamTextWriter.write).toHaveBeenCalledWith(new Array(20_000).fill('B').join(''));
288
- expect(mockStreamTextWriter.close).toHaveBeenCalled();
289
288
  });
290
289
 
291
290
  it('should register an RPC method handler', async () => {
@@ -317,7 +316,7 @@ describe('RpcServerManager', () => {
317
316
 
318
317
  // Response goes via data stream, not packet
319
318
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
320
- expect(outgoingDataStreamManager.streamText).toHaveBeenCalled();
319
+ expect(outgoingDataStreamManager.sendText).toHaveBeenCalled();
321
320
  });
322
321
 
323
322
  it('should catch and transform unhandled errors in the RPC method handler', async () => {
@@ -414,7 +413,7 @@ describe('RpcServerManager', () => {
414
413
  const errorResponse = errorEvent.packet.value.value.value.value;
415
414
  expect(errorResponse.code).toStrictEqual(RpcError.ErrorCode.UNSUPPORTED_METHOD);
416
415
 
417
- expect(outgoingDataStreamManager.streamText).not.toHaveBeenCalled();
416
+ expect(outgoingDataStreamManager.sendText).not.toHaveBeenCalled();
418
417
  expect(managerEvents.areThereBufferedEvents('sendDataPacket')).toBe(false);
419
418
  });
420
419
  });
@@ -424,8 +423,10 @@ describe('RpcServerManager', () => {
424
423
  const outgoingDataStreamManager = new OutgoingDataStreamManager(
425
424
  {} as unknown as RTCEngine,
426
425
  log,
426
+ (_identity) => CLIENT_PROTOCOL_DEFAULT,
427
+ () => [],
427
428
  );
428
- const streamTextSpy = vi.spyOn(outgoingDataStreamManager, 'streamText');
429
+ const sendTextSpy = vi.spyOn(outgoingDataStreamManager, 'sendText');
429
430
 
430
431
  const rpcServerManager = new RpcServerManager(
431
432
  log,
@@ -457,7 +458,7 @@ describe('RpcServerManager', () => {
457
458
  assert(ackEvent.packet.value.case === 'rpcAck');
458
459
 
459
460
  // Response should be a v1 RpcResponse packet, not a data stream
460
- expect(streamTextSpy).not.toHaveBeenCalled();
461
+ expect(sendTextSpy).not.toHaveBeenCalled();
461
462
  const responseEvent = await managerEvents.waitFor('sendDataPacket');
462
463
  assert(responseEvent.packet.value.case === 'rpcResponse');
463
464
  const rpcResponse = responseEvent.packet.value.value;
@@ -263,13 +263,11 @@ export default class RpcServerManager extends (EventEmitter as new () => TypedEm
263
263
 
264
264
  if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
265
265
  // Send response as a data stream
266
- const writer = await this.outgoingDataStreamManager.streamText({
266
+ await this.outgoingDataStreamManager.sendText(payload, {
267
267
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
268
268
  destinationIdentities: [destinationIdentity],
269
269
  attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
270
270
  });
271
- await writer.write(payload);
272
- await writer.close();
273
271
  return;
274
272
  }
275
273
 
@@ -213,29 +213,34 @@ class TokenSourceEndpoint extends TokenSourceCached {
213
213
  const body = await response.json();
214
214
  return TokenSourceResponse.fromJson(body, {
215
215
  // NOTE: it could be possible that the response body could contain more fields than just
216
- // what's in TokenSourceResponse depending on the implementation (ie, SandboxTokenServer)
216
+ // what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
217
217
  ignoreUnknownFields: true,
218
218
  });
219
219
  }
220
220
  }
221
221
 
222
- export type SandboxTokenServerOptions = {
222
+ export type DevelopmentTokenServerOptions = {
223
223
  baseUrl?: string;
224
224
  };
225
-
226
- class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
227
- constructor(sandboxId: string, options: SandboxTokenServerOptions) {
225
+ class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
226
+ constructor(tokenServerId: string, options: DevelopmentTokenServerOptions) {
228
227
  const { baseUrl = 'https://cloud-api.livekit.io', ...rest } = options;
229
228
 
230
229
  super(`${baseUrl}/api/v2/sandbox/connection-details`, {
231
230
  ...rest,
232
231
  headers: {
233
- 'X-Sandbox-ID': sandboxId,
232
+ 'X-Sandbox-ID': tokenServerId,
234
233
  },
235
234
  });
236
235
  }
237
236
  }
238
237
 
238
+ /** @deprecated use {@link DevelopmentTokenServerOptions} instead */
239
+ export type SandboxTokenServerOptions = DevelopmentTokenServerOptions;
240
+
241
+ /** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
242
+ class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
243
+
239
244
  export {
240
245
  /** The return type of {@link TokenSource.literal} */
241
246
  type TokenSourceLiteral,
@@ -243,8 +248,9 @@ export {
243
248
  type TokenSourceCustom,
244
249
  /** The return type of {@link TokenSource.endpoint} */
245
250
  type TokenSourceEndpoint,
246
- /** The return type of {@link TokenSource.sandboxTokenServer} */
247
251
  type TokenSourceSandboxTokenServer,
252
+ /** The return type of {@link TokenSource.developmentTokenServer} */
253
+ type TokenSourceDevelopmentTokenServer,
248
254
  decodeTokenPayload,
249
255
  areTokenSourceFetchOptionsEqual,
250
256
  };
@@ -269,22 +275,29 @@ export const TokenSource = {
269
275
  /**
270
276
  * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
271
277
  * the standard endpoint format:
272
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
278
+ * @see https://docs.livekit.io/frontends/build/authentication/endpoint/
273
279
  */
274
280
  endpoint(url: string, options: EndpointOptions = {}) {
275
281
  return new TokenSourceEndpoint(url, options);
276
282
  },
277
283
 
278
284
  /**
279
- * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
285
+ * @deprecated Use {@link TokenSource.developmentTokenServer} instead
286
+ */
287
+ sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
288
+ return new TokenSourceSandboxTokenServer(sandboxId, options);
289
+ },
290
+
291
+ /**
292
+ * TokenSource.developmentTokenServer queries a development token server for credentials,
280
293
  * which supports quick prototyping / getting started types of use cases.
281
294
  *
282
295
  * This token provider is INSECURE and should NOT be used in production.
283
296
  *
284
297
  * For more info:
285
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
298
+ * @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
286
299
  */
287
- sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
288
- return new TokenSourceSandboxTokenServer(sandboxId, options);
300
+ developmentTokenServer(tokenServerId: string, options: DevelopmentTokenServerOptions = {}) {
301
+ return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
289
302
  },
290
303
  };
@@ -1,6 +1,8 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { videoLayersFromEncodings } from './LocalVideoTrack';
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import LocalVideoTrack, { videoLayersFromEncodings } from './LocalVideoTrack';
3
+ import type { SimulcastTrackInfo } from './LocalVideoTrack';
3
4
  import { VideoQuality } from './Track';
5
+ import type { VideoCodec } from './options';
4
6
 
5
7
  describe('videoLayersFromEncodings', () => {
6
8
  it('returns single layer for no encoding', () => {
@@ -131,3 +133,104 @@ describe('videoLayersFromEncodings', () => {
131
133
  expect(layers[2].width).toBe(720);
132
134
  });
133
135
  });
136
+
137
+ function makeSender(label = 'sender', events: string[] = []) {
138
+ let params: RTCRtpSendParameters = {
139
+ encodings: [],
140
+ transactionId: '',
141
+ codecs: [],
142
+ headerExtensions: [],
143
+ rtcp: {},
144
+ };
145
+ return {
146
+ getParameters: () => params,
147
+ // resolves on a later task, like the real setParameters, so that a caller
148
+ // which fails to await it never observes the update
149
+ setParameters: vi.fn(async (next: RTCRtpSendParameters) => {
150
+ events.push(`${label}:start`);
151
+ await new Promise((resolve) => setTimeout(resolve, 0));
152
+ params = next;
153
+ events.push(`${label}:done`);
154
+ }),
155
+ get degradationPreference() {
156
+ return params.degradationPreference;
157
+ },
158
+ };
159
+ }
160
+
161
+ function makeTrack() {
162
+ const track = Object.create(LocalVideoTrack.prototype) as LocalVideoTrack;
163
+ Object.assign(track, {
164
+ log: { debug: vi.fn(), warn: vi.fn() },
165
+ simulcastCodecs: new Map<VideoCodec, SimulcastTrackInfo>(),
166
+ subscribedCodecs: undefined,
167
+ });
168
+ // logContext and mediaStreamTrack are getters we don't set up state for here
169
+ Object.defineProperty(track, 'logContext', { get: () => ({}) });
170
+ Object.defineProperty(track, 'mediaStreamTrack', { get: () => ({ clone: () => ({}) }) });
171
+ return track;
172
+ }
173
+
174
+ describe('setDegradationPreference', () => {
175
+ it('applies the preference to the primary sender', async () => {
176
+ const track = makeTrack();
177
+ const sender = makeSender();
178
+ Object.assign(track, { _sender: sender });
179
+
180
+ await track.setDegradationPreference('maintain-resolution');
181
+
182
+ expect(sender.degradationPreference).toBe('maintain-resolution');
183
+ });
184
+
185
+ it('applies the resolved preference to a backup codec sender', async () => {
186
+ const track = makeTrack();
187
+ const primary = makeSender();
188
+ Object.assign(track, { _sender: primary });
189
+
190
+ await track.setDegradationPreference('maintain-resolution');
191
+
192
+ // the backup codec transceiver is created later, when the server asks for it
193
+ const backupInfo = track.addSimulcastTrack('vp8', [])!;
194
+ const backup = makeSender();
195
+ await track.setSimulcastTrackSender('vp8', backup as unknown as RTCRtpSender);
196
+
197
+ expect(backupInfo.sender).toBe(backup);
198
+ expect(backup.degradationPreference).toBe('maintain-resolution');
199
+ expect(primary.degradationPreference).toBe('maintain-resolution');
200
+ });
201
+
202
+ it('updates every sender when the preference changes after the backup is published', async () => {
203
+ const track = makeTrack();
204
+ const primary = makeSender();
205
+ Object.assign(track, { _sender: primary });
206
+ await track.setDegradationPreference('maintain-framerate');
207
+
208
+ track.addSimulcastTrack('vp8', []);
209
+ const backup = makeSender();
210
+ await track.setSimulcastTrackSender('vp8', backup as unknown as RTCRtpSender);
211
+
212
+ await track.setDegradationPreference('balanced');
213
+
214
+ expect(primary.degradationPreference).toBe('balanced');
215
+ expect(backup.degradationPreference).toBe('balanced');
216
+ });
217
+
218
+ it('applies to senders one at a time', async () => {
219
+ const events: string[] = [];
220
+ const track = makeTrack();
221
+ const primary = makeSender('primary', events);
222
+ Object.assign(track, { _sender: primary });
223
+ track.addSimulcastTrack('vp8', []);
224
+ await track.setSimulcastTrackSender(
225
+ 'vp8',
226
+ makeSender('backup', events) as unknown as RTCRtpSender,
227
+ );
228
+ events.length = 0;
229
+
230
+ await track.setDegradationPreference('balanced');
231
+
232
+ // setParameters is only valid against the most recent getParameters, so the
233
+ // writes must not overlap
234
+ expect(events).toEqual(['primary:start', 'primary:done', 'backup:start', 'backup:done']);
235
+ });
236
+ });