livekit-client 2.18.6 → 2.18.8

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 (69) 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 +2 -2
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +399 -259
  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.map +1 -1
  10. package/dist/src/logger.d.ts +11 -1
  11. package/dist/src/logger.d.ts.map +1 -1
  12. package/dist/src/room/PCTransport.d.ts +13 -3
  13. package/dist/src/room/PCTransport.d.ts.map +1 -1
  14. package/dist/src/room/PCTransportManager.d.ts +3 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/data-track/LocalDataTrack.d.ts +31 -0
  19. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  20. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  21. package/dist/src/room/data-track/handle.d.ts +1 -0
  22. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  23. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  24. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  25. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  26. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  27. package/dist/src/room/data-track/outgoing/types.d.ts +6 -0
  28. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  29. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  31. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/processor/types.d.ts +2 -0
  35. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  36. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
  37. package/dist/ts4.2/logger.d.ts +11 -1
  38. package/dist/ts4.2/room/PCTransport.d.ts +13 -3
  39. package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
  40. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +31 -0
  41. package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
  42. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  43. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  44. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +6 -0
  45. package/dist/ts4.2/room/track/processor/types.d.ts +2 -0
  46. package/package.json +1 -1
  47. package/src/api/SignalClient.ts +19 -31
  48. package/src/logger.test.ts +61 -0
  49. package/src/logger.ts +38 -4
  50. package/src/room/PCTransport.ts +26 -3
  51. package/src/room/PCTransportManager.test.ts +281 -0
  52. package/src/room/PCTransportManager.ts +45 -31
  53. package/src/room/RTCEngine.ts +34 -52
  54. package/src/room/Room.ts +37 -59
  55. package/src/room/data-track/LocalDataTrack.ts +51 -0
  56. package/src/room/data-track/RemoteDataTrack.ts +4 -1
  57. package/src/room/data-track/handle.ts +4 -0
  58. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
  59. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
  60. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +235 -1
  61. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +45 -3
  62. package/src/room/data-track/outgoing/types.ts +5 -0
  63. package/src/room/participant/LocalParticipant.ts +59 -144
  64. package/src/room/participant/Participant.ts +4 -1
  65. package/src/room/participant/publishUtils.ts +2 -2
  66. package/src/room/track/LocalAudioTrack.ts +1 -0
  67. package/src/room/track/LocalTrack.ts +2 -0
  68. package/src/room/track/processor/types.ts +2 -0
  69. package/src/utils/subscribeToEvents.ts +11 -8
@@ -586,7 +586,7 @@ describe('DataTrackOutgoingManager', () => {
586
586
  ]);
587
587
 
588
588
  // Shut down the manager
589
- const shutdownPromise = manager.shutdown();
589
+ const shutdownPromise = manager.reset();
590
590
 
591
591
  // The pending data track should be cancelled
