livekit-client 2.18.2 → 2.18.4

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 (30) hide show
  1. package/dist/livekit-client.esm.mjs +512 -288
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/room/PCTransport.d.ts.map +1 -1
  6. package/dist/src/room/RTCEngine.d.ts +2 -0
  7. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  8. package/dist/src/room/Room.d.ts +2 -0
  9. package/dist/src/room/Room.d.ts.map +1 -1
  10. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
  11. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  12. package/dist/src/room/participant/RemoteParticipant.d.ts +4 -3
  13. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  14. package/dist/src/room/types.d.ts +1 -1
  15. package/dist/src/room/types.d.ts.map +1 -1
  16. package/dist/ts4.2/room/RTCEngine.d.ts +2 -0
  17. package/dist/ts4.2/room/Room.d.ts +2 -0
  18. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
  19. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +4 -3
  20. package/dist/ts4.2/room/types.d.ts +1 -1
  21. package/package.json +3 -3
  22. package/src/room/PCTransport.ts +4 -3
  23. package/src/room/RTCEngine.ts +19 -0
  24. package/src/room/Room.ts +72 -20
  25. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +331 -16
  26. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +92 -41
  27. package/src/room/participant/RemoteParticipant.ts +14 -2
  28. package/src/room/token-source/utils.ts +3 -3
  29. package/src/room/types.ts +2 -1
  30. package/src/utils/deferrable-map.ts +2 -2
