livekit-client 2.18.0 → 2.18.1

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 (133) 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 +7832 -5799
  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/e2ee/constants.d.ts.map +1 -1
  12. package/dist/src/e2ee/types.d.ts +6 -0
  13. package/dist/src/e2ee/types.d.ts.map +1 -1
  14. package/dist/src/e2ee/utils.d.ts +2 -1
  15. package/dist/src/e2ee/utils.d.ts.map +1 -1
  16. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  17. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  18. package/dist/src/index.d.ts +5 -4
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/room/PCTransport.d.ts +5 -0
  21. package/dist/src/room/PCTransport.d.ts.map +1 -1
  22. package/dist/src/room/PCTransportManager.d.ts +1 -1
  23. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  24. package/dist/src/room/RTCEngine.d.ts +24 -8
  25. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +13 -3
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  29. package/dist/src/room/data-track/LocalDataTrack.d.ts +28 -5
  30. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  31. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -5
  32. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  33. package/dist/src/room/data-track/depacketizer.d.ts +4 -4
  34. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  35. package/dist/src/room/data-track/frame.d.ts +14 -0
  36. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  37. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +19 -11
  38. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  39. package/dist/src/room/data-track/incoming/pipeline.d.ts +6 -5
  40. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  41. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  42. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  43. package/dist/src/room/data-track/outgoing/errors.d.ts +16 -6
  44. package/dist/src/room/data-track/outgoing/errors.d.ts.map +1 -1
  45. package/dist/src/room/data-track/outgoing/pipeline.d.ts +7 -6
  46. package/dist/src/room/data-track/outgoing/pipeline.d.ts.map +1 -1
  47. package/dist/src/room/data-track/outgoing/types.d.ts +14 -4
  48. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  49. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  50. package/dist/src/room/data-track/packetizer.d.ts +4 -4
  51. package/dist/src/room/data-track/packetizer.d.ts.map +1 -1
  52. package/dist/src/room/data-track/track-interfaces.d.ts +1 -1
  53. package/dist/src/room/data-track/track-interfaces.d.ts.map +1 -1
  54. package/dist/src/room/data-track/types.d.ts +6 -1
  55. package/dist/src/room/data-track/types.d.ts.map +1 -1
  56. package/dist/src/room/events.d.ts +24 -3
  57. package/dist/src/room/events.d.ts.map +1 -1
  58. package/dist/src/room/participant/LocalParticipant.d.ts +11 -1
  59. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  60. package/dist/src/room/participant/RemoteParticipant.d.ts +13 -0
  61. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  62. package/dist/src/room/utils.d.ts +1 -0
  63. package/dist/src/room/utils.d.ts.map +1 -1
  64. package/dist/src/utils/deferrable-map.d.ts +32 -0
  65. package/dist/src/utils/deferrable-map.d.ts.map +1 -0
  66. package/dist/ts4.2/api/SignalClient.d.ts +12 -4
  67. package/dist/ts4.2/e2ee/types.d.ts +6 -0
  68. package/dist/ts4.2/e2ee/utils.d.ts +2 -1
  69. package/dist/ts4.2/index.d.ts +5 -4
  70. package/dist/ts4.2/room/PCTransport.d.ts +5 -0
  71. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -1
  72. package/dist/ts4.2/room/RTCEngine.d.ts +24 -8
  73. package/dist/ts4.2/room/Room.d.ts +13 -3
  74. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +27 -4
  75. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +4 -4
  76. package/dist/ts4.2/room/data-track/depacketizer.d.ts +4 -4
  77. package/dist/ts4.2/room/data-track/frame.d.ts +14 -0
  78. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +21 -10
  79. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +6 -5
  80. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +57 -23
  81. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +16 -6
  82. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +7 -6
  83. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +14 -4
  84. package/dist/ts4.2/room/data-track/packetizer.d.ts +4 -4
  85. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +1 -1
  86. package/dist/ts4.2/room/data-track/types.d.ts +6 -1
  87. package/dist/ts4.2/room/events.d.ts +24 -3
  88. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +11 -1
  89. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +13 -0
  90. package/dist/ts4.2/room/utils.d.ts +1 -0
  91. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -0
  92. package/package.json +1 -1
  93. package/src/api/SignalClient.test.ts +9 -4
  94. package/src/api/SignalClient.ts +116 -9
  95. package/src/e2ee/constants.ts +1 -0
  96. package/src/e2ee/types.ts +6 -0
  97. package/src/e2ee/utils.ts +4 -3
  98. package/src/e2ee/worker/DataCryptor.ts +1 -4
  99. package/src/e2ee/worker/FrameCryptor.ts +1 -4
  100. package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -1
  101. package/src/index.ts +6 -4
  102. package/src/room/PCTransport.ts +41 -1
  103. package/src/room/PCTransportManager.ts +1 -1
  104. package/src/room/RTCEngine.ts +266 -111
  105. package/src/room/Room.ts +149 -12
  106. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -7
  107. package/src/room/data-track/LocalDataTrack.ts +83 -10
  108. package/src/room/data-track/RemoteDataTrack.ts +7 -9
  109. package/src/room/data-track/depacketizer.ts +21 -12
  110. package/src/room/data-track/frame.ts +28 -2
  111. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +58 -73
  112. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +132 -80
  113. package/src/room/data-track/incoming/pipeline.ts +29 -24
  114. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +225 -32
  115. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +150 -75
  116. package/src/room/data-track/outgoing/errors.ts +36 -7
  117. package/src/room/data-track/outgoing/pipeline.ts +23 -17
  118. package/src/room/data-track/outgoing/types.ts +12 -3
  119. package/src/room/data-track/packet/extensions.ts +17 -22
  120. package/src/room/data-track/packet/index.test.ts +22 -33
  121. package/src/room/data-track/packetizer.test.ts +2 -2
  122. package/src/room/data-track/packetizer.ts +4 -4
  123. package/src/room/data-track/track-interfaces.ts +1 -1
  124. package/src/room/data-track/types.ts +21 -1
  125. package/src/room/events.ts +26 -1
  126. package/src/room/participant/LocalParticipant.ts +57 -6
  127. package/src/room/participant/RemoteParticipant.ts +25 -0
  128. package/src/room/utils.ts +4 -0
  129. package/src/utils/deferrable-map.ts +109 -0
  130. package/dist/src/room/data-track/e2ee.d.ts +0 -12
  131. package/dist/src/room/data-track/e2ee.d.ts.map +0 -1
  132. package/dist/ts4.2/room/data-track/e2ee.d.ts +0 -12
  133. 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';
