livekit-client 2.18.0 → 2.18.2

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 (145) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +8 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +8026 -5883
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +12 -4
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  12. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  13. package/dist/src/e2ee/constants.d.ts.map +1 -1
  14. package/dist/src/e2ee/types.d.ts +6 -0
  15. package/dist/src/e2ee/types.d.ts.map +1 -1
  16. package/dist/src/e2ee/utils.d.ts +2 -1
  17. package/dist/src/e2ee/utils.d.ts.map +1 -1
  18. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  19. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  20. package/dist/src/index.d.ts +6 -4
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/room/PCTransport.d.ts +5 -0
  23. package/dist/src/room/PCTransport.d.ts.map +1 -1
  24. package/dist/src/room/PCTransportManager.d.ts +1 -1
  25. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  26. package/dist/src/room/RTCEngine.d.ts +28 -11
  27. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  28. package/dist/src/room/Room.d.ts +14 -3
  29. package/dist/src/room/Room.d.ts.map +1 -1
  30. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +5 -1
  31. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  32. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  33. package/dist/src/room/data-track/LocalDataTrack.d.ts +28 -5
  34. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  35. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -5
  36. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  37. package/dist/src/room/data-track/depacketizer.d.ts +4 -4
  38. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  39. package/dist/src/room/data-track/frame.d.ts +14 -0
  40. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  41. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +19 -11
  42. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  43. package/dist/src/room/data-track/incoming/pipeline.d.ts +6 -5
  44. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  45. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  46. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  47. package/dist/src/room/data-track/outgoing/errors.d.ts +16 -6
  48. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -1
  49. package/dist/src/room/data-track/outgoing/pipeline.d.ts +7 -6
  50. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -1
  51. package/dist/src/room/data-track/outgoing/types.d.ts +14 -4
  52. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  53. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  54. package/dist/src/room/data-track/packetizer.d.ts +4 -4
  55. package/dist/src/room/data-track/packetizer.d.ts.map +1 -1
  56. package/dist/src/room/data-track/track-interfaces.d.ts +1 -1
  57. package/dist/src/room/data-track/track-interfaces.d.ts.map +1 -1
  58. package/dist/src/room/data-track/types.d.ts +6 -1
  59. package/dist/src/room/data-track/types.d.ts.map +1 -1
  60. package/dist/src/room/events.d.ts +24 -3
  61. package/dist/src/room/events.d.ts.map +1 -1
  62. package/dist/src/room/participant/LocalParticipant.d.ts +12 -1
  63. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  64. package/dist/src/room/participant/RemoteParticipant.d.ts +13 -0
  65. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  66. package/dist/src/room/utils.d.ts +1 -0
  67. package/dist/src/room/utils.d.ts.map +1 -1
  68. package/dist/src/utils/deferrable-map.d.ts +32 -0
  69. package/dist/src/utils/deferrable-map.d.ts.map +1 -0
  70. package/dist/src/utils/serializer.d.ts +48 -0
  71. package/dist/src/utils/serializer.d.ts.map +1 -0
  72. package/dist/ts4.2/api/SignalClient.d.ts +12 -4
  73. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +2 -1
  74. package/dist/ts4.2/e2ee/types.d.ts +6 -0
  75. package/dist/ts4.2/e2ee/utils.d.ts +2 -1
  76. package/dist/ts4.2/index.d.ts +7 -4
  77. package/dist/ts4.2/room/PCTransport.d.ts +5 -0
  78. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -1
  79. package/dist/ts4.2/room/RTCEngine.d.ts +28 -11
  80. package/dist/ts4.2/room/Room.d.ts +14 -3
  81. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +5 -1
  82. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +27 -4
  83. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +4 -4
  84. package/dist/ts4.2/room/data-track/depacketizer.d.ts +4 -4
  85. package/dist/ts4.2/room/data-track/frame.d.ts +14 -0
  86. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +21 -10
  87. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +6 -5
  88. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  89. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +16 -6
  90. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +7 -6
  91. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +14 -4
  92. package/dist/ts4.2/room/data-track/packetizer.d.ts +4 -4
  93. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +1 -1
  94. package/dist/ts4.2/room/data-track/types.d.ts +6 -1
  95. package/dist/ts4.2/room/events.d.ts +24 -3
  96. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +12 -1
  97. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +13 -0
  98. package/dist/ts4.2/room/utils.d.ts +1 -0
  99. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -0
  100. package/dist/ts4.2/utils/serializer.d.ts +48 -0
  101. package/package.json +1 -1
  102. package/src/api/SignalClient.test.ts +9 -4
  103. package/src/api/SignalClient.ts +116 -9
  104. package/src/connectionHelper/ConnectionCheck.ts +1 -1
  105. package/src/e2ee/constants.ts +1 -0
  106. package/src/e2ee/types.ts +6 -0
  107. package/src/e2ee/utils.ts +4 -3
  108. package/src/e2ee/worker/DataCryptor.ts +1 -4
  109. package/src/e2ee/worker/FrameCryptor.ts +1 -4
  110. package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
  111. package/src/index.ts +13 -4
  112. package/src/room/PCTransport.ts +41 -1
  113. package/src/room/PCTransportManager.ts +1 -1
  114. package/src/room/RTCEngine.ts +312 -125
  115. package/src/room/Room.ts +168 -35
  116. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +26 -2
  117. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -7
  118. package/src/room/data-track/LocalDataTrack.ts +83 -10
  119. package/src/room/data-track/RemoteDataTrack.ts +7 -9
  120. package/src/room/data-track/depacketizer.ts +21 -12
  121. package/src/room/data-track/frame.ts +28 -2
  122. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +58 -73
  123. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +139 -80
  124. package/src/room/data-track/incoming/pipeline.ts +29 -24
  125. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +225 -32
  126. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +150 -75
  127. package/src/room/data-track/outgoing/errors.ts +36 -7
  128. package/src/room/data-track/outgoing/pipeline.ts +23 -17
  129. package/src/room/data-track/outgoing/types.ts +12 -3
  130. package/src/room/data-track/packet/extensions.ts +17 -22
  131. package/src/room/data-track/packet/index.test.ts +22 -33
  132. package/src/room/data-track/packetizer.test.ts +2 -2
  133. package/src/room/data-track/packetizer.ts +4 -4
  134. package/src/room/data-track/track-interfaces.ts +1 -1
  135. package/src/room/data-track/types.ts +21 -1
  136. package/src/room/events.ts +26 -1
  137. package/src/room/participant/LocalParticipant.ts +74 -8
  138. package/src/room/participant/RemoteParticipant.ts +25 -0
  139. package/src/room/utils.ts +4 -0
  140. package/src/utils/deferrable-map.ts +109 -0
  141. package/src/utils/serializer.ts +72 -0
  142. package/dist/src/room/data-track/e2ee.d.ts +0 -12
  143. package/dist/src/room/data-track/e2ee.d.ts.map +0 -1
  144. package/dist/ts4.2/room/data-track/e2ee.d.ts +0 -12
  145. package/src/room/data-track/e2ee.ts +0 -15