@@ -274,9 +274,8 @@ describe('DataTrackIncomingManager', () => {
274
274
 
275
275
  it('should be unable to subscribe to a non existing data track', async () => {
276
276
  const manager = new IncomingDataTrackManager();
277
- await expect(manager.subscribeRequest('does not exist')).rejects.toThrowError(
278
- 'Cannot subscribe to unknown track',
279
- );
277
+ const [, subscriptionPromise] = manager.openSubscriptionStream('does not exist');
278
+ await expect(subscriptionPromise).rejects.toThrowError('Cannot subscribe to unknown track');
280
279
  });
281
280
 
282
281
  it('should terminate the sfu subscription if the abortsignal is triggered on the only subscription', async () => {
@@ -301,20 +300,34 @@ describe('DataTrackIncomingManager', () => {
301
300
 
302
301
  // 2. Subscribe to a data track
303
302
  const controller = new AbortController();
304
- const subscribeRequestPromise = manager.subscribeRequest(sid, controller.signal);
303
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
304
+ sid,
305
+ controller.signal,
306
+ );
305
307
  await managerEvents.waitFor('sfuUpdateSubscription');
308
+ manager.receivedSfuSubscriberHandles(new Map([[DataTrackHandle.fromNumber(5), sid]]));
309
+
310
+ // 3. Wait for the subscription to be fully established
311
+ await sfuSubscriptionComplete;
312
+
313
+ // 4. Start consuming the readable stream
314
+ const reader = stream.getReader();
315
+ const inFlightReadPromise = reader.read();
306
316
 
307
- // 3. Cancel the subscription
317
+ // 5. Cancel the subscription
308
318
  controller.abort();
309
- await expect(subscribeRequestPromise).rejects.toThrowError(
319
+ await expect(inFlightReadPromise).rejects.toThrowError(
310
320
  'Subscription to data track cancelled by caller',
311
321
  );
312
322
 
313
- // 4. Make sure the underlying sfu subscription is also terminated, since nothing needs it
323
+ // 6. Make sure the underlying sfu subscription is also terminated, since nothing needs it
314
324
  // anymore.
315
325
  const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
316
326
  expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
317
327
  expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(false);
328
+
329
+ // 7. Make sure shutting down the manager doesn't throw errors
330
+ manager.shutdown();
318
331
  });
319
332
 
320
333
  it('should NOT terminate the sfu subscription if the abortsignal is triggered on one of two active subscriptions', async () => {
@@ -339,18 +352,41 @@ describe('DataTrackIncomingManager', () => {
339
352
 
340
353
  // 2. Subscribe to a data track twice
341
354
  const controllerOne = new AbortController();
342
- const subscribeRequestOnePromise = manager.subscribeRequest(sid, controllerOne.signal);
355
+ const [streamOne, sfuSubscriptionOneComplete] = manager.openSubscriptionStream(
356
+ sid,
357
+ controllerOne.signal,
358
+ );
343
359
  await managerEvents.waitFor('sfuUpdateSubscription'); // Subscription started
360
+ manager.receivedSfuSubscriberHandles(new Map([[DataTrackHandle.fromNumber(5), sid]]));
361
+ await sfuSubscriptionOneComplete;
344
362
 
345
363
  const controllerTwo = new AbortController();
346
- manager.subscribeRequest(sid, controllerTwo.signal);
364
+ const [streamTwo, sfuSubscriptionTwoComplete] = manager.openSubscriptionStream(
365
+ sid,
366
+ controllerTwo.signal,
367
+ );
368
+ // NOTE: no new sfu subscription here, the first stream handled setting this up
369
+ await sfuSubscriptionTwoComplete;
347
370
 
348
- // 3. Cancel the first subscription
371
+ // 3. Start consuming the both subscription's readable streams
372
+ const readerOne = streamOne.getReader();
373
+ const inFlightReadOnePromise = readerOne.read();
374
+
375
+ const readerTwo = streamTwo.getReader();
376
+ const inFlightReadTwoPromise = readerTwo.read();
377
+
378
+ // 3. Cancel the first subscription, make sure JUST that subscription is cancelled
349
379
  controllerOne.abort();
350
- await expect(subscribeRequestOnePromise).rejects.toThrowError(
380
+ await expect(inFlightReadOnePromise).rejects.toThrowError(
351
381
  'Subscription to data track cancelled by caller',
352
382
  );
353
383
 
384
+ // 4. Make sure the other subscription is still active / was untouched by the first stream
385
+ // being aborted.
386
+ await expect(
387
+ Promise.race([inFlightReadTwoPromise, Promise.resolve('pending')]),
388
+ ).resolves.toStrictEqual('pending');
389
+
354
390
  // 4. Make sure the underlying sfu subscription has not been also cancelled, there still is
355
391
  // one data track subscription active
356
392
  expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toBe(false);
@@ -375,7 +411,7 @@ describe('DataTrackIncomingManager', () => {
375
411
  );
376
412
 
377
413
  // Subscribe to a data track
378
- const subscribeRequestPromise = manager.subscribeRequest(
414
+ const [, subscribeRequestPromise] = manager.openSubscriptionStream(
379
415
  sid,
380
416
  AbortSignal.abort(/* already aborted */),
381
417
  );
@@ -414,14 +450,14 @@ describe('DataTrackIncomingManager', () => {
414
450
 
415
451
  // 2. Create subscription A
416
452
  const controllerA = new AbortController();
417
- const subscribeAPromise = manager.subscribeRequest(sid, controllerA.signal);
453
+ const [, subscribeAPromise] = manager.openSubscriptionStream(sid, controllerA.signal);
418
454
  const startEvent = await managerEvents.waitFor('sfuUpdateSubscription');
419
455
  expect(startEvent.sid).toStrictEqual(sid);
420
456
  expect(startEvent.subscribe).toStrictEqual(true);
421
457
 
422
458
  // 2. Create subscription B
423
459
  const controllerB = new AbortController();
424
- const subscribeBPromise = manager.subscribeRequest(sid, controllerB.signal);
460
+ const [, subscribeBPromise] = manager.openSubscriptionStream(sid, controllerB.signal);
425
461
  expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toStrictEqual(false);
426
462
 
427
463
  // 3. Cancel the subscription A
@@ -456,13 +492,13 @@ describe('DataTrackIncomingManager', () => {
456
492
  await managerEvents.waitFor('trackPublished');
457
493
 
458
494
  // 2. Begin subscribing to a data track
459
- const promise = manager.subscribeRequest(sid);
495
+ const [, subscriptionCompletePromise] = manager.openSubscriptionStream(sid);
460
496
 
461
497
  // 3. Simulate the remote participant disconnecting
462
498
  manager.handleRemoteParticipantDisconnected(senderIdentity);
463
499
 
464
500
  // 4. Make sure the pending subscribe was terminated
465
- await expect(promise).rejects.toThrowError(
501
+ await expect(subscriptionCompletePromise).rejects.toThrowError(
466
502
  'Cannot subscribe to data track when disconnected',
467
503
  );
468
504
  });
@@ -507,6 +543,285 @@ describe('DataTrackIncomingManager', () => {
507
543
  await reader.closed;
508
544
  });
509
545
 
546
+ it('should terminate ACTIVE sfu subscriptions which have been aborted if the participant disconnects', async () => {
547
+ const manager = new IncomingDataTrackManager();
548
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
549
+ 'sfuUpdateSubscription',
550
+ 'trackPublished',
551
+ ]);
552
+
553
+ const senderIdentity = 'identity';
554
+ const sid = 'data track sid';
555
+ const handle = DataTrackHandle.fromNumber(5);
556
+
557
+ // 1. Make sure the data track publication is registered
558
+ await manager.receiveSfuPublicationUpdates(
559
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
560
+ );
561
+ await managerEvents.waitFor('trackPublished');
562
+
563
+ // 2. Subscribe to a data track, and send the handle back as if the SFU acknowledged it
564
+ const controller = new AbortController();
565
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
566
+ sid,
567
+ controller.signal,
568
+ );
569
+ const reader = stream.getReader();
570
+ const sfuUpdateSubscriptionEvent = await managerEvents.waitFor('sfuUpdateSubscription');
571
+ expect(sfuUpdateSubscriptionEvent.sid).toStrictEqual(sid);
572
+ expect(sfuUpdateSubscriptionEvent.subscribe).toStrictEqual(true);
573
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
574
+
575
+ // 3. Start an in flight stream read
576
+ await sfuSubscriptionComplete;
577
+ const inFlightReadPromise = reader.read();
578
+
579
+ // 4. Abort the abort controller, which should abort the in flight stream read
580
+ controller.abort();
581
+ await expect(inFlightReadPromise).rejects.toThrowError(
582
+ 'Subscription to data track cancelled by caller',
583
+ );
584
+
585
+ // 4. Simulate the remote participant disconnecting
586
+ manager.handleRemoteParticipantDisconnected(senderIdentity);
587
+
588
+ // 5. Make sure the sfu unsubscribes
589
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
590
+ expect(endEvent.sid).toStrictEqual(sid);
591
+ expect(endEvent.subscribe).toStrictEqual(false);
592
+ });
593
+
594
+ it('should terminate ACTIVE sfu subscriptions which have been aborted if the track is unpublished', async () => {
595
+ const manager = new IncomingDataTrackManager();
596
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
597
+ 'sfuUpdateSubscription',
598
+ 'trackPublished',
599
+ 'trackUnpublished',
600
+ ]);
601
+
602
+ const senderIdentity = 'identity';
603
+ const sid = 'data track sid';
604
+ const handle = DataTrackHandle.fromNumber(5);
605
+
606
+ // 1. Make sure the data track publication is registered
607
+ await manager.receiveSfuPublicationUpdates(
608
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
609
+ );
610
+ await managerEvents.waitFor('trackPublished');
611
+
612
+ // 2. Subscribe to a data track, and send the handle back as if the SFU acknowledged it
613
+ const controller = new AbortController();
614
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
615
+ sid,
616
+ controller.signal,
617
+ );
618
+ const reader = stream.getReader();
619
+ await managerEvents.waitFor('sfuUpdateSubscription');
620
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
621
+
622
+ // 3. Start an in flight stream read
623
+ await sfuSubscriptionComplete;
624
+ const inFlightReadPromise = reader.read();
625
+
626
+ // 4. Abort the controller - this errors the stream's underlying controller
627
+ controller.abort();
628
+ await expect(inFlightReadPromise).rejects.toThrowError(
629
+ 'Subscription to data track cancelled by caller',
630
+ );
631
+
632
+ // 5. Unpublish the track - closeStreamControllers must tolerate the already-errored
633
+ // controller (this used to crashing with "Cannot close an errored readable stream")
634
+ await manager.receiveSfuPublicationUpdates(new Map([[senderIdentity, []]]));
635
+
636
+ // 6. Make sure the trackUnpublished event fires
637
+ const trackUnpublishedEvent = await managerEvents.waitFor('trackUnpublished');
638
+ expect(trackUnpublishedEvent.sid).toStrictEqual(sid);
639
+ });
640
+
641
+ it('should not throw when shutting down with an aborted active subscription', async () => {
642
+ const manager = new IncomingDataTrackManager();
643
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
644
+ 'sfuUpdateSubscription',
645
+ 'trackPublished',
646
+ 'trackUnpublished',
647
+ ]);
648
+
649
+ const senderIdentity = 'identity';
650
+ const sid = 'data track sid';
651
+ const handle = DataTrackHandle.fromNumber(5);
652
+
653
+ // 1. Make sure the data track publication is registered
654
+ await manager.receiveSfuPublicationUpdates(
655
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
656
+ );
657
+ await managerEvents.waitFor('trackPublished');
658
+
659
+ // 2. Subscribe to a data track, and send the handle back as if the SFU acknowledged it
660
+ const controller = new AbortController();
661
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
662
+ sid,
663
+ controller.signal,
664
+ );
665
+ await managerEvents.waitFor('sfuUpdateSubscription');
666
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
667
+ await sfuSubscriptionComplete;
668
+
669
+ // 3. Abort the controller to error the stream's underlying controller
670
+ const reader = stream.getReader();
671
+ const inFlightReadPromise = reader.read();
672
+ controller.abort();
673
+ await expect(inFlightReadPromise).rejects.toThrowError(
674
+ 'Subscription to data track cancelled by caller',
675
+ );
676
+
677
+ // 4. Shutdown the manager, and make sure it doesn't throw
678
+ manager.shutdown();
679
+
680
+ // 5. Make sure the trackUnpublished event fires for the descriptor
681
+ const trackUnpublishedEvent = await managerEvents.waitFor('trackUnpublished');
682
+ expect(trackUnpublishedEvent.sid).toStrictEqual(sid);
683
+ });
684
+
685
+ it('should close the remaining active stream when one of two active subscriptions is aborted before disconnect', async () => {
686
+ const manager = new IncomingDataTrackManager();
687
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
688
+ 'sfuUpdateSubscription',
689
+ 'trackPublished',
690
+ ]);
691
+
692
+ const senderIdentity = 'identity';
693
+ const sid = 'data track sid';
694
+ const handle = DataTrackHandle.fromNumber(5);
695
+
696
+ // 1. Make sure the data track publication is registered
697
+ await manager.receiveSfuPublicationUpdates(
698
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
699
+ );
700
+ await managerEvents.waitFor('trackPublished');
701
+
702
+ // 2. Open two subscriptions with separate abort controllers
703
+ const controllerA = new AbortController();
704
+ const [streamA, sfuSubscriptionACompletePromise] = manager.openSubscriptionStream(
705
+ sid,
706
+ controllerA.signal,
707
+ );
708
+ await managerEvents.waitFor('sfuUpdateSubscription');
709
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
710
+ await sfuSubscriptionACompletePromise;
711
+
712
+ const controllerB = new AbortController();
713
+ const [streamB, sfuSubscriptionBCompletePromise] = manager.openSubscriptionStream(
714
+ sid,
715
+ controllerB.signal,
716
+ );
717
+ await sfuSubscriptionBCompletePromise;
718
+
719
+ const readerA = streamA.getReader();
720
+ const readerB = streamB.getReader();
721
+ const inFlightReadAPromise = readerA.read();
722
+
723
+ // 3. Abort only A - this errors A's controller
724
+ controllerA.abort();
725
+ await expect(inFlightReadAPromise).rejects.toThrowError(
726
+ 'Subscription to data track cancelled by caller',
727
+ );
728
+
729
+ // 4. Disconnect the participant. closeStreamControllers must gracefully close B
730
+ // without crashing on A's errored controller (which should already have been removed
731
+ // from the map by onAbort).
732
+ manager.handleRemoteParticipantDisconnected(senderIdentity);
733
+
734
+ // 5. B's reader closes cleanly
735
+ await readerB.closed;
736
+
737
+ // 6. Perform a single unsubscribe event - no double-unsubscribe from A's abort + disconnect
738
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
739
+ expect(endEvent.sid).toStrictEqual(sid);
740
+ expect(endEvent.subscribe).toStrictEqual(false);
741
+ expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toBe(false);
742
+ });
743
+
744
+ it('should error the stream if the descriptor is unpublished between subscribe resolve and post-subscribe setup', async () => {
745
+ const manager = new IncomingDataTrackManager();
746
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
747
+ 'sfuUpdateSubscription',
748
+ 'trackPublished',
749
+ 'trackUnpublished',
750
+ ]);
751
+
752
+ const senderIdentity = 'identity';
753
+ const sid = 'data track sid';
754
+ const handle = DataTrackHandle.fromNumber(5);
755
+
756
+ // 1. Make sure the data track publication is registered
757
+ await manager.receiveSfuPublicationUpdates(
758
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
759
+ );
760
+ await managerEvents.waitFor('trackPublished');
761
+
762
+ // 2. Start subscribing - the .then handler on subscribeRequest is now pending
763
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(sid);
764
+ const reader = stream.getReader();
765
+ await managerEvents.waitFor('sfuUpdateSubscription');
766
+
767
+ // 3. Acknowledge the SFU handle - this synchronously resolves the completionFuture
768
+ // and flips the subscription state to 'active', but the .then microtask has not run yet
769
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
770
+
771
+ // 4. Synchronously unpublish the track before the .then microtask fires
772
+ manager.handleTrackUnpublished(sid);
773
+
774
+ // 5. When .then runs, the descriptor lookup returns undefined and the handler
775
+ // must error the stream and reject sfuSubscriptionComplete (instead of hanging)
776
+ await expect(sfuSubscriptionComplete).rejects.toStrictEqual(
777
+ DataTrackSubscribeError.disconnected(),
778
+ );
779
+ await expect(reader.read()).rejects.toStrictEqual(DataTrackSubscribeError.disconnected());
780
+ });
781
+
782
+ it('should not throw or emit extra events when aborting after a manager-driven close', async () => {
783
+ const manager = new IncomingDataTrackManager();
784
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
785
+ 'sfuUpdateSubscription',
786
+ 'trackPublished',
787
+ ]);
788
+
789
+ const senderIdentity = 'identity';
790
+ const sid = 'data track sid';
791
+ const handle = DataTrackHandle.fromNumber(5);
792
+
793
+ // 1. Make sure the data track publication is registered
794
+ await manager.receiveSfuPublicationUpdates(
795
+ new Map([[senderIdentity, [{ sid, pubHandle: handle, name: 'test', usesE2ee: false }]]]),
796
+ );
797
+ await managerEvents.waitFor('trackPublished');
798
+
799
+ // 2. Subscribe to a data track, and send the handle back as if the SFU acknowledged it
800
+ const controller = new AbortController();
801
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
802
+ sid,
803
+ controller.signal,
804
+ );
805
+ const reader = stream.getReader();
806
+ await managerEvents.waitFor('sfuUpdateSubscription');
807
+ manager.receivedSfuSubscriberHandles(new Map([[handle, sid]]));
808
+ await sfuSubscriptionComplete;
809
+
810
+ // 3. Manager-driven close via disconnect. detachSignal must have run so that the
811
+ // user's AbortSignal no longer triggers onAbort.
812
+ manager.handleRemoteParticipantDisconnected(senderIdentity);
813
+ await reader.closed;
814
+
815
+ // 4. Consume the unsubscribe event
816
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
817
+ expect(endEvent.subscribe).toBe(false);
818
+
819
+ // 5. Aborting after the manager has already closed the stream must be a no-op:
820
+ // no throw, and no additional sfuUpdateSubscription events
821
+ expect(() => controller.abort()).not.toThrow();
822
+ expect(managerEvents.areThereBufferedEvents('sfuUpdateSubscription')).toBe(false);
823
+ });
824
+
510
825
  it('should terminate the sfu subscription once all downstream ReadableStreams are cancelled', async () => {
511
826
  const manager = new IncomingDataTrackManager();
512
827
  const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
@@ -52,7 +52,10 @@ type SubscriptionStateActive = {
52
52
  type: 'active';
53
53
  subcriptionHandle: DataTrackHandle;
54
54
  pipeline: IncomingDataTrackPipeline;
55
- streamControllers: Set<ReadableStreamDefaultController<DataTrackFrame>>;
55
+ /** Map from each downstream ReadableStream's controller to a function that detaches the user's
56
+ * abort signal listener for that stream. Stored together so that whoever ends the stream
57
+ * (consumer cancel, user abort, or manager-driven close) can remove the associated listener. */
58
+ streamControllers: Map<ReadableStreamDefaultController<DataTrackFrame>, () => void>;
56
59
  };
57
60
 
58
61
  type SubscriptionState = SubscriptionStateNone | SubscriptionStatePending | SubscriptionStateActive;
@@ -126,62 +129,93 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
126
129
  let streamController: ReadableStreamDefaultController<DataTrackFrame> | null = null;
127
130
  const sfuSubscriptionComplete = new Future<void, DataTrackSubscribeError>();
128
131
 
132
+ const detachSignal = () => {
133
+ signal?.removeEventListener('abort', onAbort);
134
+ };
135
+
136
+ const cleanup = () => {
137
+ detachSignal();
138
+
139
+ if (!streamController) {
140
+ log.warn(`ReadableStream subscribed to ${sid} was not started.`);
141
+ return;
142
+ }
143
+ const descriptor = this.descriptors.get(sid);
144
+ if (!descriptor) {
145
+ log.warn(`Unknown track ${sid}, skipping cancel...`);
146
+ return;
147
+ }
148
+ if (descriptor.subscription.type !== 'active') {
149
+ log.warn(`Subscription for track ${sid} is not active, skipping cancel...`);
150
+ return;
151
+ }
152
+
153
+ descriptor.subscription.streamControllers.delete(streamController);
154
+
155
+ // If no active stream controllers are left, also unsubscribe on the SFU end.
156
+ if (descriptor.subscription.streamControllers.size === 0) {
157
+ this.unSubscribeRequest(descriptor.info.sid);
158
+ }
159
+ };
160
+
161
+ const onAbort = () => {
162
+ if (!streamController) {
163
+ return;
164
+ }
165
+ const currentDescriptor = this.descriptors.get(sid);
166
+ if (currentDescriptor?.subscription.type === 'active') {
167
+ currentDescriptor.subscription.streamControllers.delete(streamController);
168
+ }
169
+
170
+ streamController.error(DataTrackSubscribeError.cancelled());
171
+ sfuSubscriptionComplete.reject?.(DataTrackSubscribeError.cancelled());
172
+
173
+ cleanup();
174
+ };
175
+
129
176
  const stream = new ReadableStream<DataTrackFrame>(
130
177
  {
131
178
  start: (controller) => {
132
179
  streamController = controller;
133
180
 
134
- const onAbort = () => {
135
- controller.error(DataTrackSubscribeError.cancelled());
136
- sfuSubscriptionComplete.reject?.(DataTrackSubscribeError.cancelled());
137
- };
138
-
139
181
  this.subscribeRequest(sid, signal)
140
182
  .then(async () => {
141
- signal?.addEventListener('abort', onAbort);
142
-
143
183
  const descriptor = this.descriptors.get(sid);
144
184
  if (!descriptor) {
145
185
  log.error(`Unknown track ${sid}`);
186
+ const err = DataTrackSubscribeError.disconnected();
187
+ controller.error(err);
188
+ sfuSubscriptionComplete.reject?.(err);
146
189
  return;
147
190
  }
148
191
  if (descriptor.subscription.type !== 'active') {
149
192
  log.error(`Subscription for track ${sid} is not active`);
193
+ const err = DataTrackSubscribeError.disconnected();
194
+ controller.error(err);
195
+ sfuSubscriptionComplete.reject?.(err);
196
+ return;
197
+ }
198
+
199
+ // Attach the abort signal, aborting immediately if the abort signal was fired while
200
+ // subscribeRequest was in flight.
201
+ if (signal?.aborted) {
202
+ onAbort();
150
203
  return;
151
204
  }
205
+ signal?.addEventListener('abort', onAbort);
152
206
 
153
- descriptor.subscription.streamControllers.add(controller);
207
+ descriptor.subscription.streamControllers.set(controller, detachSignal);
154
208
  sfuSubscriptionComplete.resolve?.();
155
209
  })
156
210
  .catch((err) => {
211
+ // subscribeRequest rejected (cancelled, timed out, disconnected). The signal
212
+ // listener was never attached in this path, so nothing to detach.
157
213
  controller.error(err);
158
214
  sfuSubscriptionComplete.reject?.(err);
159
- })
160
- .finally(() => {
161
- signal?.removeEventListener('abort', onAbort);
162
215
  });
163
216
  },
164
217
  cancel: () => {
165
- if (!streamController) {
166
- log.warn(`ReadableStream subscribed to ${sid} was not started.`);
167
- return;
168
- }
169
- const descriptor = this.descriptors.get(sid);
170
- if (!descriptor) {
171
- log.warn(`Unknown track ${sid}, skipping cancel...`);
172
- return;
173
- }
174
- if (descriptor.subscription.type !== 'active') {
175
- log.warn(`Subscription for track ${sid} is not active, skipping cancel...`);
176
- return;
177
- }
178
-
179
- descriptor.subscription.streamControllers.delete(streamController);
180
-
181
- // If no active stream controllers are left, also unsubscribe on the SFU end.
182
- if (descriptor.subscription.streamControllers.size === 0) {
183
- this.unSubscribeRequest(descriptor.info.sid);
184
- }
218
+ cleanup();
185
219
  },
186
220
  },
187
221
  new CountQueuingStrategy({ highWaterMark: bufferSize }),
@@ -348,9 +382,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
348
382
  return;
349
383
  }
350
384
 
351
- for (const controller of descriptor.subscription.streamControllers) {
352
- controller.close();
353
- }
385
+ this.closeStreamControllers(descriptor.subscription.streamControllers, sid);
354
386
 
355
387
  // FIXME: this might be wrong? Shouldn't this only occur if it is the last subscription to
356
388
  // terminate?
@@ -361,6 +393,27 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
361
393
  this.emit('sfuUpdateSubscription', { sid, subscribe: false });
362
394
  }
363
395
 
396
+ /** Detach abort-signal listeners and close all downstream stream controllers for an active
397
+ * subscription. Used when the subscription is being torn down by the manager (unsubscribe,
398
+ * unpublish, or shutdown). */
399
+ private closeStreamControllers(
400
+ streamControllers: SubscriptionStateActive['streamControllers'],
401
+ sid: DataTrackSid,
402
+ ) {
403
+ for (const [controller, detachSignal] of streamControllers) {
404
+ // Detach before close so we don't leak a listener on the user's AbortSignal.
405
+ detachSignal();
406
+ try {
407
+ controller.close();
408
+ } catch (err) {
409
+ // Defensive: if the controller has already been errored (e.g. by a racing abort whose
410
+ // listener removed itself before we got here), close() throws. There's nothing
411
+ // meaningful to do other than log — the stream is already terminal.
412
+ log.warn(`Failed to close readable stream for track ${sid}: ${err}`);
413
+ }
414
+ }
415
+ }
416
+
364
417
  /** SFU notification that track publications have changed.
365
418
  *
366
419
  * This event is produced from both {@link JoinResponse} and {@link ParticipantUpdate}
@@ -436,9 +489,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
436
489
  this.descriptors.delete(sid);
437
490
 
438
491
  if (descriptor.subscription.type === 'active') {
439
- descriptor.subscription.streamControllers.forEach((controller) => {
440
- controller.close();
441
- });
492
+ this.closeStreamControllers(descriptor.subscription.streamControllers, sid);
442
493
  this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
443
494
  }
444
495
 
@@ -486,7 +537,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
486
537
  type: 'active',
487
538
  subcriptionHandle: assignedHandle,
488
539
  pipeline,
489
- streamControllers: new Set(),
540
+ streamControllers: new Map(),
490
541
  };
491
542
  this.subscriptionHandles.set(assignedHandle, sid);
492
543
 
@@ -529,7 +580,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
529
580
  }
530
581
 
531
582
  // Broadcast to all downstream subscribers
532
- for (const controller of descriptor.subscription.streamControllers) {
583
+ for (const controller of descriptor.subscription.streamControllers.keys()) {
533
584
  if (controller.desiredSize !== null && controller.desiredSize <= 0) {
534
585
  log.warn(
535
586
  `Cannot send frame to subscribers: readable stream is full (desiredSize is ${controller.desiredSize}). To increase this threshold, set a higher 'options.highWaterMark' when calling .subscribe().`,
@@ -588,7 +639,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
588
639
  }
589
640
 
590
641
  if (descriptor.subscription.type === 'active') {
591
- descriptor.subscription.streamControllers.forEach((controller) => controller.close());
642
+ this.closeStreamControllers(descriptor.subscription.streamControllers, descriptor.info.sid);
592
643
  }
593
644
  }
594
645
  this.descriptors.clear();
@@ -6,7 +6,9 @@ import type {
6
6
  } from '@livekit/protocol';
7
7
  import type { SignalClient } from '../../api/SignalClient';
8
8
  import { DeferrableMap } from '../../utils/deferrable-map';
9
- import type RemoteDataTrack from '../data-track/RemoteDataTrack';
9
+ import RemoteDataTrack from '../data-track/RemoteDataTrack';
10
+ import type IncomingDataTrackManager from '../data-track/incoming/IncomingDataTrackManager';
11
+ import { DataTrackInfo } from '../data-track/types';
10
12
  import { ParticipantEvent, TrackEvent } from '../events';
11
13
  import RemoteAudioTrack from '../track/RemoteAudioTrack';
12
14
  import type RemoteTrack from '../track/RemoteTrack';
@@ -48,6 +50,7 @@ export default class RemoteParticipant extends Participant {
48
50
  signalClient: SignalClient,
49
51
  pi: ParticipantInfo,
50
52
  loggerOptions: LoggerOptions,
53
+ manager: IncomingDataTrackManager,
51
54
  ): RemoteParticipant {
52
55
  return new RemoteParticipant(
53
56
  signalClient,
@@ -58,6 +61,10 @@ export default class RemoteParticipant extends Participant {
58
61
  pi.attributes,
59
62
  loggerOptions,
60
63
  pi.kind,
64
+ pi.dataTracks.map((dti) => {
65
+ const info = DataTrackInfo.from(dti);
66
+ return new RemoteDataTrack(info, manager, { publisherIdentity: pi.identity });
67
+ }),
61
68
  );
62
69
  }
63
70
 
@@ -79,13 +86,18 @@ export default class RemoteParticipant extends Participant {
79
86
  attributes?: Record<string, string>,
80
87
  loggerOptions?: LoggerOptions,
81
88
  kind: ParticipantKind = ParticipantKind.STANDARD,
89
+ remoteDataTracks: Array<RemoteDataTrack> = [],
82
90
  ) {
83
91
  super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
84
92
  this.signalClient = signalClient;
85
93
  this.trackPublications = new Map();
86
94
  this.audioTrackPublications = new Map();
87
95
  this.videoTrackPublications = new Map();
88
- this.dataTracks = new DeferrableMap();
96
+ this.dataTracks = new DeferrableMap(
97
+ remoteDataTracks.map((remoteDataTrack) => {
98
+ return [remoteDataTrack.info.name, remoteDataTrack];
99
+ }),
100
+ );
89
101
  this.volumeMap = new Map();
90
102
  }
91
103