livekit-client 2.8.1 → 2.9.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +18 -7
- 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 +1 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +3681 -2961
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +7 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +50 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/StreamReader.d.ts +56 -0
- package/dist/src/room/StreamReader.d.ts.map +1 -0
- package/dist/src/room/StreamWriter.d.ts +16 -0
- package/dist/src/room/StreamWriter.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 +23 -36
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +1 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +1 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -0
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +2 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/facingMode.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +18 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +43 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +26 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/index.d.ts +7 -5
- package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -0
- package/dist/ts4.2/src/room/Room.d.ts +49 -0
- package/dist/ts4.2/src/room/StreamReader.d.ts +56 -0
- package/dist/ts4.2/src/room/StreamWriter.d.ts +25 -0
- package/dist/ts4.2/src/room/errors.d.ts +3 -1
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +23 -36
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +1 -0
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -1
- package/dist/ts4.2/src/room/track/options.d.ts +18 -2
- package/dist/ts4.2/src/room/types.d.ts +43 -0
- package/dist/ts4.2/src/room/utils.d.ts +26 -0
- package/package.json +3 -3
- package/src/api/SignalClient.ts +5 -2
- package/src/e2ee/E2eeManager.ts +2 -2
- package/src/index.ts +17 -1
- package/src/room/RTCEngine.ts +69 -2
- package/src/room/Room.ts +311 -23
- package/src/room/StreamReader.ts +177 -0
- package/src/room/StreamWriter.ts +32 -0
- package/src/room/errors.ts +16 -2
- package/src/room/participant/LocalParticipant.ts +320 -165
- package/src/room/participant/Participant.ts +2 -5
- package/src/room/participant/RemoteParticipant.ts +3 -2
- package/src/room/{participant/LocalParticipant.test.ts → rpc.test.ts} +22 -29
- package/src/room/track/LocalAudioTrack.ts +4 -3
- package/src/room/track/LocalTrack.ts +12 -4
- package/src/room/track/LocalTrackPublication.ts +6 -1
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteTrack.ts +4 -0
- package/src/room/track/RemoteTrackPublication.ts +8 -4
- package/src/room/track/Track.ts +2 -0
- package/src/room/track/TrackPublication.ts +6 -3
- package/src/room/track/create.ts +4 -3
- package/src/room/track/facingMode.ts +2 -1
- package/src/room/track/options.ts +20 -2
- package/src/room/track/utils.ts +1 -1
- package/src/room/types.ts +50 -0
- package/src/room/utils.ts +77 -0
@@ -4,6 +4,12 @@ import {
|
|
4
4
|
Codec,
|
5
5
|
DataPacket,
|
6
6
|
DataPacket_Kind,
|
7
|
+
DataStream_ByteHeader,
|
8
|
+
DataStream_Chunk,
|
9
|
+
DataStream_Header,
|
10
|
+
DataStream_OperationType,
|
11
|
+
DataStream_TextHeader,
|
12
|
+
DataStream_Trailer,
|
7
13
|
Encryption_Type,
|
8
14
|
ParticipantInfo,
|
9
15
|
ParticipantPermission,
|
@@ -23,6 +29,7 @@ import {
|
|
23
29
|
import type { InternalRoomOptions } from '../../options';
|
24
30
|
import { PCTransportState } from '../PCTransportManager';
|
25
31
|
import type RTCEngine from '../RTCEngine';
|
32
|
+
import { TextStreamWriter } from '../StreamWriter';
|
26
33
|
import { defaultVideoCodec } from '../defaults';
|
27
34
|
import {
|
28
35
|
DeviceUnsupportedError,
|
@@ -61,15 +68,27 @@ import {
|
|
61
68
|
mimeTypeToVideoCodecString,
|
62
69
|
screenCaptureToDisplayMediaStreamOptions,
|
63
70
|
} from '../track/utils';
|
64
|
-
import
|
71
|
+
import {
|
72
|
+
type ChatMessage,
|
73
|
+
type DataPublishOptions,
|
74
|
+
type SendTextOptions,
|
75
|
+
type StreamTextOptions,
|
76
|
+
type TextStreamInfo,
|
77
|
+
} from '../types';
|
65
78
|
import {
|
66
79
|
Future,
|
67
80
|
compareVersions,
|
81
|
+
isAudioTrack,
|
68
82
|
isE2EESimulcastSupported,
|
69
83
|
isFireFox,
|
84
|
+
isLocalAudioTrack,
|
85
|
+
isLocalTrack,
|
86
|
+
isLocalVideoTrack,
|
70
87
|
isSVCCodec,
|
71
88
|
isSafari17,
|
89
|
+
isVideoTrack,
|
72
90
|
isWeb,
|
91
|
+
numberToBigInt,
|
73
92
|
sleep,
|
74
93
|
supportsAV1,
|
75
94
|
supportsVP9,
|
@@ -84,6 +103,8 @@ import {
|
|
84
103
|
mediaTrackToLocalTrack,
|
85
104
|
} from './publishUtils';
|
86
105
|
|
106
|
+
const STREAM_CHUNK_SIZE = 15_000;
|
107
|
+
|
87
108
|
export default class LocalParticipant extends Participant {
|
88
109
|
audioTrackPublications: Map<string, LocalTrackPublication>;
|
89
110
|
|
@@ -119,6 +140,8 @@ export default class LocalParticipant extends Participant {
|
|
119
140
|
|
120
141
|
private reconnectFuture?: Future<void>;
|
121
142
|
|
143
|
+
private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>;
|
144
|
+
|
122
145
|
private pendingSignalRequests: Map<
|
123
146
|
number,
|
124
147
|
{
|
@@ -130,8 +153,6 @@ export default class LocalParticipant extends Participant {
|
|
130
153
|
|
131
154
|
private enabledPublishVideoCodecs: Codec[] = [];
|
132
155
|
|
133
|
-
private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
|
134
|
-
|
135
156
|
private pendingAcks = new Map<string, { resolve: () => void; participantIdentity: string }>();
|
136
157
|
|
137
158
|
private pendingResponses = new Map<
|
@@ -143,7 +164,13 @@ export default class LocalParticipant extends Participant {
|
|
143
164
|
>();
|
144
165
|
|
145
166
|
/** @internal */
|
146
|
-
constructor(
|
167
|
+
constructor(
|
168
|
+
sid: string,
|
169
|
+
identity: string,
|
170
|
+
engine: RTCEngine,
|
171
|
+
options: InternalRoomOptions,
|
172
|
+
roomRpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>>,
|
173
|
+
) {
|
147
174
|
super(sid, identity, undefined, undefined, undefined, {
|
148
175
|
loggerName: options.loggerName,
|
149
176
|
loggerContextCb: () => this.engine.logContext,
|
@@ -160,6 +187,7 @@ export default class LocalParticipant extends Participant {
|
|
160
187
|
['audiooutput', 'default'],
|
161
188
|
]);
|
162
189
|
this.pendingSignalRequests = new Map();
|
190
|
+
this.rpcHandlers = roomRpcHandlers;
|
163
191
|
}
|
164
192
|
|
165
193
|
get lastCameraError(): Error | undefined {
|
@@ -251,17 +279,6 @@ export default class LocalParticipant extends Participant {
|
|
251
279
|
|
252
280
|
private handleDataPacket = (packet: DataPacket) => {
|
253
281
|
switch (packet.value.case) {
|
254
|
-
case 'rpcRequest':
|
255
|
-
let rpcRequest = packet.value.value as RpcRequest;
|
256
|
-
this.handleIncomingRpcRequest(
|
257
|
-
packet.participantIdentity,
|
258
|
-
rpcRequest.id,
|
259
|
-
rpcRequest.method,
|
260
|
-
rpcRequest.payload,
|
261
|
-
rpcRequest.responseTimeoutMs,
|
262
|
-
rpcRequest.version,
|
263
|
-
);
|
264
|
-
break;
|
265
282
|
case 'rpcResponse':
|
266
283
|
let rpcResponse = packet.value.value as RpcResponse;
|
267
284
|
let payload: string | null = null;
|
@@ -497,6 +514,7 @@ export default class LocalParticipant extends Participant {
|
|
497
514
|
if (e instanceof Error) {
|
498
515
|
this.emit(ParticipantEvent.MediaDevicesError, e);
|
499
516
|
}
|
517
|
+
this.pendingPublishing.delete(source);
|
500
518
|
throw e;
|
501
519
|
}
|
502
520
|
try {
|
@@ -640,9 +658,9 @@ export default class LocalParticipant extends Participant {
|
|
640
658
|
track.setAudioContext(this.audioContext);
|
641
659
|
}
|
642
660
|
track.mediaStream = stream;
|
643
|
-
if (track
|
661
|
+
if (isAudioTrack(track) && audioProcessor) {
|
644
662
|
await track.setProcessor(audioProcessor);
|
645
|
-
} else if (track
|
663
|
+
} else if (isVideoTrack(track) && videoProcessor) {
|
646
664
|
await track.setProcessor(videoProcessor);
|
647
665
|
}
|
648
666
|
return track;
|
@@ -717,7 +735,7 @@ export default class LocalParticipant extends Participant {
|
|
717
735
|
options?: TrackPublishOptions,
|
718
736
|
isRepublish = false,
|
719
737
|
): Promise<LocalTrackPublication> {
|
720
|
-
if (track
|
738
|
+
if (isLocalAudioTrack(track)) {
|
721
739
|
track.setAudioContext(this.audioContext);
|
722
740
|
}
|
723
741
|
|
@@ -725,7 +743,7 @@ export default class LocalParticipant extends Participant {
|
|
725
743
|
if (this.republishPromise && !isRepublish) {
|
726
744
|
await this.republishPromise;
|
727
745
|
}
|
728
|
-
if (track
|
746
|
+
if (isLocalTrack(track) && this.pendingPublishPromises.has(track)) {
|
729
747
|
await this.pendingPublishPromises.get(track);
|
730
748
|
}
|
731
749
|
let defaultConstraints: MediaTrackConstraints | undefined;
|
@@ -857,7 +875,7 @@ export default class LocalParticipant extends Participant {
|
|
857
875
|
|
858
876
|
private async publish(track: LocalTrack, opts: TrackPublishOptions, isStereo: boolean) {
|
859
877
|
const existingTrackOfSource = Array.from(this.trackPublications.values()).find(
|
860
|
-
(publishedTrack) => track
|
878
|
+
(publishedTrack) => isLocalTrack(track) && publishedTrack.source === track.source,
|
861
879
|
);
|
862
880
|
if (existingTrackOfSource && track.source !== Track.Source.Unknown) {
|
863
881
|
this.log.info(`publishing a second track with the same source: ${track.source}`, {
|
@@ -865,7 +883,7 @@ export default class LocalParticipant extends Participant {
|
|
865
883
|
...getLogContextFromTrack(track),
|
866
884
|
});
|
867
885
|
}
|
868
|
-
if (opts.stopMicTrackOnMute && track
|
886
|
+
if (opts.stopMicTrackOnMute && isAudioTrack(track)) {
|
869
887
|
track.stopOnMute = true;
|
870
888
|
}
|
871
889
|
|
@@ -919,6 +937,7 @@ export default class LocalParticipant extends Participant {
|
|
919
937
|
stereo: isStereo,
|
920
938
|
disableRed: this.isE2EEEnabled || !(opts.red ?? true),
|
921
939
|
stream: opts?.stream,
|
940
|
+
backupCodecPolicy: opts?.backupCodecPolicy,
|
922
941
|
});
|
923
942
|
|
924
943
|
// compute encodings and layers for video
|
@@ -950,7 +969,7 @@ export default class LocalParticipant extends Participant {
|
|
950
969
|
req.width = dims.width;
|
951
970
|
req.height = dims.height;
|
952
971
|
// for svc codecs, disable simulcast and use vp8 for backup codec
|
953
|
-
if (track
|
972
|
+
if (isLocalVideoTrack(track)) {
|
954
973
|
if (isSVCCodec(videoCodec)) {
|
955
974
|
if (track.source === Track.Source.ScreenShare) {
|
956
975
|
// vp9 svc with screenshare cannot encode multiple spatial layers
|
@@ -1036,7 +1055,7 @@ export default class LocalParticipant extends Participant {
|
|
1036
1055
|
|
1037
1056
|
track.sender = await this.engine.createSender(track, opts, encodings);
|
1038
1057
|
|
1039
|
-
if (track
|
1058
|
+
if (isLocalVideoTrack(track)) {
|
1040
1059
|
opts.degradationPreference ??= getDefaultDegradationPreference(track);
|
1041
1060
|
track.setDegradationPreference(opts.degradationPreference);
|
1042
1061
|
}
|
@@ -1126,9 +1145,9 @@ export default class LocalParticipant extends Participant {
|
|
1126
1145
|
trackInfo: ti,
|
1127
1146
|
});
|
1128
1147
|
|
1129
|
-
if (track
|
1148
|
+
if (isLocalVideoTrack(track)) {
|
1130
1149
|
track.startMonitor(this.engine.client);
|
1131
|
-
} else if (track
|
1150
|
+
} else if (isLocalAudioTrack(track)) {
|
1132
1151
|
track.startMonitor();
|
1133
1152
|
}
|
1134
1153
|
|
@@ -1169,7 +1188,7 @@ export default class LocalParticipant extends Participant {
|
|
1169
1188
|
throw new TrackInvalidError('track is not published');
|
1170
1189
|
}
|
1171
1190
|
|
1172
|
-
if (!(track
|
1191
|
+
if (!isLocalVideoTrack(track)) {
|
1173
1192
|
throw new TrackInvalidError('track is not a video track');
|
1174
1193
|
}
|
1175
1194
|
|
@@ -1236,7 +1255,7 @@ export default class LocalParticipant extends Participant {
|
|
1236
1255
|
track: LocalTrack | MediaStreamTrack,
|
1237
1256
|
stopOnUnpublish?: boolean,
|
1238
1257
|
): Promise<LocalTrackPublication | undefined> {
|
1239
|
-
if (track
|
1258
|
+
if (isLocalTrack(track)) {
|
1240
1259
|
const publishPromise = this.pendingPublishPromises.get(track);
|
1241
1260
|
if (publishPromise) {
|
1242
1261
|
this.log.info('awaiting publish promise before attempting to unpublish', {
|
@@ -1303,7 +1322,7 @@ export default class LocalParticipant extends Participant {
|
|
1303
1322
|
if (this.engine.removeTrack(trackSender)) {
|
1304
1323
|
negotiationNeeded = true;
|
1305
1324
|
}
|
1306
|
-
if (track
|
1325
|
+
if (isLocalVideoTrack(track)) {
|
1307
1326
|
for (const [, trackInfo] of track.simulcastCodecs) {
|
1308
1327
|
if (trackInfo.sender) {
|
1309
1328
|
if (this.engine.removeTrack(trackInfo.sender)) {
|
@@ -1349,9 +1368,7 @@ export default class LocalParticipant extends Participant {
|
|
1349
1368
|
tracks: LocalTrack[] | MediaStreamTrack[],
|
1350
1369
|
): Promise<LocalTrackPublication[]> {
|
1351
1370
|
const results = await Promise.all(tracks.map((track) => this.unpublishTrack(track)));
|
1352
|
-
return results.filter(
|
1353
|
-
(track) => track instanceof LocalTrackPublication,
|
1354
|
-
) as LocalTrackPublication[];
|
1371
|
+
return results.filter((track) => !!track);
|
1355
1372
|
}
|
1356
1373
|
|
1357
1374
|
async republishAllTracks(options?: TrackPublishOptions, restartTracks: boolean = true) {
|
@@ -1379,7 +1396,7 @@ export default class LocalParticipant extends Participant {
|
|
1379
1396
|
!track.isMuted &&
|
1380
1397
|
track.source !== Track.Source.ScreenShare &&
|
1381
1398
|
track.source !== Track.Source.ScreenShareAudio &&
|
1382
|
-
(track
|
1399
|
+
(isLocalAudioTrack(track) || isLocalVideoTrack(track)) &&
|
1383
1400
|
!track.isUserProvided
|
1384
1401
|
) {
|
1385
1402
|
// generally we need to restart the track before publishing, often a full reconnect
|
@@ -1453,11 +1470,12 @@ export default class LocalParticipant extends Participant {
|
|
1453
1470
|
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1454
1471
|
}
|
1455
1472
|
|
1456
|
-
async sendChatMessage(text: string): Promise<ChatMessage> {
|
1473
|
+
async sendChatMessage(text: string, options?: SendTextOptions): Promise<ChatMessage> {
|
1457
1474
|
const msg = {
|
1458
1475
|
id: crypto.randomUUID(),
|
1459
1476
|
message: text,
|
1460
1477
|
timestamp: Date.now(),
|
1478
|
+
attachedFiles: options?.attachments,
|
1461
1479
|
} as const satisfies ChatMessage;
|
1462
1480
|
const packet = new DataPacket({
|
1463
1481
|
value: {
|
@@ -1469,6 +1487,7 @@ export default class LocalParticipant extends Participant {
|
|
1469
1487
|
},
|
1470
1488
|
});
|
1471
1489
|
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1490
|
+
|
1472
1491
|
this.emit(ParticipantEvent.ChatMessage, msg);
|
1473
1492
|
return msg;
|
1474
1493
|
}
|
@@ -1494,6 +1513,263 @@ export default class LocalParticipant extends Participant {
|
|
1494
1513
|
return msg;
|
1495
1514
|
}
|
1496
1515
|
|
1516
|
+
async sendText(text: string, options?: SendTextOptions): Promise<TextStreamInfo> {
|
1517
|
+
const streamId = crypto.randomUUID();
|
1518
|
+
const textInBytes = new TextEncoder().encode(text);
|
1519
|
+
const totalTextLength = textInBytes.byteLength;
|
1520
|
+
|
1521
|
+
const fileIds = options?.attachments?.map(() => crypto.randomUUID());
|
1522
|
+
|
1523
|
+
const progresses = new Array<number>(fileIds ? fileIds.length + 1 : 1).fill(0);
|
1524
|
+
|
1525
|
+
const handleProgress = (progress: number, idx: number) => {
|
1526
|
+
progresses[idx] = progress;
|
1527
|
+
const totalProgress = progresses.reduce((acc, val) => acc + val, 0);
|
1528
|
+
options?.onProgress?.(totalProgress);
|
1529
|
+
};
|
1530
|
+
|
1531
|
+
const writer = await this.streamText({
|
1532
|
+
streamId,
|
1533
|
+
totalSize: totalTextLength,
|
1534
|
+
destinationIdentities: options?.destinationIdentities,
|
1535
|
+
topic: options?.topic,
|
1536
|
+
attachedStreamIds: fileIds,
|
1537
|
+
});
|
1538
|
+
|
1539
|
+
const textChunkSize = Math.floor(STREAM_CHUNK_SIZE / 4); // utf8 is at most 4 bytes long, so play it safe and take a quarter of the byte size to slice the string
|
1540
|
+
const totalTextChunks = Math.ceil(totalTextLength / textChunkSize);
|
1541
|
+
|
1542
|
+
for (let i = 0; i < totalTextChunks; i++) {
|
1543
|
+
const chunkData = text.slice(
|
1544
|
+
i * textChunkSize,
|
1545
|
+
Math.min((i + 1) * textChunkSize, totalTextLength),
|
1546
|
+
);
|
1547
|
+
await this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
1548
|
+
await writer.write(chunkData);
|
1549
|
+
|
1550
|
+
handleProgress(Math.ceil((i + 1) / totalTextChunks), 0);
|
1551
|
+
}
|
1552
|
+
|
1553
|
+
await writer.close();
|
1554
|
+
|
1555
|
+
if (options?.attachments && fileIds) {
|
1556
|
+
await Promise.all(
|
1557
|
+
options.attachments.map(async (file, idx) =>
|
1558
|
+
this._sendFile(fileIds[idx], file, {
|
1559
|
+
topic: options.topic,
|
1560
|
+
mimeType: file.type,
|
1561
|
+
onProgress: (progress) => {
|
1562
|
+
handleProgress(progress, idx + 1);
|
1563
|
+
},
|
1564
|
+
}),
|
1565
|
+
),
|
1566
|
+
);
|
1567
|
+
}
|
1568
|
+
return writer.info;
|
1569
|
+
}
|
1570
|
+
|
1571
|
+
/**
|
1572
|
+
* @internal
|
1573
|
+
* @experimental CAUTION, might get removed in a minor release
|
1574
|
+
*/
|
1575
|
+
async streamText(options?: StreamTextOptions): Promise<TextStreamWriter> {
|
1576
|
+
const streamId = options?.streamId ?? crypto.randomUUID();
|
1577
|
+
|
1578
|
+
const info: TextStreamInfo = {
|
1579
|
+
id: streamId,
|
1580
|
+
mimeType: 'text/plain',
|
1581
|
+
timestamp: Date.now(),
|
1582
|
+
topic: options?.topic ?? '',
|
1583
|
+
size: options?.totalSize,
|
1584
|
+
};
|
1585
|
+
const header = new DataStream_Header({
|
1586
|
+
streamId,
|
1587
|
+
mimeType: info.mimeType,
|
1588
|
+
topic: info.topic,
|
1589
|
+
timestamp: numberToBigInt(info.timestamp),
|
1590
|
+
totalLength: numberToBigInt(options?.totalSize),
|
1591
|
+
contentHeader: {
|
1592
|
+
case: 'textHeader',
|
1593
|
+
value: new DataStream_TextHeader({
|
1594
|
+
version: options?.version,
|
1595
|
+
attachedStreamIds: options?.attachedStreamIds,
|
1596
|
+
replyToStreamId: options?.replyToStreamId,
|
1597
|
+
operationType:
|
1598
|
+
options?.type === 'update'
|
1599
|
+
? DataStream_OperationType.UPDATE
|
1600
|
+
: DataStream_OperationType.CREATE,
|
1601
|
+
}),
|
1602
|
+
},
|
1603
|
+
});
|
1604
|
+
const destinationIdentities = options?.destinationIdentities;
|
1605
|
+
const packet = new DataPacket({
|
1606
|
+
destinationIdentities,
|
1607
|
+
value: {
|
1608
|
+
case: 'streamHeader',
|
1609
|
+
value: header,
|
1610
|
+
},
|
1611
|
+
});
|
1612
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1613
|
+
|
1614
|
+
let chunkId = 0;
|
1615
|
+
const localP = this;
|
1616
|
+
|
1617
|
+
const writableStream = new WritableStream<[string, number?]>({
|
1618
|
+
// Implement the sink
|
1619
|
+
write([textChunk]) {
|
1620
|
+
const textInBytes = new TextEncoder().encode(textChunk);
|
1621
|
+
|
1622
|
+
if (textInBytes.byteLength > STREAM_CHUNK_SIZE) {
|
1623
|
+
this.abort?.();
|
1624
|
+
throw new Error('chunk size too large');
|
1625
|
+
}
|
1626
|
+
|
1627
|
+
return new Promise(async (resolve) => {
|
1628
|
+
await localP.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
1629
|
+
const chunk = new DataStream_Chunk({
|
1630
|
+
content: textInBytes,
|
1631
|
+
streamId,
|
1632
|
+
chunkIndex: numberToBigInt(chunkId),
|
1633
|
+
});
|
1634
|
+
const chunkPacket = new DataPacket({
|
1635
|
+
destinationIdentities,
|
1636
|
+
value: {
|
1637
|
+
case: 'streamChunk',
|
1638
|
+
value: chunk,
|
1639
|
+
},
|
1640
|
+
});
|
1641
|
+
await localP.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
|
1642
|
+
|
1643
|
+
chunkId += 1;
|
1644
|
+
resolve();
|
1645
|
+
});
|
1646
|
+
},
|
1647
|
+
async close() {
|
1648
|
+
const trailer = new DataStream_Trailer({
|
1649
|
+
streamId,
|
1650
|
+
});
|
1651
|
+
const trailerPacket = new DataPacket({
|
1652
|
+
destinationIdentities,
|
1653
|
+
value: {
|
1654
|
+
case: 'streamTrailer',
|
1655
|
+
value: trailer,
|
1656
|
+
},
|
1657
|
+
});
|
1658
|
+
await localP.engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
|
1659
|
+
},
|
1660
|
+
abort(err) {
|
1661
|
+
console.log('Sink error:', err);
|
1662
|
+
// TODO handle aborts to signal something to receiver side
|
1663
|
+
},
|
1664
|
+
});
|
1665
|
+
|
1666
|
+
let onEngineClose = async () => {
|
1667
|
+
await writer.close();
|
1668
|
+
};
|
1669
|
+
|
1670
|
+
localP.engine.once(EngineEvent.Closing, onEngineClose);
|
1671
|
+
|
1672
|
+
const writer = new TextStreamWriter(writableStream, info, () =>
|
1673
|
+
this.engine.off(EngineEvent.Closing, onEngineClose),
|
1674
|
+
);
|
1675
|
+
|
1676
|
+
return writer;
|
1677
|
+
}
|
1678
|
+
|
1679
|
+
async sendFile(
|
1680
|
+
file: File,
|
1681
|
+
options?: {
|
1682
|
+
mimeType?: string;
|
1683
|
+
topic?: string;
|
1684
|
+
destinationIdentities?: Array<string>;
|
1685
|
+
onProgress?: (progress: number) => void;
|
1686
|
+
},
|
1687
|
+
): Promise<{ id: string }> {
|
1688
|
+
const streamId = crypto.randomUUID();
|
1689
|
+
await this._sendFile(streamId, file, options);
|
1690
|
+
return { id: streamId };
|
1691
|
+
}
|
1692
|
+
|
1693
|
+
private async _sendFile(
|
1694
|
+
streamId: string,
|
1695
|
+
file: File,
|
1696
|
+
options?: {
|
1697
|
+
mimeType?: string;
|
1698
|
+
topic?: string;
|
1699
|
+
encryptionType?: Encryption_Type.NONE;
|
1700
|
+
destinationIdentities?: Array<string>;
|
1701
|
+
onProgress?: (progress: number) => void;
|
1702
|
+
},
|
1703
|
+
) {
|
1704
|
+
const totalLength = file.size;
|
1705
|
+
const header = new DataStream_Header({
|
1706
|
+
totalLength: numberToBigInt(totalLength),
|
1707
|
+
mimeType: options?.mimeType ?? file.type,
|
1708
|
+
streamId,
|
1709
|
+
topic: options?.topic,
|
1710
|
+
encryptionType: options?.encryptionType,
|
1711
|
+
timestamp: numberToBigInt(Date.now()),
|
1712
|
+
contentHeader: {
|
1713
|
+
case: 'byteHeader',
|
1714
|
+
value: new DataStream_ByteHeader({
|
1715
|
+
name: file.name,
|
1716
|
+
}),
|
1717
|
+
},
|
1718
|
+
});
|
1719
|
+
|
1720
|
+
const destinationIdentities = options?.destinationIdentities;
|
1721
|
+
const packet = new DataPacket({
|
1722
|
+
destinationIdentities,
|
1723
|
+
value: {
|
1724
|
+
case: 'streamHeader',
|
1725
|
+
value: header,
|
1726
|
+
},
|
1727
|
+
});
|
1728
|
+
|
1729
|
+
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1730
|
+
function read(b: Blob): Promise<Uint8Array> {
|
1731
|
+
return new Promise((resolve) => {
|
1732
|
+
const fr = new FileReader();
|
1733
|
+
fr.onload = () => {
|
1734
|
+
resolve(new Uint8Array(fr.result as ArrayBuffer));
|
1735
|
+
};
|
1736
|
+
fr.readAsArrayBuffer(b);
|
1737
|
+
});
|
1738
|
+
}
|
1739
|
+
const totalChunks = Math.ceil(totalLength / STREAM_CHUNK_SIZE);
|
1740
|
+
for (let i = 0; i < totalChunks; i++) {
|
1741
|
+
const chunkData = await read(
|
1742
|
+
file.slice(i * STREAM_CHUNK_SIZE, Math.min((i + 1) * STREAM_CHUNK_SIZE, totalLength)),
|
1743
|
+
);
|
1744
|
+
await this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
|
1745
|
+
const chunk = new DataStream_Chunk({
|
1746
|
+
content: chunkData,
|
1747
|
+
streamId,
|
1748
|
+
chunkIndex: numberToBigInt(i),
|
1749
|
+
});
|
1750
|
+
const chunkPacket = new DataPacket({
|
1751
|
+
destinationIdentities,
|
1752
|
+
value: {
|
1753
|
+
case: 'streamChunk',
|
1754
|
+
value: chunk,
|
1755
|
+
},
|
1756
|
+
});
|
1757
|
+
await this.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
|
1758
|
+
options?.onProgress?.((i + 1) / totalChunks);
|
1759
|
+
}
|
1760
|
+
const trailer = new DataStream_Trailer({
|
1761
|
+
streamId,
|
1762
|
+
});
|
1763
|
+
const trailerPacket = new DataPacket({
|
1764
|
+
destinationIdentities,
|
1765
|
+
value: {
|
1766
|
+
case: 'streamTrailer',
|
1767
|
+
value: trailer,
|
1768
|
+
},
|
1769
|
+
});
|
1770
|
+
await this.engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
|
1771
|
+
}
|
1772
|
+
|
1497
1773
|
/**
|
1498
1774
|
* Initiate an RPC call to a remote participant
|
1499
1775
|
* @param params - Parameters for initiating the RPC call, see {@link PerformRpcParams}
|
@@ -1571,39 +1847,20 @@ export default class LocalParticipant extends Participant {
|
|
1571
1847
|
}
|
1572
1848
|
|
1573
1849
|
/**
|
1574
|
-
*
|
1575
|
-
* Will overwrite any existing callback for the same method.
|
1576
|
-
*
|
1577
|
-
* @param method - The name of the indicated RPC method
|
1578
|
-
* @param handler - Will be invoked when an RPC request for this method is received
|
1579
|
-
* @returns A promise that resolves when the method is successfully registered
|
1580
|
-
*
|
1581
|
-
* @example
|
1582
|
-
* ```typescript
|
1583
|
-
* room.localParticipant?.registerRpcMethod(
|
1584
|
-
* 'greet',
|
1585
|
-
* async (data: RpcInvocationData) => {
|
1586
|
-
* console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
|
1587
|
-
* return `Hello, ${data.callerIdentity}!`;
|
1588
|
-
* }
|
1589
|
-
* );
|
1590
|
-
* ```
|
1591
|
-
*
|
1592
|
-
* The handler should return a Promise that resolves to a string.
|
1593
|
-
* If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
|
1594
|
-
*
|
1595
|
-
* You may throw errors of type `RpcError` with a string `message` in the handler,
|
1596
|
-
* and they will be received on the caller's side with the message intact.
|
1597
|
-
* Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
|
1850
|
+
* @deprecated use `room.registerRpcMethod` instead
|
1598
1851
|
*/
|
1599
1852
|
registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
|
1853
|
+
if (this.rpcHandlers.has(method)) {
|
1854
|
+
this.log.warn(
|
1855
|
+
`you're overriding the RPC handler for method ${method}, in the future this will throw an error`,
|
1856
|
+
);
|
1857
|
+
}
|
1858
|
+
|
1600
1859
|
this.rpcHandlers.set(method, handler);
|
1601
1860
|
}
|
1602
1861
|
|
1603
1862
|
/**
|
1604
|
-
*
|
1605
|
-
*
|
1606
|
-
* @param method - The name of the RPC method to unregister
|
1863
|
+
* @deprecated use `room.unregisterRpcMethod` instead
|
1607
1864
|
*/
|
1608
1865
|
unregisterRpcMethod(method: string) {
|
1609
1866
|
this.rpcHandlers.delete(method);
|
@@ -1661,68 +1918,6 @@ export default class LocalParticipant extends Participant {
|
|
1661
1918
|
}
|
1662
1919
|
}
|
1663
1920
|
|
1664
|
-
private async handleIncomingRpcRequest(
|
1665
|
-
callerIdentity: string,
|
1666
|
-
requestId: string,
|
1667
|
-
method: string,
|
1668
|
-
payload: string,
|
1669
|
-
responseTimeout: number,
|
1670
|
-
version: number,
|
1671
|
-
) {
|
1672
|
-
await this.publishRpcAck(callerIdentity, requestId);
|
1673
|
-
|
1674
|
-
if (version !== 1) {
|
1675
|
-
await this.publishRpcResponse(
|
1676
|
-
callerIdentity,
|
1677
|
-
requestId,
|
1678
|
-
null,
|
1679
|
-
RpcError.builtIn('UNSUPPORTED_VERSION'),
|
1680
|
-
);
|
1681
|
-
return;
|
1682
|
-
}
|
1683
|
-
|
1684
|
-
const handler = this.rpcHandlers.get(method);
|
1685
|
-
|
1686
|
-
if (!handler) {
|
1687
|
-
await this.publishRpcResponse(
|
1688
|
-
callerIdentity,
|
1689
|
-
requestId,
|
1690
|
-
null,
|
1691
|
-
RpcError.builtIn('UNSUPPORTED_METHOD'),
|
1692
|
-
);
|
1693
|
-
return;
|
1694
|
-
}
|
1695
|
-
|
1696
|
-
let responseError: RpcError | null = null;
|
1697
|
-
let responsePayload: string | null = null;
|
1698
|
-
|
1699
|
-
try {
|
1700
|
-
const response = await handler({
|
1701
|
-
requestId,
|
1702
|
-
callerIdentity,
|
1703
|
-
payload,
|
1704
|
-
responseTimeout,
|
1705
|
-
});
|
1706
|
-
if (byteLength(response) > MAX_PAYLOAD_BYTES) {
|
1707
|
-
responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
|
1708
|
-
console.warn(`RPC Response payload too large for ${method}`);
|
1709
|
-
} else {
|
1710
|
-
responsePayload = response;
|
1711
|
-
}
|
1712
|
-
} catch (error) {
|
1713
|
-
if (error instanceof RpcError) {
|
1714
|
-
responseError = error;
|
1715
|
-
} else {
|
1716
|
-
console.warn(
|
1717
|
-
`Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
|
1718
|
-
error,
|
1719
|
-
);
|
1720
|
-
responseError = RpcError.builtIn('APPLICATION_ERROR');
|
1721
|
-
}
|
1722
|
-
}
|
1723
|
-
await this.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
|
1724
|
-
}
|
1725
|
-
|
1726
1921
|
/** @internal */
|
1727
1922
|
private async publishRpcRequest(
|
1728
1923
|
destinationIdentity: string,
|
@@ -1749,46 +1944,6 @@ export default class LocalParticipant extends Participant {
|
|
1749
1944
|
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1750
1945
|
}
|
1751
1946
|
|
1752
|
-
/** @internal */
|
1753
|
-
private async publishRpcResponse(
|
1754
|
-
destinationIdentity: string,
|
1755
|
-
requestId: string,
|
1756
|
-
payload: string | null,
|
1757
|
-
error: RpcError | null,
|
1758
|
-
) {
|
1759
|
-
const packet = new DataPacket({
|
1760
|
-
destinationIdentities: [destinationIdentity],
|
1761
|
-
kind: DataPacket_Kind.RELIABLE,
|
1762
|
-
value: {
|
1763
|
-
case: 'rpcResponse',
|
1764
|
-
value: new RpcResponse({
|
1765
|
-
requestId,
|
1766
|
-
value: error
|
1767
|
-
? { case: 'error', value: error.toProto() }
|
1768
|
-
: { case: 'payload', value: payload ?? '' },
|
1769
|
-
}),
|
1770
|
-
},
|
1771
|
-
});
|
1772
|
-
|
1773
|
-
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1774
|
-
}
|
1775
|
-
|
1776
|
-
/** @internal */
|
1777
|
-
private async publishRpcAck(destinationIdentity: string, requestId: string) {
|
1778
|
-
const packet = new DataPacket({
|
1779
|
-
destinationIdentities: [destinationIdentity],
|
1780
|
-
kind: DataPacket_Kind.RELIABLE,
|
1781
|
-
value: {
|
1782
|
-
case: 'rpcAck',
|
1783
|
-
value: new RpcAck({
|
1784
|
-
requestId,
|
1785
|
-
}),
|
1786
|
-
},
|
1787
|
-
});
|
1788
|
-
|
1789
|
-
await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
|
1790
|
-
}
|
1791
|
-
|
1792
1947
|
/** @internal */
|
1793
1948
|
handleParticipantDisconnected(participantIdentity: string) {
|
1794
1949
|
for (const [id, { participantIdentity: pendingIdentity }] of this.pendingAcks) {
|
@@ -1962,7 +2117,7 @@ export default class LocalParticipant extends Participant {
|
|
1962
2117
|
this.unpublishTrack(track);
|
1963
2118
|
} else if (track.isUserProvided) {
|
1964
2119
|
await track.mute();
|
1965
|
-
} else if (track
|
2120
|
+
} else if (isLocalAudioTrack(track) || isLocalVideoTrack(track)) {
|
1966
2121
|
try {
|
1967
2122
|
if (isWeb()) {
|
1968
2123
|
try {
|
@@ -1997,7 +2152,7 @@ export default class LocalParticipant extends Participant {
|
|
1997
2152
|
...this.logContext,
|
1998
2153
|
...getLogContextFromTrack(track),
|
1999
2154
|
});
|
2000
|
-
if (track
|
2155
|
+
if (isLocalAudioTrack(track)) {
|
2001
2156
|
// fall back to default device if available
|
2002
2157
|
await track.restartTrack({ deviceId: 'default' });
|
2003
2158
|
} else {
|
@@ -2026,7 +2181,7 @@ export default class LocalParticipant extends Participant {
|
|
2026
2181
|
|
2027
2182
|
// this looks overly complicated due to this object tree
|
2028
2183
|
if (track instanceof MediaStreamTrack) {
|
2029
|
-
if (localTrack
|
2184
|
+
if (isLocalAudioTrack(localTrack) || isLocalVideoTrack(localTrack)) {
|
2030
2185
|
if (localTrack.mediaStreamTrack === track) {
|
2031
2186
|
publication = <LocalTrackPublication>pub;
|
2032
2187
|
}
|