@@ -205,6 +210,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
205
210
 
206
211
  private outgoingDataStreamManager: OutgoingDataStreamManager;
207
212
 
213
+ private incomingDataTrackManager: IncomingDataTrackManager;
214
+
215
+ private outgoingDataTrackManager: OutgoingDataTrackManager;
216
+
208
217
  private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
209
218
 
210
219
  get hasE2EESetup(): boolean {
@@ -243,6 +252,46 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
243
252
  this.incomingDataStreamManager = new IncomingDataStreamManager();
244
253
  this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
245
254
 
255
+ this.incomingDataTrackManager = new IncomingDataTrackManager({ e2eeManager: this.e2eeManager });
256
+ this.incomingDataTrackManager
257
+ .on('sfuUpdateSubscription', (event) => {
258
+ this.engine.client.sendUpdateDataSubscription(event.sid, event.subscribe);
259
+ })
260
+ .on('trackPublished', (event) => {
261
+ if (event.track.publisherIdentity === this.localParticipant.identity) {
262
+ // Only advertize tracks from other participants
263
+ return;
264
+ }
265
+ this.emit(RoomEvent.DataTrackPublished, event.track);
266
+ this.remoteParticipants.get(event.track.publisherIdentity)?.addRemoteDataTrack(event.track);
267
+ })
268
+ .on('trackUnpublished', (event) => {
269
+ if (event.publisherIdentity === this.localParticipant.identity) {
270
+ // Only advertize tracks from other participants
271
+ return;
272
+ }
273
+ this.emit(RoomEvent.DataTrackUnpublished, event.sid);
274
+ this.remoteParticipants.get(event.publisherIdentity)?.removeRemoteDataTrack(event.sid);
275
+ });
276
+
277
+ this.outgoingDataTrackManager = new OutgoingDataTrackManager({ e2eeManager: this.e2eeManager });
278
+ this.outgoingDataTrackManager
279
+ .on('sfuPublishRequest', (event) => {
280
+ this.engine.client.sendPublishDataTrackRequest(event.handle, event.name, event.usesE2ee);
281
+ })
282
+ .on('sfuUnpublishRequest', (event) => {
283
+ this.engine.client.sendUnPublishDataTrackRequest(event.handle);
284
+ })
285
+ .on('trackPublished', (event) => {
286
+ this.emit(RoomEvent.LocalDataTrackPublished, event.track);
287
+ })
288
+ .on('trackUnpublished', (event) => {
289
+ this.emit(RoomEvent.LocalDataTrackUnpublished, event.sid);
290
+ })
291
+ .on('packetAvailable', ({ bytes }) => {
292
+ this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait');
293
+ });
294
+
246
295
  this.disconnectLock = new Mutex();
247
296
 
248
297
  this.localParticipant = new LocalParticipant(
@@ -252,6 +301,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
252
301
  this.options,
253
302
  this.rpcHandlers,
254
303
  this.outgoingDataStreamManager,
304
+ this.outgoingDataTrackManager,
255
305
  );
256
306
 
257
307
  if (this.options.e2ee || this.options.encryption) {
@@ -259,6 +309,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
259
309
  }
260
310
 
261
311
  this.engine.e2eeManager = this.e2eeManager;
312
+ this.incomingDataTrackManager.updateE2eeManager(this.e2eeManager ?? null);
313
+ this.outgoingDataTrackManager.updateE2eeManager(this.e2eeManager ?? null);
262
314
 
263
315
  if (this.options.videoCaptureDefaults.deviceId) {
264
316
  this.localParticipant.activeDeviceMap.set(
@@ -515,6 +567,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
515
567
  }
516
568
  })
