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.
- package/README.md +10 -10
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +6 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2588 -1056
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +6 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +48 -65
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
- package/dist/ts4.2/room/Room.d.ts +6 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +28 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +14 -13
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +397 -101
- package/src/room/RTCEngine.ts +244 -545
- package/src/room/Room.test.ts +138 -4
- package/src/room/Room.ts +92 -25
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +9 -3
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/errors.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +119 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- 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
|
+
}
|
package/src/room/errors.ts
CHANGED
|
@@ -248,8 +248,7 @@ export class PublishTrackError extends LivekitError {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
export type RequestErrorReason =
|
|
251
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
143
|
-
write: vi.fn().mockResolvedValue(undefined),
|
|
144
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
145
|
-
};
|
|
141
|
+
sendTextMock = vi.fn().mockResolvedValue(undefined);
|
|
146
142
|
mockOutgoingDataStreamManager = {
|
|
147
|
-
|
|
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.
|
|
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.
|
|
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
|
|
341
|
-
let
|
|
342
|
-
const
|
|
343
|
-
|
|
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
|
-
|
|
339
|
+
sendTextMock.mockReturnValue(sendBlocked);
|
|
346
340
|
|
|
347
|
-
// Start performRpc but don't await its return yet. The synchronous prefix runs
|
|
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
|
-
//
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
|
222
|
+
export type DevelopmentTokenServerOptions = {
|
|
223
223
|
baseUrl?: string;
|
|
224
224
|
};
|
|
225
|
-
|
|
226
|
-
|
|
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':
|
|
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://
|
|
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
|
-
*
|
|
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://
|
|
298
|
+
* @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
|
|
286
299
|
*/
|
|
287
|
-
|
|
288
|
-
return new
|
|
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
|
+
});
|