livekit-client 2.8.0 → 2.9.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 (94) hide show
  1. package/README.md +18 -7
  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 +1 -0
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +3685 -2966
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +7 -5
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/room/RTCEngine.d.ts +6 -0
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/Room.d.ts +50 -1
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/StreamReader.d.ts +56 -0
  18. package/dist/src/room/StreamReader.d.ts.map +1 -0
  19. package/dist/src/room/StreamWriter.d.ts +16 -0
  20. package/dist/src/room/StreamWriter.d.ts.map +1 -0
  21. package/dist/src/room/errors.d.ts +3 -1
  22. package/dist/src/room/errors.d.ts.map +1 -1
  23. package/dist/src/room/participant/LocalParticipant.d.ts +23 -36
  24. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  25. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  26. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  27. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalTrack.d.ts +1 -0
  29. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalTrackPublication.d.ts +1 -0
  31. package/dist/src/room/track/LocalTrackPublication.d.ts.map +1 -1
  32. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/RemoteTrack.d.ts +1 -0
  34. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -0
  36. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  37. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  38. package/dist/src/room/track/Track.d.ts +1 -0
  39. package/dist/src/room/track/Track.d.ts.map +1 -1
  40. package/dist/src/room/track/TrackPublication.d.ts +2 -1
  41. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  42. package/dist/src/room/track/create.d.ts.map +1 -1
  43. package/dist/src/room/track/facingMode.d.ts.map +1 -1
  44. package/dist/src/room/track/options.d.ts +18 -2
  45. package/dist/src/room/track/options.d.ts.map +1 -1
  46. package/dist/src/room/types.d.ts +43 -0
  47. package/dist/src/room/types.d.ts.map +1 -1
  48. package/dist/src/room/utils.d.ts +26 -0
  49. package/dist/src/room/utils.d.ts.map +1 -1
  50. package/dist/ts4.2/src/index.d.ts +7 -5
  51. package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -0
  52. package/dist/ts4.2/src/room/Room.d.ts +49 -0
  53. package/dist/ts4.2/src/room/StreamReader.d.ts +56 -0
  54. package/dist/ts4.2/src/room/StreamWriter.d.ts +25 -0
  55. package/dist/ts4.2/src/room/errors.d.ts +3 -1
  56. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +23 -36
  57. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
  58. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -0
  59. package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +1 -0
  60. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -0
  61. package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
  62. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -1
  63. package/dist/ts4.2/src/room/track/options.d.ts +18 -2
  64. package/dist/ts4.2/src/room/types.d.ts +43 -0
  65. package/dist/ts4.2/src/room/utils.d.ts +26 -0
  66. package/package.json +3 -3
  67. package/src/api/SignalClient.ts +5 -2
  68. package/src/e2ee/E2eeManager.ts +2 -2
  69. package/src/index.ts +17 -1
  70. package/src/room/RTCEngine.ts +69 -2
  71. package/src/room/Room.ts +317 -25
  72. package/src/room/StreamReader.ts +177 -0
  73. package/src/room/StreamWriter.ts +32 -0
  74. package/src/room/errors.ts +16 -2
  75. package/src/room/participant/LocalParticipant.ts +320 -165
  76. package/src/room/participant/Participant.ts +2 -5
  77. package/src/room/participant/RemoteParticipant.ts +3 -2
  78. package/src/room/{participant/LocalParticipant.test.ts → rpc.test.ts} +22 -29
  79. package/src/room/track/LocalAudioTrack.ts +4 -3
  80. package/src/room/track/LocalTrack.ts +12 -4
  81. package/src/room/track/LocalTrackPublication.ts +6 -1
  82. package/src/room/track/LocalVideoTrack.ts +1 -1
  83. package/src/room/track/RemoteAudioTrack.ts +1 -0
  84. package/src/room/track/RemoteTrack.ts +4 -0
  85. package/src/room/track/RemoteTrackPublication.ts +8 -4
  86. package/src/room/track/RemoteVideoTrack.ts +1 -0
  87. package/src/room/track/Track.ts +2 -0
  88. package/src/room/track/TrackPublication.ts +6 -3
  89. package/src/room/track/create.ts +4 -3
  90. package/src/room/track/facingMode.ts +2 -1
  91. package/src/room/track/options.ts +20 -2
  92. package/src/room/track/utils.ts +1 -1
  93. package/src/room/types.ts +50 -0
  94. 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 type { ChatMessage, DataPublishOptions } from '../types';
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(sid: string, identity: string, engine: RTCEngine, options: InternalRoomOptions) {
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 instanceof LocalAudioTrack && audioProcessor) {
661
+ if (isAudioTrack(track) && audioProcessor) {
644
662
  await track.setProcessor(audioProcessor);
645
- } else if (track instanceof LocalVideoTrack && videoProcessor) {
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 instanceof LocalAudioTrack) {
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 instanceof LocalTrack && this.pendingPublishPromises.has(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 instanceof LocalTrack && publishedTrack.source === track.source,
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 instanceof LocalAudioTrack) {
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 instanceof LocalVideoTrack) {
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 instanceof LocalVideoTrack) {
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 instanceof LocalVideoTrack) {
1148
+ if (isLocalVideoTrack(track)) {
1130
1149
  track.startMonitor(this.engine.client);
1131
- } else if (track instanceof LocalAudioTrack) {
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 instanceof LocalVideoTrack)) {
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 instanceof LocalTrack) {
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 instanceof LocalVideoTrack) {
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 instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
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
- * Establishes the participant as a receiver for calls of the specified RPC method.
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
- * Unregisters a previously registered RPC method.
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 instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
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 instanceof LocalAudioTrack) {
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 instanceof LocalAudioTrack || localTrack instanceof LocalVideoTrack) {
2184
+ if (isLocalAudioTrack(localTrack) || isLocalVideoTrack(localTrack)) {
2030
2185
  if (localTrack.mediaStreamTrack === track) {
2031
2186
  publication = <LocalTrackPublication>pub;
2032
2187
  }