package/src/room/Room.ts CHANGED
@@ -47,7 +47,7 @@ import TypedPromise from '../utils/TypedPromise';
47
47
  import { getBrowser } from '../utils/browserParser';
48
48
  import { BackOffStrategy } from './BackOffStrategy';
49
49
  import DeviceManager from './DeviceManager';
50
- import RTCEngine from './RTCEngine';
50
+ import RTCEngine, { DataChannelKind } from './RTCEngine';
51
51
  import { RegionUrlProvider } from './RegionUrlProvider';
52
52
  import IncomingDataStreamManager from './data-stream/incoming/IncomingDataStreamManager';
53
53
  import {
@@ -55,6 +55,11 @@ import {
55
55
  type TextStreamHandler,
56
56
  } from './data-stream/incoming/StreamReader';
57
57
  import OutgoingDataStreamManager from './data-stream/outgoing/OutgoingDataStreamManager';
58
+ import type LocalDataTrack from './data-track/LocalDataTrack';
59
+ import type RemoteDataTrack from './data-track/RemoteDataTrack';
60
+ import IncomingDataTrackManager from './data-track/incoming/IncomingDataTrackManager';
61
+ import OutgoingDataTrackManager from './data-track/outgoing/OutgoingDataTrackManager';
62
+ import { DataTrackInfo, type DataTrackSid } from './data-track/types';
58
63
  import {
59
64
  audioDefaults,
60
65
  publishDefaults,
@@ -70,7 +75,7 @@ import {
70
75
  } from './errors';
71
76
  import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
72
77
  import LocalParticipant from './participant/LocalParticipant';
73
- import type Participant from './participant/Participant';
78
+ import Participant from './participant/Participant';
74
79
  import { type ConnectionQuality, ParticipantKind } from './participant/Participant';
75
80
  import RemoteParticipant from './participant/RemoteParticipant';
76
81
  import { MAX_PAYLOAD_BYTES, RpcError, type RpcInvocationData, byteLength } from './rpc';
@@ -182,6 +187,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
182
187
 
183
188
  private e2eeManager: BaseE2EEManager | undefined;
184
189
 
190
+ private e2eeStateMutex: Mutex = new Mutex();
191
+
185
192
  private connectionReconcileInterval?: ReturnType<typeof setInterval>;
186
193
 
187
194
  private regionUrlProvider?: RegionUrlProvider;
@@ -205,6 +212,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
205
212
 
206
213
  private outgoingDataStreamManager: OutgoingDataStreamManager;
207
214
 
215
+ private incomingDataTrackManager: IncomingDataTrackManager;
216
+
217
+ private outgoingDataTrackManager: OutgoingDataTrackManager;
218
+
208
219
  private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
209
220
 
210
221
  get hasE2EESetup(): boolean {
@@ -243,8 +254,47 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
243
254
  this.incomingDataStreamManager = new IncomingDataStreamManager();
244
255
  this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
245
256
 
246
- this.disconnectLock = new Mutex();
257
+ this.incomingDataTrackManager = new IncomingDataTrackManager({ e2eeManager: this.e2eeManager });
258
+ this.incomingDataTrackManager
259
+ .on('sfuUpdateSubscription', (event) => {
260
+ this.engine.client.sendUpdateDataSubscription(event.sid, event.subscribe);
261
+ })
262
+ .on('trackPublished', (event) => {
263
+ if (event.track.publisherIdentity === this.localParticipant.identity) {
264
+ // Only advertize tracks from other participants
265
+ return;
266
+ }
267
+ this.emit(RoomEvent.DataTrackPublished, event.track);
268
+ this.remoteParticipants.get(event.track.publisherIdentity)?.addRemoteDataTrack(event.track);
269
+ })
270
+ .on('trackUnpublished', (event) => {
271
+ if (event.publisherIdentity === this.localParticipant.identity) {
272
+ // Only advertize tracks from other participants
273
+ return;
274
+ }
275
+ this.emit(RoomEvent.DataTrackUnpublished, event.sid);
276
+ this.remoteParticipants.get(event.publisherIdentity)?.removeRemoteDataTrack(event.sid);
277
+ });
247
278
 
279
+ this.outgoingDataTrackManager = new OutgoingDataTrackManager({ e2eeManager: this.e2eeManager });
280
+ this.outgoingDataTrackManager
281
+ .on('sfuPublishRequest', (event) => {
282
+ this.engine.client.sendPublishDataTrackRequest(event.handle, event.name, event.usesE2ee);
283
+ })
284
+ .on('sfuUnpublishRequest', (event) => {
285
+ this.engine.client.sendUnPublishDataTrackRequest(event.handle);
286
+ })
287
+ .on('trackPublished', (event) => {
288
+ this.emit(RoomEvent.LocalDataTrackPublished, event.track);
289
+ })
290
+ .on('trackUnpublished', (event) => {
291
+ this.emit(RoomEvent.LocalDataTrackUnpublished, event.sid);
292
+ })
293
+ .on('packetAvailable', ({ bytes }) => {
294
+ this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait');
295
+ });
296
+
297
+ this.disconnectLock = new Mutex();
248
298
  this.localParticipant = new LocalParticipant(
249
299
  '',
250
300
  '',
@@ -252,6 +302,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
252
302
  this.options,
253
303
  this.rpcHandlers,
254
304
  this.outgoingDataStreamManager,
305
+ this.outgoingDataTrackManager,
255
306
  );
256
307
 
257
308
  if (this.options.e2ee || this.options.encryption) {
@@ -259,6 +310,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
259
310
  }
260
311
 
261
312
  this.engine.e2eeManager = this.e2eeManager;
313
+ this.incomingDataTrackManager.updateE2eeManager(this.e2eeManager ?? null);
314
+ this.outgoingDataTrackManager.updateE2eeManager(this.e2eeManager ?? null);
262
315
 
263
316
  if (this.options.videoCaptureDefaults.deviceId) {
264
317
  this.localParticipant.activeDeviceMap.set(
@@ -359,13 +412,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
359
412
  * @experimental
360
413
  */
361
414
  async setE2EEEnabled(enabled: boolean) {
362
- if (this.e2eeManager) {
363
- await Promise.all([this.localParticipant.setE2EEEnabled(enabled)]);
364
- if (this.localParticipant.identity !== '') {
365
- this.e2eeManager.setParticipantCryptorEnabled(enabled, this.localParticipant.identity);
415
+ const unlock = await this.e2eeStateMutex.lock();
416
+ try {
417
+ if (this.e2eeManager) {
418
+ if (this.isE2EEEnabled !== enabled) {
419
+ await this.localParticipant.setE2EEEnabled(enabled);
420
+
421
+ if (this.localParticipant.identity !== '') {
422
+ this.e2eeManager.setParticipantCryptorEnabled(enabled, this.localParticipant.identity);
423
+ }
424
+ }
425
+ } else {
426
+ throw Error('e2ee not configured, please set e2ee settings within the room options');
366
427
  }
367
- } else {
368
- throw Error('e2ee not configured, please set e2ee settings within the room options');
428
+ } finally {
429
+ unlock();
369
430
  }
370
431
  }
371
432
 
@@ -398,6 +459,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
398
459
  this.emit(RoomEvent.EncryptionError, error, participant);
399
460
  });
400
461
  this.e2eeManager?.setup(this);
462
+ this.e2eeManager?.setupEngine(this.engine);
401
463
  }
402
464
  }
403
465
 
@@ -515,6 +577,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
515
577
  }
516
578
  })