592
592
  await expect(pendingDescriptor.completionFuture.promise).rejects.toThrowError(
@@ -602,4 +602,238 @@ describe('DataTrackOutgoingManager', () => {
602
602
 
603
603
  await shutdownPromise;
604
604
  });
605
+
606
+ it('should resolve flush() after a single tryPush once the packet is acknowledged', async () => {
607
+ const pubHandle = 5;
608
+ const manager = OutgoingDataTrackManager.withDescriptors(
609
+ new Map([
610
+ [
611
+ DataTrackHandle.fromNumber(pubHandle),
612
+ Descriptor.active(
613
+ {
614
+ sid: 'bogus-sid',
615
+ pubHandle,
616
+ name: 'test',
617
+ usesE2ee: false,
618
+ },
619
+ null,
620
+ ),
621
+ ],
622
+ ]),
623
+ );
624
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
625
+ 'packetAvailable',
626
+ 'packetsFlushed',
627
+ ]);
628
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
629
+ { name: 'track name' },
630
+ manager,
631
+ pubHandle,
632
+ );
633
+
634
+ // 1. Push a single-packet payload
635
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]) });
636
+
637
+ // 2. The packet should have been emitted to be sent over the data channel
638
+ const packetEvent = await managerEvents.waitFor('packetAvailable');
639
+ expect(packetEvent.handle).toStrictEqual(pubHandle);
640
+
641
+ // 3. Calling flush() right after tryPush() should not resolve until the packet
642
+ // is acknowledged via handlePacketSendComplete
643
+ let flushed = false;
644
+ const flushPromise = localDataTrack.flush().then(() => {
645
+ flushed = true;
646
+ });
647
+
648
+ await new Promise((resolve) => setTimeout(resolve, 0));
649
+ expect(flushed).toStrictEqual(false);
650
+ expect(managerEvents.areThereBufferedEvents('packetsFlushed')).toBe(false);
651
+
652
+ // 4. Acknowledge that the packet has been sent over the data channel
653
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
654
+
655
+ // 5. The packetsFlushed event fires once the in-flight packet counter reaches 0
656
+ const flushedEvent = await managerEvents.waitFor('packetsFlushed');
657
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
658
+
659
+ // 6. The flush() promise resolves
660
+ await flushPromise;
661
+ expect(flushed).toStrictEqual(true);
662
+ });
663
+
664
+ it('should resolve flush() only after all packets in a multi-packet payload are acknowledged', async () => {
665
+ const pubHandle = 5;
666
+ const manager = OutgoingDataTrackManager.withDescriptors(
667
+ new Map([
668
+ [
669
+ DataTrackHandle.fromNumber(pubHandle),
670
+ Descriptor.active(
671
+ {
672
+ sid: 'bogus-sid',
673
+ pubHandle,
674
+ name: 'test',
675
+ usesE2ee: false,
676
+ },
677
+ null,
678
+ ),
679
+ ],
680
+ ]),
681
+ );
682
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
683
+ 'packetAvailable',
684
+ 'packetsFlushed',
685
+ ]);
686
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
687
+ { name: 'track name' },
688
+ manager,
689
+ pubHandle,
690
+ );
691
+
692
+ // 1. Push a payload large enough to span multiple packets (24k > single packet mtu)
693
+ await localDataTrack.tryPush({ payload: new Uint8Array(24_000).fill(0xbe) });
694
+
695
+ // 2. Two packetAvailable events should be emitted for this payload
696
+ await managerEvents.waitFor('packetAvailable');
697
+ await managerEvents.waitFor('packetAvailable');
698
+
699
+ // 3. Call flush() before any of the packets have been acknowledged
700
+ let flushed = false;
701
+ const flushPromise = localDataTrack.flush().then(() => {
702
+ flushed = true;
703
+ });
704
+
705
+ // 4. Acknowledge the first packet -- flush should not resolve yet, in-flight counter still > 0
706
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
707
+ await new Promise((resolve) => setTimeout(resolve, 0));
708
+ expect(flushed).toStrictEqual(false);
709
+ expect(managerEvents.areThereBufferedEvents('packetsFlushed')).toBe(false);
710
+
711
+ // 5. Acknowledge the second packet -- flush resolves once the counter reaches 0
712
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
713
+
714
+ const flushedEvent = await managerEvents.waitFor('packetsFlushed');
715
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
716
+
717
+ await flushPromise;
718
+ expect(flushed).toStrictEqual(true);
719
+ });
720
+
721
+ it('should resolve any pending flush() calls when the manager is reset', async () => {
722
+ const pubHandle = 5;
723
+ const manager = OutgoingDataTrackManager.withDescriptors(
724
+ new Map([
725
+ [
726
+ DataTrackHandle.fromNumber(pubHandle),
727
+ Descriptor.active(
728
+ {
729
+ sid: 'bogus-sid',
730
+ pubHandle,
731
+ name: 'test',
732
+ usesE2ee: false,
733
+ },
734
+ null,
735
+ ),
736
+ ],
737
+ ]),
738
+ );
739
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
740
+ 'packetAvailable',
741
+ 'packetsFlushed',
742
+ 'reset',
743
+ ]);
744
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
745
+ { name: 'track name' },
746
+ manager,
747
+ pubHandle,
748
+ );
749
+
750
+ // 1. Push a single-packet payload
751
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]) });
752
+ await managerEvents.waitFor('packetAvailable');
753
+
754
+ // 2. Call flush() before the in-flight packet is acknowledged -- it should remain
755
+ // pending because the in-flight counter is still > 0
756
+ let flushed = false;
757
+ const flushPromise = localDataTrack.flush().then(() => {
758
+ flushed = true;
759
+ });
760
+
761
+ await new Promise((resolve) => setTimeout(resolve, 0));
762
+ expect(flushed).toStrictEqual(false);
763
+ expect(managerEvents.areThereBufferedEvents('packetsFlushed')).toBe(false);
764
+
765
+ // 3. Reset the manager. This simulates a RTCEngine disconnect and should resolve
766
+ // the pending flush() even though the packet was never acknowledged.
767
+ await manager.reset();
768
+ await managerEvents.waitFor('reset');
769
+
770
+ // 4. The flush() promise resolves
771
+ await flushPromise;
772
+ expect(flushed).toStrictEqual(true);
773
+
774
+ // 5. No packetsFlushed event was emitted -- reset short-circuits the flush directly
775
+ // on the LocalDataTrack rather than going through the in-flight counter.
776
+ expect(managerEvents.areThereBufferedEvents('packetsFlushed')).toBe(false);
777
+ });
778
+
779
+ it('should resolve flush() at the end of a batch of tryPush calls once all packets are acknowledged', async () => {
780
+ const pubHandle = 5;
781
+ const manager = OutgoingDataTrackManager.withDescriptors(
782
+ new Map([
783
+ [
784
+ DataTrackHandle.fromNumber(pubHandle),
785
+ Descriptor.active(
786
+ {
787
+ sid: 'bogus-sid',
788
+ pubHandle,
789
+ name: 'test',
790
+ usesE2ee: false,
791
+ },
792
+ null,
793
+ ),
794
+ ],
795
+ ]),
796
+ );
797
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
798
+ 'packetAvailable',
799
+ 'packetsFlushed',
800
+ ]);
801
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
802
+ { name: 'track name' },
803
+ manager,
804
+ pubHandle,
805
+ );
806
+
807
+ // 1. Run a batch of tryPush calls
808
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01]) });
809
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x02]) });
810
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x03]) });
811
+
812
+ // 2. Three packetAvailable events should be emitted, one per pushed frame
813
+ await managerEvents.waitFor('packetAvailable');
814
+ await managerEvents.waitFor('packetAvailable');
815
+ await managerEvents.waitFor('packetAvailable');
816
+
817
+ // 3. After the batch is enqueued, call flush() to wait for the SFU to drain them
818
+ let flushed = false;
819
+ const flushPromise = localDataTrack.flush().then(() => {
820
+ flushed = true;
821
+ });
822
+
823
+ // 4. Acknowledge two of the three packets -- flush should not resolve yet
824
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
825
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
826
+ await new Promise((resolve) => setTimeout(resolve, 0));
827
+ expect(flushed).toStrictEqual(false);
828
+ expect(managerEvents.areThereBufferedEvents('packetsFlushed')).toBe(false);
829
+
830
+ // 5. Acknowledge the last packet -- flush resolves once the counter reaches 0
831
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
832
+
833
+ const flushedEvent = await managerEvents.waitFor('packetsFlushed');
834
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
835
+
836
+ await flushPromise;
837
+ expect(flushed).toStrictEqual(true);
838
+ });
605
839
  });
