@livekit/rtc-node 0.13.25 → 0.13.27
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/audio_mixer.cjs +9 -3
- package/dist/audio_mixer.cjs.map +1 -1
- package/dist/audio_mixer.d.cts +3 -1
- package/dist/audio_mixer.d.ts +3 -1
- package/dist/audio_mixer.d.ts.map +1 -1
- package/dist/audio_mixer.js +9 -3
- package/dist/audio_mixer.js.map +1 -1
- package/dist/ffi_client.cjs +19 -3
- package/dist/ffi_client.cjs.map +1 -1
- package/dist/ffi_client.d.cts +3 -1
- package/dist/ffi_client.d.ts +3 -1
- package/dist/ffi_client.d.ts.map +1 -1
- package/dist/ffi_client.js +19 -3
- package/dist/ffi_client.js.map +1 -1
- package/dist/participant.cjs +108 -63
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +2 -1
- package/dist/participant.d.ts +2 -1
- package/dist/participant.d.ts.map +1 -1
- package/dist/participant.js +108 -63
- package/dist/participant.js.map +1 -1
- package/dist/room.cjs +114 -54
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +7 -1
- package/dist/room.d.ts +7 -1
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +107 -47
- package/dist/room.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
- package/src/audio_mixer.test.ts +102 -0
- package/src/audio_mixer.ts +9 -3
- package/src/ffi_client.ts +26 -3
- package/src/participant.ts +100 -49
- package/src/room.ts +142 -50
- package/src/tests/e2e.test.ts +168 -0
- package/src/version.ts +1 -1
package/src/tests/e2e.test.ts
CHANGED
|
@@ -514,4 +514,172 @@ describeE2E('livekit-rtc e2e', () => {
|
|
|
514
514
|
},
|
|
515
515
|
testTimeoutMs * 2,
|
|
516
516
|
);
|
|
517
|
+
|
|
518
|
+
it(
|
|
519
|
+
'cleans up stream controllers when disconnecting during an active stream',
|
|
520
|
+
async () => {
|
|
521
|
+
const { rooms } = await connectTestRooms(2);
|
|
522
|
+
const [receivingRoom, sendingRoom] = rooms;
|
|
523
|
+
const topic = 'cleanup-stream-topic';
|
|
524
|
+
|
|
525
|
+
// Register a handler on the receiving side that will intentionally
|
|
526
|
+
// NOT fully consume the stream — simulating an abandoned transfer.
|
|
527
|
+
let readerReceived = false;
|
|
528
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
529
|
+
receivingRoom!.registerTextStreamHandler(topic, async (_reader, _sender) => {
|
|
530
|
+
readerReceived = true;
|
|
531
|
+
// Deliberately do not call reader.readAll() so the stream stays open
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Start sending a text stream but don't close it
|
|
535
|
+
const writer = await sendingRoom!.localParticipant!.streamText({ topic });
|
|
536
|
+
await writer.write('partial data');
|
|
537
|
+
|
|
538
|
+
// Wait for the receiving side to get the stream header
|
|
539
|
+
await waitFor(() => readerReceived, {
|
|
540
|
+
timeoutMs: 5000,
|
|
541
|
+
debugName: 'text stream header received',
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Disconnect the receiving room while the stream is still open.
|
|
545
|
+
// This should close the stream controller without throwing.
|
|
546
|
+
await receivingRoom!.disconnect();
|
|
547
|
+
|
|
548
|
+
// Also close the writer and disconnect the sender
|
|
549
|
+
await writer.close();
|
|
550
|
+
await sendingRoom!.disconnect();
|
|
551
|
+
|
|
552
|
+
// If we got here without hanging or throwing, the stream controller
|
|
553
|
+
// was properly cleaned up on disconnect.
|
|
554
|
+
},
|
|
555
|
+
testTimeoutMs,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
it(
|
|
559
|
+
'cleans up track publications when a remote participant disconnects',
|
|
560
|
+
async () => {
|
|
561
|
+
const { rooms } = await connectTestRooms(2);
|
|
562
|
+
const [stayingRoom, leavingRoom] = rooms;
|
|
563
|
+
|
|
564
|
+
// Publish a track from the leaving participant so its track publication
|
|
565
|
+
// will need to be cleaned up on disconnect.
|
|
566
|
+
const source = new AudioSource(48_000, 1);
|
|
567
|
+
const track = LocalAudioTrack.createAudioTrack('cleanup-test', source);
|
|
568
|
+
const options = new TrackPublishOptions();
|
|
569
|
+
options.source = TrackSource.SOURCE_MICROPHONE;
|
|
570
|
+
await leavingRoom!.localParticipant!.publishTrack(track, options);
|
|
571
|
+
|
|
572
|
+
// Wait for the staying room to see the track subscription
|
|
573
|
+
await waitFor(
|
|
574
|
+
() => {
|
|
575
|
+
const remote = stayingRoom!.remoteParticipants.get(
|
|
576
|
+
leavingRoom!.localParticipant!.identity,
|
|
577
|
+
);
|
|
578
|
+
return remote !== undefined && remote.trackPublications.size > 0;
|
|
579
|
+
},
|
|
580
|
+
{ timeoutMs: 5000, debugName: 'track publication visible' },
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Capture a reference to the remote participant before disconnect
|
|
584
|
+
const remoteParticipant = stayingRoom!.remoteParticipants.get(
|
|
585
|
+
leavingRoom!.localParticipant!.identity,
|
|
586
|
+
)!;
|
|
587
|
+
expect(remoteParticipant.trackPublications.size).toBeGreaterThan(0);
|
|
588
|
+
|
|
589
|
+
// Listen for the disconnect event
|
|
590
|
+
const disconnected = waitForRoomEvent(
|
|
591
|
+
stayingRoom!,
|
|
592
|
+
RoomEvent.ParticipantDisconnected,
|
|
593
|
+
testTimeoutMs,
|
|
594
|
+
(p: { identity: string }) => p.identity,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
await leavingRoom!.disconnect();
|
|
598
|
+
await disconnected;
|
|
599
|
+
|
|
600
|
+
// trackUnpublished events fire before participantDisconnected, so
|
|
601
|
+
// by this point all publications should already be removed and disposed.
|
|
602
|
+
expect(remoteParticipant.trackPublications.size).toBe(0);
|
|
603
|
+
expect(stayingRoom!.remoteParticipants.has(remoteParticipant.identity)).toBe(false);
|
|
604
|
+
|
|
605
|
+
await source.close();
|
|
606
|
+
await stayingRoom!.disconnect();
|
|
607
|
+
},
|
|
608
|
+
testTimeoutMs,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
it(
|
|
612
|
+
'cleans up resources when multiple participants disconnect simultaneously',
|
|
613
|
+
async () => {
|
|
614
|
+
// Connect 4 participants to stress-test concurrent disconnection cleanup
|
|
615
|
+
const { rooms } = await connectTestRooms(4);
|
|
616
|
+
|
|
617
|
+
// Publish a track from each participant to create track publications
|
|
618
|
+
const sources: AudioSource[] = [];
|
|
619
|
+
for (const room of rooms) {
|
|
620
|
+
const source = new AudioSource(48_000, 1);
|
|
621
|
+
sources.push(source);
|
|
622
|
+
const track = LocalAudioTrack.createAudioTrack('multi-cleanup', source);
|
|
623
|
+
const options = new TrackPublishOptions();
|
|
624
|
+
options.source = TrackSource.SOURCE_MICROPHONE;
|
|
625
|
+
await room.localParticipant!.publishTrack(track, options);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Wait for all participants to see each other's tracks
|
|
629
|
+
await waitFor(
|
|
630
|
+
() =>
|
|
631
|
+
rooms.every(
|
|
632
|
+
(r) =>
|
|
633
|
+
r.remoteParticipants.size === 3 &&
|
|
634
|
+
[...r.remoteParticipants.values()].every((p) => p.trackPublications.size > 0),
|
|
635
|
+
),
|
|
636
|
+
{ timeoutMs: 5000, debugName: 'all tracks visible' },
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
// Register listeners before disconnecting so we can verify both
|
|
640
|
+
// RoomEvent.Disconnected and RoomEvent.ConnectionStateChanged fire
|
|
641
|
+
// for every room, even when disconnects race.
|
|
642
|
+
const disconnectedEvents = rooms.map((r) =>
|
|
643
|
+
waitForRoomEvent(r, RoomEvent.Disconnected, 3_000, (reason) => reason),
|
|
644
|
+
);
|
|
645
|
+
const connectionStateEvents = rooms.map((r) =>
|
|
646
|
+
waitForRoomEvent(r, RoomEvent.ConnectionStateChanged, 3_000, (state) => state),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Disconnect all participants simultaneously
|
|
650
|
+
await Promise.all([...rooms.map((r) => r.disconnect()), ...sources.map((s) => s.close())]);
|
|
651
|
+
|
|
652
|
+
await Promise.all(disconnectedEvents);
|
|
653
|
+
const observedStates = await Promise.all(connectionStateEvents);
|
|
654
|
+
|
|
655
|
+
// Verify all rooms are disconnected and remote participant maps are empty
|
|
656
|
+
for (const room of rooms) {
|
|
657
|
+
expect(room.isConnected).toBe(false);
|
|
658
|
+
}
|
|
659
|
+
for (const state of observedStates) {
|
|
660
|
+
expect(state).toBe(ConnectionState.CONN_DISCONNECTED);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
testTimeoutMs * 2,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
it(
|
|
667
|
+
'concurrent getSid() calls share a single listener and resolve consistently',
|
|
668
|
+
async () => {
|
|
669
|
+
const { rooms } = await connectTestRooms(1);
|
|
670
|
+
const room = rooms[0]!;
|
|
671
|
+
|
|
672
|
+
// Fire multiple concurrent getSid() calls — they should all resolve
|
|
673
|
+
// to the same SID without leaking event listeners.
|
|
674
|
+
const results = await Promise.all([room.getSid(), room.getSid(), room.getSid()]);
|
|
675
|
+
|
|
676
|
+
// All calls should return the same non-empty SID
|
|
677
|
+
expect(results[0]).toBeTruthy();
|
|
678
|
+
expect(results[1]).toBe(results[0]);
|
|
679
|
+
expect(results[2]).toBe(results[0]);
|
|
680
|
+
|
|
681
|
+
await room.disconnect();
|
|
682
|
+
},
|
|
683
|
+
testTimeoutMs,
|
|
684
|
+
);
|
|
517
685
|
});
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.13.
|
|
1
|
+
export const SDK_VERSION = "0.13.27";
|