livekit-client 2.8.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) 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 +2565 -1849
  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/RemoteTrack.d.ts +1 -0
  33. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -0
  35. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  36. package/dist/src/room/track/Track.d.ts +1 -0
  37. package/dist/src/room/track/Track.d.ts.map +1 -1
  38. package/dist/src/room/track/TrackPublication.d.ts +2 -1
  39. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  40. package/dist/src/room/track/create.d.ts.map +1 -1
  41. package/dist/src/room/track/facingMode.d.ts.map +1 -1
  42. package/dist/src/room/track/options.d.ts +18 -2
  43. package/dist/src/room/track/options.d.ts.map +1 -1
  44. package/dist/src/room/types.d.ts +43 -0
  45. package/dist/src/room/types.d.ts.map +1 -1
  46. package/dist/src/room/utils.d.ts +26 -0
  47. package/dist/src/room/utils.d.ts.map +1 -1
  48. package/dist/ts4.2/src/index.d.ts +7 -5
  49. package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -0
  50. package/dist/ts4.2/src/room/Room.d.ts +49 -0
  51. package/dist/ts4.2/src/room/StreamReader.d.ts +56 -0
  52. package/dist/ts4.2/src/room/StreamWriter.d.ts +25 -0
  53. package/dist/ts4.2/src/room/errors.d.ts +3 -1
  54. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +23 -36
  55. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
  56. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +1 -0
  57. package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +1 -0
  58. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -0
  59. package/dist/ts4.2/src/room/track/Track.d.ts +1 -0
  60. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -1
  61. package/dist/ts4.2/src/room/track/options.d.ts +18 -2
  62. package/dist/ts4.2/src/room/types.d.ts +43 -0
  63. package/dist/ts4.2/src/room/utils.d.ts +26 -0
  64. package/package.json +3 -3
  65. package/src/api/SignalClient.ts +5 -2
  66. package/src/e2ee/E2eeManager.ts +2 -2
  67. package/src/index.ts +17 -1
  68. package/src/room/RTCEngine.ts +69 -2
  69. package/src/room/Room.ts +311 -23
  70. package/src/room/StreamReader.ts +177 -0
  71. package/src/room/StreamWriter.ts +32 -0
  72. package/src/room/errors.ts +16 -2
  73. package/src/room/participant/LocalParticipant.ts +320 -165
  74. package/src/room/participant/Participant.ts +2 -5
  75. package/src/room/participant/RemoteParticipant.ts +3 -2
  76. package/src/room/{participant/LocalParticipant.test.ts → rpc.test.ts} +22 -29
  77. package/src/room/track/LocalAudioTrack.ts +4 -3
  78. package/src/room/track/LocalTrack.ts +12 -4
  79. package/src/room/track/LocalTrackPublication.ts +6 -1
  80. package/src/room/track/LocalVideoTrack.ts +1 -1
  81. package/src/room/track/RemoteTrack.ts +4 -0
  82. package/src/room/track/RemoteTrackPublication.ts +8 -4
  83. package/src/room/track/Track.ts +2 -0
  84. package/src/room/track/TrackPublication.ts +6 -3
  85. package/src/room/track/create.ts +4 -3
  86. package/src/room/track/facingMode.ts +2 -1
  87. package/src/room/track/options.ts +20 -2
  88. package/src/room/track/utils.ts +1 -1
  89. package/src/room/types.ts +50 -0
  90. package/src/room/utils.ts +77 -0