@@ -18,6 +18,7 @@ import DataTrackOutgoingPipeline from './pipeline';
18
18
  import {
19
19
  type DataTrackOptions,
20
20
  type EventPacketAvailable,
21
+ type EventPacketsFlushed,
21
22
  type EventSfuPublishRequest,
22
23
  type EventSfuUnpublishRequest,
23
24
  type EventTrackPublished,
@@ -74,6 +75,11 @@ export type DataTrackOutgoingManagerCallbacks = {
74
75
  trackPublished: (event: EventTrackPublished) => void;
75
76
  /** A {@link LocalDataTrack} has been unpublished */
76
77
  trackUnpublished: (event: EventTrackUnpublished) => void;
78
+ /** A {@link LocalDataTrack} has had all of its in flight packets sent via the rtc data channel. */
79
+ packetsFlushed: (event: EventPacketsFlushed) => void;
80
+ /** The manager has been reset and all state has been cleared in preparation for the next room
81
+ * connection. */
82
+ reset: () => void;
77
83
  };
78
84
 
79
85
  type OutgoingDataTrackManagerOptions = {
@@ -95,6 +101,11 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
95
101
 
96
102
  private descriptors = new Map<DataTrackHandle, Descriptor>();
97
103
 
104
+ /** Number of packets for each data track which have been emitted via the `packetAvailable` event
105
+ * and which have not yet been sent via the rtc data channel yet. Once this goes to 0, then
106
+ * all in flight packets have been delivered, and the data tracks is "flushed". */
107
+ private inFlightPacketCounter = new Map<DataTrackHandle, number>();
108
+
98
109
  constructor(options?: OutgoingDataTrackManagerOptions) {
99
110
  super();
100
111
  this.e2eeManager = options?.e2eeManager ?? null;
@@ -154,7 +165,9 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
154
165
 
155
166
  try {
156
167
  for await (const packet of descriptor.pipeline.processFrame(frame)) {
157
- this.emit('packetAvailable', { bytes: packet.toBinary() });
168
+ const prev = this.inFlightPacketCounter.get(handle) ?? 0;
169
+ this.inFlightPacketCounter.set(handle, prev + 1);
170
+ this.emit('packetAvailable', { handle, bytes: packet.toBinary() });
158
171
  }
159
172
  } catch (err) {
160
173
  // NOTE: In the rust implementation this "dropped" error means something different (not enough room
@@ -163,6 +176,27 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
163
176
  }
164
177
  }
165
178
 
179
+ /** The client has sent a packet over the rtc data channel. This signal is used for determining
180
+ * once all packets are sent and a data track has been "flushed".
181
+ *
182
+ * @internal */
183
+ handlePacketSendComplete(handle: DataTrackHandle) {
184
+ const prev = this.inFlightPacketCounter.get(handle) ?? 0;
185
+ let counter = prev - 1;
186
+
187
+ if (counter < 0) {
188
+ log.warn(
189
+ `OutgoingDataTrackManager.handlePacketSendComplete: inFlightPacketCounter was decremented below 0 (got ${this.inFlightPacketCounter} - resetting to 0. Were more packets send than were emitted?`,
190
+ );
191
+ counter = 0;
192
+ }
193
+ this.inFlightPacketCounter.set(handle, counter);
194
+
195
+ if (counter === 0) {
196
+ this.emit('packetsFlushed', { handle });
197
+ }
198
+ }
199
+
166
200
  /**
167
201
  * Client requested to publish a track.
168
202
  *
@@ -266,6 +300,7 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
266
300
 
267
301
  await descriptor.unpublishingFuture.promise;
268
302
 
303
+ this.inFlightPacketCounter.delete(handle);
269
304
  this.emit('trackUnpublished', { sid: descriptor.info.sid });
270
305
  }
271
306
 
@@ -360,10 +395,13 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
360
395
  }
361
396
 
362
397
  /**
363
- * Shuts down the manager and all associated tracks.
398
+ * Reset's the state of the manager and all associated tracks. Run on room disconnect to get
399
+ * the manager ready for the next room connection.
364
400
  * @internal
365
401
  **/
366
- async shutdown() {
402
+ async reset() {
403
+ this.handleAllocator.reset();
404
+
367
405
  for (const descriptor of this.descriptors.values()) {
368
406
  switch (descriptor.type) {
369
407
  case 'pending':
@@ -379,5 +417,9 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
379
417
  }
380
418
  }
381
419
  this.descriptors.clear();
420
+
421
+ this.inFlightPacketCounter.clear();
422
+
423
+ this.emit('reset');
382
424
  }
383
425
  }
@@ -34,6 +34,8 @@ export type EventSfuUnpublishRequest = {
34
34
 
35
35
  /** A serialized packet is ready to be sent over the transport. */
36
36
  export type EventPacketAvailable = {
37
+ /** The handle associated with the data track which this packet bytes belong to. */
38
+ handle: DataTrackHandle;
37
39
  bytes: Uint8Array;
38
40
  };
39
41
 
@@ -43,3 +45,6 @@ export type EventTrackPublished = { track: LocalDataTrack };
43
45
 
44
46
  /** A track has been unpublished by a remote participant and can no longer be subscribed to. */
45
47
  export type EventTrackUnpublished = { sid: DataTrackSid };
48
+
49
+ /** A track has had all of its in flight packets sent via the rtc data channel. */
50
+ export type EventPacketsFlushed = { handle: DataTrackHandle };