517
579
  .on(EngineEvent.Restarting, this.handleRestarting)
580
+ .on(EngineEvent.Restarted, this.handleRestarted)
518
581
  .on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
519
582
  .on(EngineEvent.Offline, () => {
520
583
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
@@ -560,6 +623,66 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
560
623
  } else {
561
624
  this.handleParticipantUpdates(roomMoved.otherParticipants);
562
625
  }
626
+ })
627
+ .on(EngineEvent.PublishDataTrackResponse, (event) => {
628
+ if (!event.info) {
629
+ this.log.warn(
630
+ `received PublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
631
+ this.logContext,
632
+ );
633
+ return;
634
+ }
635
+
636
+ this.outgoingDataTrackManager.receivedSfuPublishResponse(event.info.pubHandle, {
637
+ type: 'ok',
638
+ data: {
639
+ sid: event.info.sid,
640
+ pubHandle: event.info.pubHandle,
641
+ name: event.info.name,
642
+ usesE2ee: event.info.encryption !== Encryption_Type.NONE,
643
+ },
644
+ });
645
+ })
646
+ .on(EngineEvent.UnPublishDataTrackResponse, (event) => {
647
+ if (!event.info) {
648
+ this.log.warn(
649
+ `received UnPublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
650
+ this.logContext,
651
+ );
652
+ return;
653
+ }
654
+
655
+ this.outgoingDataTrackManager.receivedSfuUnpublishResponse(event.info.pubHandle);
656
+ })
657
+ .on(EngineEvent.DataTrackSubscriberHandles, (event) => {
658
+ const handleToSidMapping = new Map(
659
+ Object.entries(event.subHandles).map(([key, value]) => {
660
+ return [parseInt(key, 10), value.trackSid];
661
+ }),
662
+ );
663
+
664
+ this.incomingDataTrackManager.receivedSfuSubscriberHandles(handleToSidMapping);
665
+ })
666
+ .on(EngineEvent.DataTrackPacketReceived, (packetBytes) => {
667
+ try {
668
+ this.incomingDataTrackManager.packetReceived(packetBytes);
669
+ } catch (err) {
670
+ // NOTE: wrapping in the bare try/catch like this means that the Throws<...> type doesn't
671
+ // propagate upwards into the public interface.
672
+ throw err;
673
+ }
674
+ })
675
+ .on(EngineEvent.Joined, (joinResponse) => {
676
+ // Ingest data track publication updates into data tracks infrastructure
677
+ const mapped = new Map(
678
+ joinResponse.otherParticipants.map((participant) => {
679
+ return [
680
+ participant.identity,
681
+ participant.dataTracks.map((info) => DataTrackInfo.from(info)),
682
+ ];
683
+ }),
684
+ );
685
+ this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
563
686
  });