517
569
  .on(EngineEvent.Restarting, this.handleRestarting)
570
+ .on(EngineEvent.Restarted, this.handleRestarted)
518
571
  .on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
519
572
  .on(EngineEvent.Offline, () => {
520
573
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
@@ -560,6 +613,66 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
560
613
  } else {
561
614
  this.handleParticipantUpdates(roomMoved.otherParticipants);
562
615
  }
616
+ })
617
+ .on(EngineEvent.PublishDataTrackResponse, (event) => {
618
+ if (!event.info) {
619
+ this.log.warn(
620
+ `received PublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
621
+ this.logContext,
622
+ );
623
+ return;
624
+ }
625
+
626
+ this.outgoingDataTrackManager.receivedSfuPublishResponse(event.info.pubHandle, {
627
+ type: 'ok',
628
+ data: {
629
+ sid: event.info.sid,
630
+ pubHandle: event.info.pubHandle,
631
+ name: event.info.name,
632
+ usesE2ee: event.info.encryption !== Encryption_Type.NONE,
633
+ },
634
+ });
635
+ })
636
+ .on(EngineEvent.UnPublishDataTrackResponse, (event) => {
637
+ if (!event.info) {
638
+ this.log.warn(
639
+ `received UnPublishDataTrackResponse, but event.info was ${event.info}, so skipping.`,
640
+ this.logContext,
641
+ );
642
+ return;
643
+ }
644
+
645
+ this.outgoingDataTrackManager.receivedSfuUnpublishResponse(event.info.pubHandle);
646
+ })
647
+ .on(EngineEvent.DataTrackSubscriberHandles, (event) => {
648
+ const handleToSidMapping = new Map(
649
+ Object.entries(event.subHandles).map(([key, value]) => {
650
+ return [parseInt(key, 10), value.trackSid];
651
+ }),
652
+ );
653
+
654
+ this.incomingDataTrackManager.receivedSfuSubscriberHandles(handleToSidMapping);
655
+ })
656
+ .on(EngineEvent.DataTrackPacketReceived, (packetBytes) => {
657
+ try {
658
+ this.incomingDataTrackManager.packetReceived(packetBytes);
659
+ } catch (err) {
660
+ // NOTE: wrapping in the bare try/catch like this means that the Throws<...> type doesn't
661
+ // propagate upwards into the public interface.
662
+ throw err;
663
+ }
664
+ })
665
+ .on(EngineEvent.Joined, (joinResponse) => {
666
+ // Ingest data track publication updates into data tracks infrastructure
667
+ const mapped = new Map(
668
+ joinResponse.otherParticipants.map((participant) => {
669
+ return [
670
+ participant.identity,
671
+ participant.dataTracks.map((info) => DataTrackInfo.from(info)),
672
+ ];
673
+ }),
674
+ );
675
+ this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
563
676
  });
564
677
 
565
678
  if (this.localParticipant) {
@@ -1456,10 +1569,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1456
1569
  ) as RemoteParticipant | undefined;
1457
1570
 
1458
1571
  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
- );
1572
+ // server could require extra media sections to accelerate subscription.
1573
+ if (participantSid.startsWith('PA')) {
1574
+ this.log.error(
1575
+ `Tried to add a track for a participant, that's not present. Sid: ${participantSid}`,
1576
+ this.logContext,
1577
+ );
1578
+ }
1463
1579
  return;
1464
1580
  }
1465
1581
 
@@ -1527,6 +1643,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1527
1643
  }