package/src/room/Room.ts CHANGED
@@ -4,6 +4,9 @@ import {
4
4
  ConnectionQualityUpdate,
5
5
  type DataPacket,
6
6
  DataPacket_Kind,
7
+ DataStream_Chunk,
8
+ DataStream_Header,
9
+ DataStream_Trailer,
7
10
  DisconnectReason,
8
11
  JoinResponse,
9
12
  LeaveRequest,
@@ -45,6 +48,12 @@ import { getBrowser } from '../utils/browserParser';
45
48
  import DeviceManager from './DeviceManager';
46
49
  import RTCEngine from './RTCEngine';
47
50
  import { RegionUrlProvider } from './RegionUrlProvider';
51
+ import {
52
+ type ByteStreamHandler,
53
+ ByteStreamReader,
54
+ type TextStreamHandler,
55
+ TextStreamReader,
56
+ } from './StreamReader';
48
57
  import {
49
58
  audioDefaults,
50
59
  publishDefaults,
@@ -58,6 +67,7 @@ import LocalParticipant from './participant/LocalParticipant';
58
67
  import type Participant from './participant/Participant';
59
68
  import type { ConnectionQuality } from './participant/Participant';
60
69
  import RemoteParticipant from './participant/RemoteParticipant';
70
+ import { MAX_PAYLOAD_BYTES, RpcError, type RpcInvocationData, byteLength } from './rpc';
61
71
  import CriticalTimers from './timers';
62
72
  import LocalAudioTrack from './track/LocalAudioTrack';
63
73
  import type LocalTrack from './track/LocalTrack';
@@ -70,14 +80,18 @@ import type { TrackPublication } from './track/TrackPublication';
70
80
  import type { TrackProcessor } from './track/processor/types';
71
81
  import type { AdaptiveStreamSettings } from './track/types';
72
82
  import { getNewAudioContext, sourceToKind } from './track/utils';
73
- import type {
74
- ChatMessage,
75
- SimulationOptions,
76
- SimulationScenario,
77
- TranscriptionSegment,
83
+ import {
84
+ type ByteStreamInfo,
85
+ type ChatMessage,
86
+ type SimulationOptions,
87
+ type SimulationScenario,
88
+ type StreamController,
89
+ type TextStreamInfo,
90
+ type TranscriptionSegment,
78
91
  } from './types';
79
92
  import {
80
93
  Future,
94
+ bigIntToNumber,
81
95
  createDummyVideoStreamTrack,
82
96
  extractChatMessage,
83
97
  extractTranscriptionSegments,
@@ -85,9 +99,14 @@ import {
85
99
  getEmptyAudioStreamTrack,
86
100
  isBrowserSupported,
87
101
  isCloud,
102
+ isLocalAudioTrack,
103
+ isLocalParticipant,
88
104
  isReactNative,
105
+ isRemotePub,
89
106
  isSafari,
90
107
  isWeb,
108
+ numberToBigInt,
109
+ sleep,
91
110
  supportsSetSinkId,
92
111
  toHttpUrl,
93
112
  unpackStreamId,
@@ -180,6 +199,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
180
199
  */
181
200
  private transcriptionReceivedTimes: Map<string, number>;
182
201
 
202
+ private byteStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
203
+
204
+ private textStreamControllers = new Map<string, StreamController<DataStream_Chunk>>();
205
+
206
+ private byteStreamHandlers = new Map<string, ByteStreamHandler>();
207
+
208
+ private textStreamHandlers = new Map<string, TextStreamHandler>();
209
+
210
+ private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
211
+
183
212
  /**
184
213
  * Creates a new Room, the primary construct for a LiveKit session.
185
214
  * @param options
@@ -211,7 +240,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
211
240
 
212
241
  this.disconnectLock = new Mutex();
213
242
 
214
- this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
243
+ this.localParticipant = new LocalParticipant(
244
+ '',
245
+ '',
246
+ this.engine,
247
+ this.options,
248
+ this.rpcHandlers,
249
+ );
215
250
 
216
251
  if (this.options.videoCaptureDefaults.deviceId) {
217
252
  this.localParticipant.activeDeviceMap.set(
@@ -252,6 +287,135 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
252
287
  }
253
288
  }
254
289
 
290
+ registerTextStreamHandler(topic: string, callback: TextStreamHandler) {
291
+ if (this.textStreamHandlers.has(topic)) {
292
+ throw new TypeError(`A text stream handler for topic "${topic}" has already been set.`);
293
+ }
294
+ this.textStreamHandlers.set(topic, callback);
295
+ }
296
+
297
+ unregisterTextStreamHandler(topic: string) {
298
+ this.textStreamHandlers.delete(topic);
299
+ }
300
+
301
+ registerByteStreamHandler(topic: string, callback: ByteStreamHandler) {
302
+ if (this.byteStreamHandlers.has(topic)) {
303
+ throw new TypeError(`A byte stream handler for topic "${topic}" has already been set.`);
304
+ }
305
+ this.byteStreamHandlers.set(topic, callback);
306
+ }
307
+
308
+ unregisterByteStreamHandler(topic: string) {
309
+ this.byteStreamHandlers.delete(topic);
310
+ }
311
+
312
+ /**
313
+ * Establishes the participant as a receiver for calls of the specified RPC method.
314
+ * Will overwrite any existing callback for the same method.
315
+ *
316
+ * @param method - The name of the indicated RPC method
317
+ * @param handler - Will be invoked when an RPC request for this method is received
318
+ * @returns A promise that resolves when the method is successfully registered
319
+ * @throws {Error} if the handler for a specific method has already been registered already
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * room.localParticipant?.registerRpcMethod(
324
+ * 'greet',
325
+ * async (data: RpcInvocationData) => {
326
+ * console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`);
327
+ * return `Hello, ${data.callerIdentity}!`;
328
+ * }
329
+ * );
330
+ * ```
331
+ *
332
+ * The handler should return a Promise that resolves to a string.
333
+ * If unable to respond within `responseTimeout`, the request will result in an error on the caller's side.
334
+ *
335
+ * You may throw errors of type `RpcError` with a string `message` in the handler,
336
+ * and they will be received on the caller's side with the message intact.
337
+ * Other errors thrown in your handler will not be transmitted as-is, and will instead arrive to the caller as `1500` ("Application Error").
338
+ */
339
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
340
+ if (this.rpcHandlers.has(method)) {
341
+ throw Error(
342
+ `RPC handler already registered for method ${method}, unregisterRpcMethod before trying to register again`,
343
+ );
344
+ }
345
+ this.rpcHandlers.set(method, handler);
346
+ }
347
+
348
+ /**
349
+ * Unregisters a previously registered RPC method.
350
+ *
351
+ * @param method - The name of the RPC method to unregister
352
+ */
353
+ unregisterRpcMethod(method: string) {
354
+ this.rpcHandlers.delete(method);
355
+ }
356
+
357
+ private async handleIncomingRpcRequest(
358
+ callerIdentity: string,
359
+ requestId: string,
360
+ method: string,
361
+ payload: string,
362
+ responseTimeout: number,
363
+ version: number,
364
+ ) {
365
+ await this.engine.publishRpcAck(callerIdentity, requestId);
366
+
367
+ if (version !== 1) {
368
+ await this.engine.publishRpcResponse(
369
+ callerIdentity,
370
+ requestId,
371
+ null,
372
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
373
+ );
374
+ return;
375
+ }
376
+
377
+ const handler = this.rpcHandlers.get(method);
378
+
379
+ if (!handler) {
380
+ await this.engine.publishRpcResponse(
381
+ callerIdentity,
382
+ requestId,
383
+ null,
384
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
385
+ );
386
+ return;
387
+ }
388
+
389
+ let responseError: RpcError | null = null;
390
+ let responsePayload: string | null = null;
391
+
392
+ try {
393
+ const response = await handler({
394
+ requestId,
395
+ callerIdentity,
396
+ payload,
397
+ responseTimeout,
398
+ });
399
+ if (byteLength(response) > MAX_PAYLOAD_BYTES) {
400
+ responseError = RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE');
401
+ console.warn(`RPC Response payload too large for ${method}`);
402
+ } else {
403
+ responsePayload = response;
404
+ }
405
+ } catch (error) {
406
+ if (error instanceof RpcError) {
407
+ responseError = error;
408
+ } else {
409
+ console.warn(
410
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
411
+ error,
412
+ );
413
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
414
+ }
415
+ }
416
+ await this.engine.publishRpcResponse(callerIdentity, requestId, responsePayload, responseError);
417
+ }
418
+
255
419
  /**
256
420
  * @experimental
257
421
  */
@@ -276,7 +440,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
276
440
  this.e2eeManager.on(
277
441
  EncryptionEvent.ParticipantEncryptionStatusChanged,
278
442
  (enabled, participant) => {
279
- if (participant instanceof LocalParticipant) {
443
+ if (isLocalParticipant(participant)) {
280
444
  this.isE2EEEnabled = enabled;
281
445
  }
282
446
  this.emit(RoomEvent.ParticipantEncryptionStatusChanged, enabled, participant);
@@ -960,7 +1124,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
960
1124
  req = new SimulateScenario({
961
1125
  scenario: {
962
1126
  case: 'subscriberBandwidth',
963
- value: BigInt(arg),
1127
+ value: numberToBigInt(arg),
964
1128
  },
965
1129
  });
966
1130
  break;
@@ -1593,9 +1757,136 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1593
1757
  this.handleChatMessage(participant, packet.value.value);
1594
1758
  } else if (packet.value.case === 'metrics') {
1595
1759
  this.handleMetrics(packet.value.value, participant);
1760
+ } else if (packet.value.case === 'streamHeader') {
1761
+ this.handleStreamHeader(packet.value.value, packet.participantIdentity);
1762
+ } else if (packet.value.case === 'streamChunk') {
1763
+ this.handleStreamChunk(packet.value.value);
1764
+ } else if (packet.value.case === 'streamTrailer') {
1765
+ this.handleStreamTrailer(packet.value.value);
1766
+ } else if (packet.value.case === 'rpcRequest') {
1767
+ const rpc = packet.value.value;
1768
+ this.handleIncomingRpcRequest(
1769
+ packet.participantIdentity,
1770
+ rpc.id,
1771
+ rpc.method,
1772
+ rpc.payload,
1773
+ rpc.responseTimeoutMs,
1774
+ rpc.version,
1775
+ );
1596
1776
  }
1597
1777
  };
1598
1778
 
1779
+ private async handleStreamHeader(streamHeader: DataStream_Header, participantIdentity: string) {
1780
+ if (streamHeader.contentHeader.case === 'byteHeader') {
1781
+ const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
1782
+
1783
+ if (!streamHandlerCallback) {
1784
+ this.log.debug(
1785
+ 'ignoring incoming byte stream due to no handler for topic',
1786
+ streamHeader.topic,
1787
+ );
1788
+ return;
1789
+ }
1790
+ let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
1791
+ const info: ByteStreamInfo = {
1792
+ id: streamHeader.streamId,
1793
+ name: streamHeader.contentHeader.value.name ?? 'unknown',
1794
+ mimeType: streamHeader.mimeType,
1795
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
1796
+ topic: streamHeader.topic,
1797
+ timestamp: bigIntToNumber(streamHeader.timestamp),
1798
+ attributes: streamHeader.attributes,
1799
+ };
1800
+ const stream = new ReadableStream({
1801
+ start: (controller) => {
1802
+ streamController = controller;
1803
+ this.byteStreamControllers.set(streamHeader.streamId, {
1804
+ info,
1805
+ controller: streamController,
1806
+ startTime: Date.now(),
1807
+ });
1808
+ },
1809
+ });
1810
+ streamHandlerCallback(
1811
+ new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
1812
+ {
1813
+ identity: participantIdentity,
1814
+ },
1815
+ );
1816
+ } else if (streamHeader.contentHeader.case === 'textHeader') {
1817
+ const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
1818
+
1819
+ if (!streamHandlerCallback) {
1820
+ this.log.debug(
1821
+ 'ignoring incoming text stream due to no handler for topic',
1822
+ streamHeader.topic,
1823
+ );
1824
+ return;
1825
+ }
1826
+ let streamController: ReadableStreamDefaultController<DataStream_Chunk>;
1827
+ const info: TextStreamInfo = {
1828
+ id: streamHeader.streamId,
1829
+ mimeType: streamHeader.mimeType,
1830
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
1831
+ topic: streamHeader.topic,
1832
+ timestamp: Number(streamHeader.timestamp),
1833
+ attributes: streamHeader.attributes,
1834
+ };
1835
+
1836
+ const stream = new ReadableStream<DataStream_Chunk>({
1837
+ start: (controller) => {
1838
+ streamController = controller;
1839
+ this.textStreamControllers.set(streamHeader.streamId, {
1840
+ info,
1841
+ controller: streamController,
1842
+ startTime: Date.now(),
1843
+ });
1844
+ },
1845
+ });
1846
+ streamHandlerCallback(
1847
+ new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)),
1848
+ { identity: participantIdentity },
1849
+ );
1850
+ }
1851
+ }
1852
+
1853
+ private handleStreamChunk(chunk: DataStream_Chunk) {
1854
+ const fileBuffer = this.byteStreamControllers.get(chunk.streamId);
1855
+ if (fileBuffer) {
1856
+ if (chunk.content.length > 0) {
1857
+ fileBuffer.controller.enqueue(chunk);
1858
+ }
1859
+ }
1860
+ const textBuffer = this.textStreamControllers.get(chunk.streamId);
1861
+ if (textBuffer) {
1862
+ if (chunk.content.length > 0) {
1863
+ textBuffer.controller.enqueue(chunk);
1864
+ }
1865
+ }
1866
+ }
1867
+
1868
+ private handleStreamTrailer(trailer: DataStream_Trailer) {
1869
+ const textBuffer = this.textStreamControllers.get(trailer.streamId);
1870
+
1871
+ if (textBuffer) {
1872
+ textBuffer.info.attributes = {
1873
+ ...textBuffer.info.attributes,
1874
+ ...trailer.attributes,
1875
+ };
1876
+ textBuffer.controller.close();
1877
+ this.byteStreamControllers.delete(trailer.streamId);
1878
+ }
1879
+
1880
+ const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
1881
+ if (fileBuffer) {
1882
+ {
1883
+ fileBuffer.info.attributes = { ...fileBuffer.info.attributes, ...trailer.attributes };
1884
+ fileBuffer.controller.close();
1885
+ this.byteStreamControllers.delete(trailer.streamId);
1886
+ }
1887
+ }
1888
+ }
1889
+
1599
1890
  private handleUserPacket = (
1600
1891
  participant: RemoteParticipant | undefined,
1601
1892
  userPacket: UserPacket,
@@ -1771,24 +2062,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1771
2062
  this.audioContext = getNewAudioContext() ?? undefined;
1772
2063
  }
1773
2064
 
2065
+ if (this.options.webAudioMix) {
2066
+ this.remoteParticipants.forEach((participant) =>
2067
+ participant.setAudioContext(this.audioContext),
2068
+ );
2069
+ }
2070
+
2071
+ this.localParticipant.setAudioContext(this.audioContext);
2072
+
1774
2073
  if (this.audioContext && this.audioContext.state === 'suspended') {
1775
2074
  // for iOS a newly created AudioContext is always in `suspended` state.
1776
2075
  // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
1777
2076
  try {
1778
- await this.audioContext.resume();
2077
+ await Promise.race([this.audioContext.resume(), sleep(200)]);
1779
2078
  } catch (e: any) {
1780
2079
  this.log.warn('Could not resume audio context', { ...this.logContext, error: e });
1781
2080
  }
1782
2081
  }
1783
2082
 
1784
- if (this.options.webAudioMix) {
1785
- this.remoteParticipants.forEach((participant) =>
1786
- participant.setAudioContext(this.audioContext),
1787
- );
1788
- }
1789
-
1790
- this.localParticipant.setAudioContext(this.audioContext);
1791
-
1792
2083
  const newContextIsRunning = this.audioContext?.state === 'running';
1793
2084
  if (newContextIsRunning !== this.canPlaybackAudio) {
1794
2085
  this.audioEnabled = newContextIsRunning;
@@ -1877,9 +2168,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1877
2168
  this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
1878
2169
  },
1879
2170
  )
1880
- .on(ParticipantEvent.TrackSubscriptionFailed, (sid: string) => {
1881
- this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
1882
- })
1883
2171
  .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
1884
2172
  this.emitWhenConnected(RoomEvent.TrackMuted, pub, participant);
1885
2173
  })