564
687
 
565
688
  if (this.localParticipant) {
@@ -782,7 +905,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
782
905
  roomOptions: InternalRoomOptions,
783
906
  abortController: AbortController,
784
907
  ): Promise<JoinResponse> => {
785
- const joinResponse = await engine.join(
908
+ const { joinResponse, serverInfo } = await engine.join(
786
909
  url,
787
910
  token,
788
911
  {
@@ -797,23 +920,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
797
920
  !roomOptions.singlePeerConnection,
798
921
  );
799
922
 
800
- let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
801
- if (!serverInfo) {
802
- serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
803
- }
804
923
  this.serverInfo = serverInfo;
805
924
 
806
- this.log.debug(
807
- `connected to Livekit Server ${Object.entries(serverInfo)
808
- .map(([key, value]) => `${key}: ${value}`)
809
- .join(', ')}`,
810
- {
811
- room: joinResponse.room?.name,
812
- roomSid: joinResponse.room?.sid,
813
- identity: joinResponse.participant?.identity,
814
- },
815
- );
816
-
817
925
  if (!serverInfo.version) {
818
926
  throw new UnsupportedServer('unknown server version');
819
927
  }
@@ -1456,10 +1564,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1456
1564
  ) as RemoteParticipant | undefined;
1457
1565
 
1458
1566
  if (!participant) {
1459
- this.log.error(
1460
- `Tried to add a track for a participant, that's not present. Sid: ${participantSid}`,
1461
- this.logContext,
1462
- );
1567
+ // server could require extra media sections to accelerate subscription.
1568
+ if (participantSid.startsWith('PA')) {
1569
+ this.log.error(
1570
+ `Tried to add a track for a participant, that's not present. Sid: ${participantSid}`,
1571
+ this.logContext,
1572
+ );
1573
+ }
1463
1574
  return;
1464
1575
  }
1465
1576
 
@@ -1527,6 +1638,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1527
1638
  }
1528
1639
  };
