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.
- package/dist/livekit-client.esm.mjs +512 -288
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +2 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +2 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +4 -3
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -1
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +2 -0
- package/dist/ts4.2/room/Room.d.ts +2 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +4 -3
- package/dist/ts4.2/room/types.d.ts +1 -1
- package/package.json +3 -3
- package/src/room/PCTransport.ts +4 -3
- package/src/room/RTCEngine.ts +19 -0
- package/src/room/Room.ts +72 -20
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +331 -16
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +92 -41
- package/src/room/participant/RemoteParticipant.ts +14 -2
- package/src/room/token-source/utils.ts +3 -3
- package/src/room/types.ts +2 -1
- 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
|
-
|
|
278
|
-
|
|
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
|
|
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
|
-
//
|
|
317
|
+
// 5. Cancel the subscription
|
|
308
318
|
controller.abort();
|
|
309
|
-
await expect(
|
|
319
|
+
await expect(inFlightReadPromise).rejects.toThrowError(
|
|
310
320
|
'Subscription to data track cancelled by caller',
|
|
311
321
|
);
|
|
312
322
|
|
|
313
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|