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
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;
@@ -1184,8 +1348,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1184
1348
  throw e;
1185
1349
  }
1186
1350
  }
1187
- if (needsUpdateWithoutTracks) {
1188
- this.localParticipant.activeDeviceMap.set(kind, deviceId);
1351
+ if (needsUpdateWithoutTracks || kind === 'audiooutput') {
1352
+ // if there are not active tracks yet or we're switching audiooutput, we need to manually update the active device map here as changing audio output won't result in a track restart
1353
+ this.localParticipant.activeDeviceMap.set(
1354
+ kind,
1355
+ (kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
1356
+ );
1189
1357
  this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
1190
1358
  }
1191
1359
 
@@ -1589,9 +1757,136 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1589
1757
  this.handleChatMessage(participant, packet.value.value);
1590
1758
  } else if (packet.value.case === 'metrics') {
1591
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
+ );
1592
1776
  }
1593
1777
  };
1594
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
+
1595
1890
  private handleUserPacket = (
1596
1891
  participant: RemoteParticipant | undefined,
1597
1892
  userPacket: UserPacket,
@@ -1767,24 +2062,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1767
2062
  this.audioContext = getNewAudioContext() ?? undefined;
1768
2063
  }
1769
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
+
1770
2073
  if (this.audioContext && this.audioContext.state === 'suspended') {
1771
2074
  // for iOS a newly created AudioContext is always in `suspended` state.
1772
2075
  // we try our best to resume the context here, if that doesn't work, we just continue with regular processing
1773
2076
  try {
1774
- await this.audioContext.resume();
2077
+ await Promise.race([this.audioContext.resume(), sleep(200)]);
1775
2078
  } catch (e: any) {
1776
2079
  this.log.warn('Could not resume audio context', { ...this.logContext, error: e });
1777
2080
  }
1778
2081
  }
1779
2082
 
1780
- if (this.options.webAudioMix) {
1781
- this.remoteParticipants.forEach((participant) =>
1782
- participant.setAudioContext(this.audioContext),
1783
- );
1784
- }
1785
-
1786
- this.localParticipant.setAudioContext(this.audioContext);
1787
-
1788
2083
  const newContextIsRunning = this.audioContext?.state === 'running';
1789
2084
  if (newContextIsRunning !== this.canPlaybackAudio) {
1790
2085
  this.audioEnabled = newContextIsRunning;
@@ -1873,9 +2168,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1873
2168
  this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
1874
2169
  },
1875
2170
  )
1876
- .on(ParticipantEvent.TrackSubscriptionFailed, (sid: string) => {
1877
- this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
1878
- })
1879
2171
  .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
1880
2172
  this.emitWhenConnected(RoomEvent.TrackMuted, pub, participant);
1881
2173
  })
@@ -1946,7 +2238,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1946
2238
  private updateSubscriptions() {
1947
2239
  for (const p of this.remoteParticipants.values()) {
1948
2240
  for (const pub of p.videoTrackPublications.values()) {
1949
- if (pub.isSubscribed && pub instanceof RemoteTrackPublication) {
2241
+ if (pub.isSubscribed && isRemotePub(pub)) {
1950
2242
  pub.emitTrackUpdate();
1951
2243
  }
1952
2244
  }
@@ -2068,7 +2360,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2068
2360
 
2069
2361
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
2070
2362
 
2071
- if (pub.track instanceof LocalAudioTrack) {
2363
+ if (isLocalAudioTrack(pub.track)) {
2072
2364
  const trackIsSilent = await pub.track.checkForSilence();
2073
2365
  if (trackIsSilent) {
2074
2366
  this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
@@ -2296,7 +2588,7 @@ function mapArgs(args: unknown[]): any {
2296
2588
  return mapArgs(arg);
2297
2589
  }
2298
2590
  if (typeof arg === 'object') {
2299
- return 'logContext' in arg && arg.logContext;
2591
+ return 'logContext' in arg ? arg.logContext : undefined;
2300
2592
  }
2301
2593
  return arg;
2302
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> {}