1529
1640
 
1641
+ private handleRestarted = () => {
1642
+ this.outgoingDataTrackManager.sfuWillRepublishTracks();
1643
+ this.incomingDataTrackManager.resendSubscriptionUpdates();
1644
+ };
1645
+
1530
1646
  private handleSignalRestarted = async (joinResponse: JoinResponse) => {
1531
1647
  this.log.debug(`signal reconnected to server, region ${joinResponse.serverRegion}`, {
1532
1648
  ...this.logContext,
@@ -1640,10 +1756,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1640
1756
 
1641
1757
  private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
1642
1758
  // handle changes to participant state, and send events
1643
- participantInfos.forEach((info) => {
1759
+ for (const info of participantInfos) {
1644
1760
  if (info.identity === this.localParticipant.identity) {
1645
1761
  this.localParticipant.updateInfo(info);
1646
- return;
1762
+ continue;
1647
1763
  }
1648
1764
 
1649
1765
  // LiveKit server doesn't send identity info prior to version 1.5.2 in disconnect updates
@@ -1661,7 +1777,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1661
1777
  // create participant if doesn't exist
1662
1778
  remoteParticipant = this.getOrCreateParticipant(info.identity, info);
1663
1779
  }
1664
- });
1780
+ }
1781
+
1782
+ // Ingest data track publication updates into data tracks infrastructure
1783
+ const mapped = new Map(
1784
+ participantInfos
1785
+ .filter((p) => p.identity !== this.localParticipant.identity)
1786
+ .map((info) => {
1787
+ return [info.identity, info.dataTracks.map((dataTrack) => DataTrackInfo.from(dataTrack))];
1788
+ }),
1789
+ );
1790
+ this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
1665
1791
  };
1666
1792
 
1667
1793
  private handleParticipantDisconnected(identity: string, participant?: RemoteParticipant) {
@@ -1672,6 +1798,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1672
1798
  }
1673
1799
 
1674
1800
  this.incomingDataStreamManager.validateParticipantHasNoActiveDataStreams(identity);
1801
+ this.incomingDataTrackManager.handleRemoteParticipantDisconnected(identity);
1675
1802
 
1676
1803
  participant.trackPublications.forEach((publication) => {
1677
1804
  participant.unpublishTrack(publication.trackSid, true);
@@ -2192,7 +2319,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2192
2319
  track.on(TrackEvent.VideoPlaybackFailed, this.handleVideoPlaybackFailed);
2193
2320
  track.on(TrackEvent.VideoPlaybackStarted, this.handleVideoPlaybackStarted);
2194
2321
  }
2195
- this.emit(RoomEvent.TrackSubscribed, track, publication, participant);
2322
+ this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
2196
2323
  },
2197
2324
  )
2198
2325
  .on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
@@ -2270,7 +2397,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2270
2397
  return acc;
2271
2398
  }, [] as RemoteTrackPublication[]);
2272
2399
  const localTracks = this.localParticipant.getTrackPublications() as LocalTrackPublication[]; // FIXME would be nice to have this return LocalTrackPublications directly instead of the type cast