1528
1644
  };
1529
1645
 
1646
+ private handleRestarted = () => {
1647
+ this.outgoingDataTrackManager.sfuWillRepublishTracks();
1648
+ this.incomingDataTrackManager.resendSubscriptionUpdates();
1649
+ };
1650
+
1530
1651
  private handleSignalRestarted = async (joinResponse: JoinResponse) => {
1531
1652
  this.log.debug(`signal reconnected to server, region ${joinResponse.serverRegion}`, {
1532
1653
  ...this.logContext,
@@ -1640,10 +1761,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1640
1761
 
1641
1762
  private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
1642
1763
  // handle changes to participant state, and send events
1643
- participantInfos.forEach((info) => {
1764
+ for (const info of participantInfos) {
1644
1765
  if (info.identity === this.localParticipant.identity) {
1645
1766
  this.localParticipant.updateInfo(info);
1646
- return;
1767
+ continue;
1647
1768
  }
1648
1769
 
1649
1770
  // LiveKit server doesn't send identity info prior to version 1.5.2 in disconnect updates
@@ -1661,7 +1782,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1661
1782
  // create participant if doesn't exist
1662
1783
  remoteParticipant = this.getOrCreateParticipant(info.identity, info);
1663
1784
  }
1664
- });
1785
+ }
1786
+
1787
+ // Ingest data track publication updates into data tracks infrastructure
1788
+ const mapped = new Map(
1789
+ participantInfos
1790
+ .filter((p) => p.identity !== this.localParticipant.identity)
1791
+ .map((info) => {
1792
+ return [info.identity, info.dataTracks.map((dataTrack) => DataTrackInfo.from(dataTrack))];
1793
+ }),
1794
+ );
1795
+ this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
1665
1796
  };
1666
1797
 
1667
1798
  private handleParticipantDisconnected(identity: string, participant?: RemoteParticipant) {
@@ -1672,6 +1803,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1672
1803
  }
1673
1804
 
1674
1805
  this.incomingDataStreamManager.validateParticipantHasNoActiveDataStreams(identity);
1806
+ this.incomingDataTrackManager.handleRemoteParticipantDisconnected(identity);
1675
1807
 
1676
1808
  participant.trackPublications.forEach((publication) => {
1677
1809
  participant.unpublishTrack(publication.trackSid, true);
@@ -2192,7 +2324,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2192
2324
  track.on(TrackEvent.VideoPlaybackFailed, this.handleVideoPlaybackFailed);
2193
2325
  track.on(TrackEvent.VideoPlaybackStarted, this.handleVideoPlaybackStarted);
2194
2326
  }
2195
- this.emit(RoomEvent.TrackSubscribed, track, publication, participant);
2327
+ this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
2196
2328
  },
2197
2329
  )
2198
2330
  .on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
@@ -2270,7 +2402,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2270
2402
  return acc;
2271
2403
  }, [] as RemoteTrackPublication[]);
2272
2404
  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);
2405
+ const localDataTrackInfos = this.outgoingDataTrackManager.queryPublished();
2406
+ this.engine.sendSyncState(remoteTracks, localTracks, localDataTrackInfos);
2274
2407
  }
2275
2408
 