@@ -1950,7 +2238,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1950
2238
  private updateSubscriptions() {
1951
2239
  for (const p of this.remoteParticipants.values()) {
1952
2240
  for (const pub of p.videoTrackPublications.values()) {
1953
- if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
2241
+ if (pub.isSubscribed && isRemotePub(pub)) {
1954
2242
  pub.emitTrackUpdate();
1955
2243
  }
1956
2244
  }
@@ -2072,7 +2360,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2072
2360
 
2073
2361
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
2074
2362
 
2075
- if (pub.track instanceof LocalAudioTrack) {
2363
+ if (isLocalAudioTrack(pub.track)) {
2076
2364
  const trackIsSilent = await pub.track.checkForSilence();
2077
2365
  if (trackIsSilent) {
2078
2366
  this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
@@ -2300,7 +2588,7 @@ function mapArgs(args: unknown[]): any {
2300
2588
  return mapArgs(arg);
2301
2589
  }
2302
2590
  if (typeof arg === 'object') {
2303
- return 'logContext' in arg && arg.logContext;
2591
+ return 'logContext' in arg ? arg.logContext : undefined;
2304
2592
  }
2305
2593
  return arg;
2306
2594
  });
@@ -0,0 +1,177 @@
1
+ import type { DataStream_Chunk } from '@livekit/protocol';
2
+ import type { BaseStreamInfo, ByteStreamInfo, TextStreamChunk, TextStreamInfo } from './types';
3
+ import { bigIntToNumber } from './utils';
4
+
5
+ abstract class BaseStreamReader<T extends BaseStreamInfo> {
6
+ protected reader: ReadableStream<DataStream_Chunk>;
7
+
8
+ protected totalByteSize?: number;
9
+
10
+ protected _info: T;
11
+
12
+ protected bytesReceived: number;
13
+
14
+ get info() {
15
+ return this._info;
16
+ }
17
+
18
+ constructor(info: T, stream: ReadableStream<DataStream_Chunk>, totalByteSize?: number) {
19
+ this.reader = stream;
20
+ this.totalByteSize = totalByteSize;
21
+ this._info = info;
22
+ this.bytesReceived = 0;
23
+ }
24
+
25
+ protected abstract handleChunkReceived(chunk: DataStream_Chunk): void;
26
+
27
+ onProgress?: (progress: number | undefined) => void;
28
+
29
+ abstract readAll(): Promise<string | Array<Uint8Array>>;
30
+ }
31
+
32
+ export class ByteStreamReader extends BaseStreamReader<ByteStreamInfo> {
33
+ protected handleChunkReceived(chunk: DataStream_Chunk) {
34
+ this.bytesReceived += chunk.content.byteLength;
35
+ const currentProgress = this.totalByteSize
36
+ ? this.bytesReceived / this.totalByteSize
37
+ : undefined;
38
+ this.onProgress?.(currentProgress);
39
+ }
40
+
41
+ onProgress?: (progress: number | undefined) => void;
42
+
43
+ [Symbol.asyncIterator]() {
44
+ const reader = this.reader.getReader();
45
+
46
+ return {
47
+ next: async (): Promise<IteratorResult<Uint8Array>> => {
48
+ try {
49
+ const { done, value } = await reader.read();
50
+ if (done) {
51
+ return { done: true, value: undefined as any };
52
+ } else {
53
+ this.handleChunkReceived(value);
54
+ return { done: false, value: value.content };
55
+ }
56
+ } catch (error) {
57
+ // TODO handle errors
58
+ return { done: true, value: undefined };
59
+ }
60
+ },
61
+
62
+ return(): IteratorResult<Uint8Array> {
63
+ reader.releaseLock();
64
+ return { done: true, value: undefined };
65
+ },
66
+ };
67
+ }
68
+
69
+ async readAll(): Promise<Array<Uint8Array>> {
70
+ let chunks: Set<Uint8Array> = new Set();
71
+ for await (const chunk of this) {
72
+ chunks.add(chunk);
73
+ }
74
+ return Array.from(chunks);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * A class to read chunks from a ReadableStream and provide them in a structured format.
80
+ */
81
+ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
82
+ private receivedChunks: Map<number, DataStream_Chunk>;
83
+
84
+ /**
85
+ * A TextStreamReader instance can be used as an AsyncIterator that returns the entire string
86
+ * that has been received up to the current point in time.
87
+ */
88
+ constructor(
89
+ info: TextStreamInfo,
90
+ stream: ReadableStream<DataStream_Chunk>,
91
+ totalChunkCount?: number,
92
+ ) {
93
+ super(info, stream, totalChunkCount);
94
+ this.receivedChunks = new Map();
95
+ }
96
+
97
+ protected handleChunkReceived(chunk: DataStream_Chunk) {
98
+ const index = bigIntToNumber(chunk.chunkIndex);
99
+ const previousChunkAtIndex = this.receivedChunks.get(index);
100
+ if (previousChunkAtIndex && previousChunkAtIndex.version > chunk.version) {
101
+ // we have a newer version already, dropping the old one
102
+ return;
103
+ }
104
+ this.receivedChunks.set(index, chunk);
105
+ this.bytesReceived += chunk.content.byteLength;
106
+ const currentProgress = this.totalByteSize
107
+ ? this.bytesReceived / this.totalByteSize
108
+ : undefined;
109
+ this.onProgress?.(currentProgress);
110
+ }
111
+
112
+ /**
113
+ * @param progress - progress of the stream between 0 and 1. Undefined for streams of unknown size
114
+ */
115
+ onProgress?: (progress: number | undefined) => void;
116
+
117
+ /**
118
+ * Async iterator implementation to allow usage of `for await...of` syntax.
119
+ * Yields structured chunks from the stream.
120
+ *
121
+ */
122
+ [Symbol.asyncIterator]() {
123
+ const reader = this.reader.getReader();
124
+ const decoder = new TextDecoder();
125
+
126
+ return {
127
+ next: async (): Promise<IteratorResult<TextStreamChunk>> => {
128
+ try {
129
+ const { done, value } = await reader.read();
130
+ if (done) {
131
+ return { done: true, value: undefined };
132
+ } else {
133
+ this.handleChunkReceived(value);
134
+
135
+ return {
136
+ done: false,
137
+ value: {
138
+ index: bigIntToNumber(value.chunkIndex),
139
+ current: decoder.decode(value.content),
140
+ collected: Array.from(this.receivedChunks.values())
141
+ .sort((a, b) => bigIntToNumber(a.chunkIndex) - bigIntToNumber(b.chunkIndex))
142
+ .map((chunk) => decoder.decode(chunk.content))
143
+ .join(''),
144
+ },
145
+ };
146
+ }
147
+ } catch (error) {
148
+ // TODO handle errors
149
+ return { done: true, value: undefined };
150
+ }
151
+ },
152
+
153
+ return(): IteratorResult<TextStreamChunk> {
154
+ reader.releaseLock();
155
+ return { done: true, value: undefined };
156
+ },
157
+ };
158
+ }
159
+
160
+ async readAll(): Promise<string> {
161
+ let latestString: string = '';
162
+ for await (const { collected } of this) {
163
+ latestString = collected;
164
+ }
165
+ return latestString;
166
+ }
167
+ }
168
+
169
+ export type ByteStreamHandler = (
170
+ reader: ByteStreamReader,
171
+ participantInfo: { identity: string },
172
+ ) => void;
173
+
174
+ export type TextStreamHandler = (
175
+ reader: TextStreamReader,
176
+ participantInfo: { identity: string },
177
+ ) => void;
@@ -0,0 +1,32 @@
1
+ import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types';
2
+
3
+ class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
4
+ protected writableStream: WritableStream<[T, number?]>;
5
+
6
+ protected defaultWriter: WritableStreamDefaultWriter<[T, number?]>;
7
+
8
+ protected onClose?: () => void;
9
+
10
+ readonly info: InfoType;
11
+
12
+ constructor(writableStream: WritableStream<[T, number?]>, info: InfoType, onClose?: () => void) {
13
+ this.writableStream = writableStream;
14
+ this.defaultWriter = writableStream.getWriter();
15
+ this.onClose = onClose;
16
+ this.info = info;
17
+ }
18
+
19
+ write(chunk: T): Promise<void> {
20
+ return this.defaultWriter.write([chunk]);
21
+ }
22
+
23
+ async close() {
24
+ await this.defaultWriter.close();
25
+ this.defaultWriter.releaseLock();
26
+ this.onClose?.();
27
+ }
28
+ }
29
+
30
+ export class TextStreamWriter extends BaseStreamWriter<string, TextStreamInfo> {}
31
+
32
+ export class BinaryStreamWriter extends BaseStreamWriter<Uint8Array, ByteStreamInfo> {}