2273
- this.engine.sendSyncState(remoteTracks, localTracks);
2400
+ const localDataTrackInfos = this.outgoingDataTrackManager.queryPublished();
2401
+ this.engine.sendSyncState(remoteTracks, localTracks, localDataTrackInfos);
2274
2402
  }
2275
2403
 
2276
2404
  /**
@@ -2342,6 +2470,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2342
2470
  return false;
2343
2471
  }
2344
2472
  this.state = state;
2473
+ this.incomingDataStreamManager.setConnected(state === ConnectionState.Connected);
2345
2474
  this.emit(RoomEvent.ConnectionStateChanged, this.state);
2346
2475
  return true;
2347
2476
  }
@@ -2732,10 +2861,14 @@ export type RoomEventCallbacks = {
2732
2861
  recordingStatusChanged: (recording: boolean) => void;
2733
2862
  participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
2734
2863
  encryptionError: (error: Error, participant?: Participant) => void;
2735
- dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
2864
+ dcBufferStatusChanged: (isLow: boolean, kind: DataChannelKind) => void;
2736
2865
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
2737
2866
  chatMessage: (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant) => void;
2738
2867
  localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
2739
2868
  metricsReceived: (metrics: MetricsBatch, participant?: Participant) => void;
2740
2869
  participantActive: (participant: Participant) => void;
2870
+ dataTrackPublished: (track: RemoteDataTrack) => void;
2871
+ dataTrackUnpublished: (sid: DataTrackSid) => void;
2872
+ localDataTrackPublished: (track: LocalDataTrack) => void;
2873
+ localDataTrackUnpublished: (sid: DataTrackSid) => void;
2741
2874
  };
@@ -27,6 +27,25 @@ export default class IncomingDataStreamManager {
27
27
 
28
28
  private textStreamHandlers = new Map<string, TextStreamHandler>();
29
29
 
30
+ private isConnected = false;
31
+
32
+ private bufferedPackets: Array<{ packet: DataPacket; encryptionType: Encryption_Type }> = [];
33
+
34
+ setConnected(connected: boolean) {
35
+ this.isConnected = connected;
36
+ if (connected) {
37
+ this.flushBufferedPackets();
38
+ }
39
+ }
40
+
41
+ private flushBufferedPackets() {
42
+ const packets = this.bufferedPackets;
43
+ this.bufferedPackets = [];
44
+ for (const { packet, encryptionType } of packets) {
45
+ this.handleDataStreamPacket(packet, encryptionType);
46
+ }
47
+ }
48
+
30
49
  registerTextStreamHandler(topic: string, callback: TextStreamHandler) {
31
50
  if (this.textStreamHandlers.has(topic)) {
32
51
  throw new DataStreamError(
@@ -58,6 +77,7 @@ export default class IncomingDataStreamManager {
58
77
  clearControllers() {
59
78
  this.byteStreamControllers.clear();
60
79
  this.textStreamControllers.clear();
80
+ this.bufferedPackets = [];
61
81
  }
62
82
 
63
83
  validateParticipantHasNoActiveDataStreams(participantIdentity: string) {
@@ -88,7 +108,11 @@ export default class IncomingDataStreamManager {
88
108
  }
89
109
  }
90
110
 
91
- async handleDataStreamPacket(packet: DataPacket, encryptionType: Encryption_Type) {
111
+ handleDataStreamPacket(packet: DataPacket, encryptionType: Encryption_Type) {
112
+ if (!this.isConnected) {
113
+ this.bufferedPackets.push({ packet, encryptionType });
114
+ return;
115
+ }
92
116
  switch (packet.value.case) {
93
117
  case 'streamHeader':
94
118
  return this.handleStreamHeader(
@@ -105,7 +129,7 @@ export default class IncomingDataStreamManager {
105
129
  }
106
130
  }
107
131
 
108
- private async handleStreamHeader(
132
+ private handleStreamHeader(
109
133
  streamHeader: DataStream_Header,
110
134
  participantIdentity: string,
111
135
  encryptionType: Encryption_Type,
@@ -1,7 +1,6 @@
1
1
  import { Mutex } from '@livekit/mutex';
2
2
  import {
3
3
  DataPacket,
4
- DataPacket_Kind,
5
4
  DataStream_ByteHeader,
6
5
  DataStream_Chunk,
7
6
  DataStream_Header,
@@ -12,6 +11,7 @@ import {
12
11
  } from '@livekit/protocol';
13
12
  import { type StructuredLogger } from '../../../logger';
14
13
  import type RTCEngine from '../../RTCEngine';
14
+ import { DataChannelKind } from '../../RTCEngine';
15
15
  import { EngineEvent } from '../../events';
16
16
  import type {
17
17
  ByteStreamInfo,
@@ -137,7 +137,7 @@ export default class OutgoingDataStreamManager {
137
137
  value: header,
138
138
  },
139
139
  });
140
- await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
140
+ await this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
141
141
 
142
142
  let chunkId = 0;
143
143
  const engine = this.engine;
@@ -158,7 +158,7 @@ export default class OutgoingDataStreamManager {
158
158
  value: chunk,
159
159
  },
160
160
  });
161
- await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
161
+ await engine.sendDataPacket(chunkPacket, DataChannelKind.RELIABLE);
162
162
 
163
163
  chunkId += 1;
164
164
  }
@@ -174,7 +174,7 @@ export default class OutgoingDataStreamManager {
174
174
  value: trailer,
175
175
  },
176
176
  });
177
- await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
177
+ await engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
178
178
  },
179
179
  abort(err) {
180
180
  console.log('Sink error:', err);
@@ -262,7 +262,7 @@ export default class OutgoingDataStreamManager {
262
262
  },
263
263
  });
264
264
 
265
- await this.engine.sendDataPacket(packet, DataPacket_Kind.RELIABLE);
265
+ await this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
266
266
 
267
267
  let chunkId = 0;
268
268
  const writeMutex = new Mutex();
@@ -288,7 +288,7 @@ export default class OutgoingDataStreamManager {
288
288
  }),
289
289
  },
290
290
  });
291
- await engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
291
+ await engine.sendDataPacket(chunkPacket, DataChannelKind.RELIABLE);
292
292
  chunkId += 1;
293
293
  byteOffset += subChunk.byteLength;
294
294
  }
@@ -307,7 +307,7 @@ export default class OutgoingDataStreamManager {
307
307
  value: trailer,
308
308
  },
309
309
  });
310
- await engine.sendDataPacket(trailerPacket, DataPacket_Kind.RELIABLE);
310
+ await engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
311
311
  },
312
312
  abort(err) {
313
313
  logLocal.error('Sink error:', err);
@@ -1,5 +1,9 @@
1
- import type { DataTrackFrame } from './frame';
1
+ import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
2
+ import { type DataTrackFrame, DataTrackFrameInternal } from './frame';
3
+ import type { DataTrackHandle } from './handle';
2
4
  import type OutgoingDataTrackManager from './outgoing/OutgoingDataTrackManager';
5
+ import { DataTrackPushFrameError } from './outgoing/errors';
6
+ import type { DataTrackOptions } from './outgoing/types';
3
7
  import {
4
8
  DataTrackSymbol,
5
9
  type IDataTrack,
@@ -15,23 +19,66 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
15
19
 
16
20
  readonly typeSymbol = DataTrackSymbol;
17
21
 
18
- info: DataTrackInfo;
22
+ protected options: DataTrackOptions;
23
+
24
+ /** Represents the currently active {@link DataTrackHandle} for the publication. */
25
+ protected handle: DataTrackHandle | null = null;
19
26
 