2276
2409
  /**
@@ -2732,10 +2865,14 @@ export type RoomEventCallbacks = {
2732
2865
  recordingStatusChanged: (recording: boolean) => void;
2733
2866
  participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
2734
2867
  encryptionError: (error: Error, participant?: Participant) => void;
2735
- dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
2868
+ dcBufferStatusChanged: (isLow: boolean, kind: DataChannelKind) => void;
2736
2869
  activeDeviceChanged: (kind: MediaDeviceKind, deviceId: string) => void;
2737
2870
  chatMessage: (message: ChatMessage, participant?: RemoteParticipant | LocalParticipant) => void;
2738
2871
  localTrackSubscribed: (publication: LocalTrackPublication, participant: LocalParticipant) => void;
2739
2872
  metricsReceived: (metrics: MetricsBatch, participant?: Participant) => void;
2740
2873
  participantActive: (participant: Participant) => void;
2874
+ dataTrackPublished: (track: RemoteDataTrack) => void;
2875
+ dataTrackUnpublished: (sid: DataTrackSid) => void;
2876
+ localDataTrackPublished: (track: LocalDataTrack) => void;
2877
+ localDataTrackUnpublished: (sid: DataTrackSid) => void;
2741
2878
  };
@@ -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) {
@@ -1,7 +1,7 @@
1
1
  import { type Throws } from '@livekit/throws-transformer/throws';
2
2
  import { LoggerNames, getLogger } from '../../logger';
3
3
  import { LivekitReasonedError } from '../errors';
4
- import { type DataTrackFrame } from './frame';
4
+ import { type DataTrackFrameInternal } from './frame';
5
5
  import { DataTrackPacket, FrameMarker } from './packet';
6
6
  import { DataTrackExtensions } from './packet/extensions';
7
7
  import { U16_MAX_SIZE, WrapAroundUnsignedInt } from './utils';
@@ -38,9 +38,9 @@ export class DataTrackDepacketizerDropError<
38
38
  this.frameNumber = frameNumber;
39
39
  }
40
40
 
41
- static interrupted(frameNumber: number) {
41
+ static interrupted(frameNumber: number, newFrameNumber: number) {
42
42
  return new DataTrackDepacketizerDropError(
43
- 'Interrupted by the start of a new frame',
43
+ `Interrupted by the start of a new frame ${newFrameNumber}`,
44
44
  DataTrackDepacketizerDropReason.Interrupted,
45
45
  frameNumber,
46
46
  );
@@ -94,12 +94,12 @@ export default class DataTrackDepacketizer {
94
94
  /** Should be repeatedly called with received {@link DataTrackPacket}s - intermediate calls
95
95
  * aggregate the packet's state internally, and return null.
96
96
  *
97
- * Once this method is called with the final packet to form a frame, a new {@link DataTrackFrame}
97
+ * Once this method is called with the final packet to form a frame, a new {@link DataTrackFrameInternal}
98
98
  * is returned.*/