20
27
  protected manager: OutgoingDataTrackManager;
21
28
 
29
+ protected log: StructuredLogger = log;
30
+
22
31
  /** @internal */
23
- constructor(info: DataTrackInfo, manager: OutgoingDataTrackManager) {
24
- this.info = info;
32
+ constructor(options: DataTrackOptions, manager: OutgoingDataTrackManager) {
33
+ this.options = options;
25
34
  this.manager = manager;
35
+
36
+ this.log = getLogger(LoggerNames.DataTracks);
37
+ }
38
+
39
+ /** @internal */
40
+ static withExplicitHandle(
41
+ options: DataTrackOptions,
42
+ manager: OutgoingDataTrackManager,
43
+ handle: DataTrackHandle,
44
+ ) {
45
+ const track = new LocalDataTrack(options, manager);
46
+ track.handle = handle;
47
+ return track;
48
+ }
49
+
50
+ /** Metrics about the data track publication. */
51
+ get info() {
52
+ const descriptor = this.descriptor;
53
+ if (descriptor?.type === 'active') {
54
+ return descriptor.info;
55
+ } else {
56
+ return undefined;
57
+ }
26
58
  }
27
59
 
28
60
  /** The raw descriptor from the manager containing the internal state for this local track. */
29
61
  protected get descriptor() {
30
- return this.manager.getDescriptor(this.info.pubHandle);
62
+ return this.handle ? this.manager.getDescriptor(this.handle) : null;
63
+ }
64
+
65
+ /**
66
+ * Publish the track to the SFU. This must be done before calling {@link tryPush} for the first time.
67
+ * @internal
68
+ * */
69
+ async publish(signal?: AbortSignal) {
70
+ try {
71
+ this.handle = await this.manager.publishRequest(this.options, signal);
72
+ } catch (err) {
73
+ // NOTE: Rethrow errors to break Throws<...> type boundary
74
+ throw err;
75
+ }
31
76
  }
32
77
 
33
- isPublished() {
34
- return this.descriptor?.type === 'active';
78
+ isPublished(): this is { info: DataTrackInfo } {
79
+ // NOTE: a track which is internally in the "resubscribing" state is still considered
80
+ // published from the public API perspective.
81
+ return this.descriptor?.type === 'active' && this.descriptor.publishState !== 'unpublished';
35
82
  }
36
83
 
37
84
  /** Try pushing a frame to subscribers of the track.
@@ -41,12 +88,38 @@ export default class LocalDataTrack implements ILocalTrack, IDataTrack {
41
88
  * - The track has been unpublished by the local participant or SFU
42
89
  * - The room is no longer connected
43
90
  */
44
- tryPush(payload: DataTrackFrame['payload']) {
91
+ tryPush(frame: DataTrackFrame) {
92
+ if (!this.handle) {
93
+ throw DataTrackPushFrameError.trackUnpublished();
94
+ }
95
+
96
+ const internalFrame = DataTrackFrameInternal.from(frame);
97
+
45
98
  try {
46
- return this.manager.tryProcessAndSend(this.info.pubHandle, payload);
99
+ return this.manager.tryProcessAndSend(this.handle, internalFrame);
47
100
  } catch (err) {
48
101
  // NOTE: wrapping in the bare try/catch like this means that the Throws<...> type doesn't
49
- // propegate upwards into the public interface.
102
+ // propagate upwards into the public interface.
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Unpublish the track from the SFU. Once this is called, any further calls to {@link tryPush}
109
+ * will fail.
110
+ * */
111
+ async unpublish() {
112
+ if (!this.handle) {
113
+ log.warn(
114
+ `Data track "${this.options.name}" is not published, so unpublishing has no effect.`,
115
+ );
116
+ return;
117
+ }
118
+
119
+ try {
120
+ await this.manager.unpublishRequest(this.handle);
121
+ } catch (err) {
122
+ // NOTE: Rethrow errors to break Throws<...> type boundary
50
123
  throw err;
51
124
  }
52
125
  }
@@ -1,5 +1,5 @@
1
1
  import type Participant from '../participant/Participant';
2
- import type { DataTrackFrame } from './frame';
2
+ import { type DataTrackFrame } from './frame';
3
3
  import type IncomingDataTrackManager from './incoming/IncomingDataTrackManager';
4
4
  import {
5
5
  DataTrackSymbol,
@@ -13,12 +13,12 @@ type RemoteDataTrackOptions = {
13
13
  publisherIdentity: Participant['identity'];
14
14
  };
15
15
 
16
- export type RemoteDataTrackSubscribeOptions = {
16
+ export type DataTrackSubscribeOptions = {
17
17
  signal?: AbortSignal;
18
18
 
19
19
  /** The number of {@link DataTrackFrame}s to hold in the ReadableStream before disgarding extra
20
- * frames. Defaults to 4, but this may not be good enough for especially high frequency data. */
21
- highWaterMark?: number;
20
+ * frames. Defaults to 16, but this may not be good enough for especially high frequency data. */
21
+ bufferSize?: number;
22
22
  };
23
23
 
24
24
  export default class RemoteDataTrack implements IRemoteTrack, IDataTrack {
@@ -64,14 +64,12 @@ export default class RemoteDataTrack implements IRemoteTrack, IDataTrack {
64
64
  * Note that newly created subscriptions only receive frames published after
65
65
  * the initial subscription is established.
66
66
  */
67
- async subscribe(
68
- options?: RemoteDataTrackSubscribeOptions,
69
- ): Promise<ReadableStream<DataTrackFrame>> {
67
+ subscribe(options?: DataTrackSubscribeOptions): ReadableStream<DataTrackFrame> {
70
68
  try {
71
- const stream = await this.manager.subscribeRequest(
69
+ const [stream] = this.manager.openSubscriptionStream(
72
70
  this.info.sid,
73
71
  options?.signal,
74
- options?.highWaterMark,
72
+ options?.bufferSize,
75
73
  );
76
74
  return stream;
77
75
  } catch (err) {