99
99
  push(
100
100
  packet: DataTrackPacket,
101
101
  options?: PushOptions,
102
- ): Throws<DataTrackFrame | null, DataTrackDepacketizerDropError> {
102
+ ): Throws<DataTrackFrameInternal | null, DataTrackDepacketizerDropError> {
103
103
  switch (packet.header.marker) {
104
104
  case FrameMarker.Single:
105
105
  return this.frameFromSingle(packet, options);
@@ -119,7 +119,7 @@ export default class DataTrackDepacketizer {
119
119
  packet: DataTrackPacket,
120
120
  options?: PushOptions,
121
121
  ): Throws<
122
- DataTrackFrame | null,
122
+ DataTrackFrameInternal | null,
123
123
  DataTrackDepacketizerDropError<DataTrackDepacketizerDropReason.Interrupted>
124
124
  > {
125
125
  if (packet.header.marker !== FrameMarker.Single) {
@@ -133,7 +133,10 @@ export default class DataTrackDepacketizer {
133
133
  if (options?.errorOnPartialFrames) {
134
134
  const frameNumber = this.partial.frameNumber;
135
135
  this.reset();
136
- throw DataTrackDepacketizerDropError.interrupted(frameNumber);
136
+ throw DataTrackDepacketizerDropError.interrupted(
137
+ frameNumber,
138
+ packet.header.frameNumber.value,
139
+ );
137
140
  } else {
138
141
  log.warn(
139
142
  `Data track frame ${this.partial.frameNumber} was interrupted by the start of a new frame, dropping.`,
@@ -161,10 +164,13 @@ export default class DataTrackDepacketizer {
161
164
  if (options?.errorOnPartialFrames) {
162
165
  const frameNumber = this.partial.frameNumber;
163
166
  this.reset();
164
- throw DataTrackDepacketizerDropError.interrupted(frameNumber);
167
+ throw DataTrackDepacketizerDropError.interrupted(
168
+ frameNumber,
169
+ packet.header.frameNumber.value,
170
+ );
165
171
  } else {
166
172
  log.warn(
167
- `Data track frame ${this.partial.frameNumber} was interrupted by the start of a new frame, dropping.`,
173
+ `Data track frame ${this.partial.frameNumber} was interrupted by the start of a new frame ${packet.header.frameNumber.value}, dropping.`,
168
174
  );
169
175
  }
170
176
  }
@@ -185,7 +191,7 @@ export default class DataTrackDepacketizer {
185
191
  /** Push to the existing partial frame. */
186
192
  private pushToPartial(
187
193
  packet: DataTrackPacket,
188
- ): Throws<DataTrackFrame | null, DataTrackDepacketizerDropError> {
194
+ ): Throws<DataTrackFrameInternal | null, DataTrackDepacketizerDropError> {
189
195
  if (packet.header.marker !== FrameMarker.Inter && packet.header.marker !== FrameMarker.Final) {
190
196
  // @throws-transformer ignore - this should be treated as a "panic" and not be caught
191
197
  throw new Error(
@@ -201,7 +207,10 @@ export default class DataTrackDepacketizer {
201
207
  if (packet.header.frameNumber.value !== this.partial.frameNumber) {
202
208
  const frameNumber = this.partial.frameNumber;
203
209
  this.reset();
204
- throw DataTrackDepacketizerDropError.interrupted(frameNumber);
210
+ throw DataTrackDepacketizerDropError.interrupted(
211
+ frameNumber,
212
+ packet.header.frameNumber.value,
213
+ );
205
214
  }
206
215
 
207
216
  // NOTE: this check will block reprocessing packets with duplicate sequence values if the
@@ -234,7 +243,7 @@ export default class DataTrackDepacketizer {
234
243
  partial: PartialFrame,
235
244
  endSequence: number,
236
245
  ): Throws<
237
- DataTrackFrame,
246
+ DataTrackFrameInternal,
238
247
  DataTrackDepacketizerDropError<DataTrackDepacketizerDropReason.Incomplete>
239
248
  > {
240
249
  const received = partial.payloads.size;
@@ -1,8 +1,34 @@
1
- import { DataTrackExtensions } from './packet/extensions';
2
- import DataTrackPacketizer from './packetizer';
1
+ import { DataTrackExtensions, DataTrackUserTimestampExtension } from './packet/extensions';
3
2
 
4
3
  /** A pair of payload bytes and packet extensions which can be fed into a {@link DataTrackPacketizer}. */
5
4
  export type DataTrackFrame = {
5
+ payload: Uint8Array;
6
+ userTimestamp?: bigint;
7
+ };
8
+
9
+ /** An internal representation o data track frame which contains all SFU metadata. */
10
+ export type DataTrackFrameInternal = {
6
11
  payload: Uint8Array;
7
12
  extensions: DataTrackExtensions;
8
13
  };
14
+
15
+ export const DataTrackFrameInternal = {
16
+ from(frame: DataTrackFrame) {
17
+ return {
18
+ payload: frame.payload,
19
+ extensions: new DataTrackExtensions({
20
+ userTimestamp: frame.userTimestamp
21
+ ? new DataTrackUserTimestampExtension(frame.userTimestamp)
22
+ : undefined,
23
+ }),
24
+ };
25
+ },
26
+ /** Converts from a DataTrackFrameInternal -> DataTrackFrame. Some internal information is
27
+ * discarded like e2ee encrption extension data. */
28
+ lossyIntoFrame(frame: DataTrackFrameInternal): DataTrackFrame {
29
+ return {
30
+ payload: frame.payload,
31
+ userTimestamp: frame.extensions.userTimestamp?.timestamp,
32
+ };
33
+ },
34
+ };