@livekit/rtc-node 0.13.29 → 0.13.31
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_source.cjs +38 -19
- package/dist/audio_source.cjs.map +1 -1
- package/dist/audio_source.d.cts +12 -4
- package/dist/audio_source.d.ts +12 -4
- package/dist/audio_source.d.ts.map +1 -1
- package/dist/audio_source.js +38 -19
- package/dist/audio_source.js.map +1 -1
- package/dist/audio_stream-B4hGVcKz.d.cts +498 -0
- package/dist/audio_stream-DEG1JKge.d.ts +498 -0
- package/dist/audio_stream.cjs +21 -6
- package/dist/audio_stream.cjs.map +1 -1
- package/dist/audio_stream.d.cts +12 -22
- package/dist/audio_stream.d.ts +12 -22
- package/dist/audio_stream.d.ts.map +1 -1
- package/dist/audio_stream.js +19 -5
- package/dist/audio_stream.js.map +1 -1
- package/dist/data_streams/index.d.cts +1 -1
- package/dist/data_streams/index.d.ts +1 -1
- package/dist/data_streams/stream_reader.d.cts +1 -1
- package/dist/data_streams/stream_reader.d.ts +1 -1
- package/dist/data_streams/stream_writer.d.cts +1 -1
- package/dist/data_streams/stream_writer.d.ts +1 -1
- package/dist/data_streams/types.d.cts +1 -1
- package/dist/data_streams/types.d.ts +1 -1
- package/dist/frame_processor.cjs +6 -0
- package/dist/frame_processor.cjs.map +1 -1
- package/dist/frame_processor.d.cts +2 -0
- package/dist/frame_processor.d.ts +2 -0
- package/dist/frame_processor.d.ts.map +1 -1
- package/dist/frame_processor.js +6 -0
- package/dist/frame_processor.js.map +1 -1
- package/dist/index.d.cts +4 -7
- package/dist/index.d.ts +4 -7
- package/dist/participant.cjs +6 -0
- package/dist/participant.cjs.map +1 -1
- package/dist/participant.d.cts +14 -153
- package/dist/participant.d.ts +14 -153
- package/dist/participant.d.ts.map +1 -1
- package/dist/participant.js +6 -0
- package/dist/participant.js.map +1 -1
- package/dist/room.cjs +28 -0
- package/dist/room.cjs.map +1 -1
- package/dist/room.d.cts +12 -221
- package/dist/room.d.ts +12 -221
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +28 -0
- package/dist/room.js.map +1 -1
- package/dist/track.cjs +123 -0
- package/dist/track.cjs.map +1 -1
- package/dist/track.d.cts +15 -42
- package/dist/track.d.ts +15 -42
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +123 -0
- package/dist/track.js.map +1 -1
- package/dist/track_publication.d.cts +14 -47
- package/dist/track_publication.d.ts +14 -47
- package/dist/{stream_reader-CuG4b8Y-.d.cts → types-_PdPVish.d.cts} +40 -40
- package/dist/{stream_reader-CuG4b8Y-.d.ts → types-_PdPVish.d.ts} +40 -40
- 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/dist/video_stream.d.cts +12 -2
- package/dist/video_stream.d.ts +12 -2
- package/package.json +3 -3
- package/src/audio_source.test.ts +92 -0
- package/src/audio_source.ts +42 -18
- package/src/audio_stream.ts +28 -4
- package/src/audio_stream_room_lifecycle.test.ts +753 -0
- package/src/bun_runtime.test.ts +27 -0
- package/src/frame_processor.ts +4 -0
- package/src/participant.ts +17 -0
- package/src/room.ts +44 -0
- package/src/track.ts +141 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
// Guard: the "Bun" CI job runs vitest via `bun --bun run test` so test bodies
|
|
7
|
+
// execute on the bun runtime (with vitest still providing module mocking). The
|
|
8
|
+
// `--bun` flag is load-bearing — without it, bun honors vitest's
|
|
9
|
+
// `#!/usr/bin/env node` shebang and silently runs everything under node, making
|
|
10
|
+
// the bun job a meaningless duplicate of the node job.
|
|
11
|
+
//
|
|
12
|
+
// When EXPECT_BUN_RUNTIME=1 (set only by the bun CI step), assert that this test
|
|
13
|
+
// body is actually executing under bun. It runs through the exact same vitest
|
|
14
|
+
// path as the real suite, so it catches a regression where the runtime falls
|
|
15
|
+
// back to node. In every other run (the node CI job, local dev) the var is
|
|
16
|
+
// unset and this is skipped.
|
|
17
|
+
describe('bun runtime guard', () => {
|
|
18
|
+
const enforced = process.env.EXPECT_BUN_RUNTIME === '1';
|
|
19
|
+
|
|
20
|
+
it.skipIf(!enforced)('test bodies execute on the bun runtime', () => {
|
|
21
|
+
expect(
|
|
22
|
+
process.versions.bun,
|
|
23
|
+
'EXPECT_BUN_RUNTIME=1 but tests are not running under bun — `bun --bun run test` ' +
|
|
24
|
+
'fell back to node (check the --bun flag / invocation).',
|
|
25
|
+
).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/frame_processor.ts
CHANGED
|
@@ -43,7 +43,11 @@ export abstract class FrameProcessor<Frame extends VideoFrame | AudioFrame> {
|
|
|
43
43
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
44
44
|
onStreamInfoUpdated(_info: FrameProcessorStreamInfo): void {}
|
|
45
45
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
46
|
+
onStreamInfoCleared(): void {}
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
46
48
|
onCredentialsUpdated(_credentials: FrameProcessorCredentials): void {}
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
onCredentialsCleared(): void {}
|
|
47
51
|
|
|
48
52
|
abstract process(frame: Frame): Frame;
|
|
49
53
|
abstract close(): void;
|
package/src/participant.ts
CHANGED
|
@@ -731,6 +731,14 @@ export class LocalParticipant extends Participant {
|
|
|
731
731
|
case 'publication':
|
|
732
732
|
const track_publication = new LocalTrackPublication(cb.message.value!);
|
|
733
733
|
track_publication.track = track;
|
|
734
|
+
// Stamp the server-assigned publication
|
|
735
|
+
// SID onto the track so track.sid == publication.sid. Both
|
|
736
|
+
// Track.pushProcessorMetadataToStream and the localTrackRepublished
|
|
737
|
+
// handler look up the local publication by this SID; without it they
|
|
738
|
+
// depend on createAudioTrack's provisional SID happening to match.
|
|
739
|
+
if (track.info && track_publication.sid) {
|
|
740
|
+
track.info.sid = track_publication.sid;
|
|
741
|
+
}
|
|
734
742
|
this.trackPublications.set(track_publication.sid!, track_publication);
|
|
735
743
|
|
|
736
744
|
return track_publication;
|
|
@@ -767,6 +775,15 @@ export class LocalParticipant extends Participant {
|
|
|
767
775
|
|
|
768
776
|
const pub = this.trackPublications.get(trackSid);
|
|
769
777
|
if (pub) {
|
|
778
|
+
// Clear the processor's room context here too: this path races the
|
|
779
|
+
// localTrackUnpublished room event, and whichever loses finds the
|
|
780
|
+
// publication already gone and skips its own setRoom(null). Calling it
|
|
781
|
+
// from both paths guarantees the processor is cleared (and the
|
|
782
|
+
// tokenRefreshed listener detached); setRoom(null) is idempotent, so a
|
|
783
|
+
// double-clear when this path wins is safe.
|
|
784
|
+
if (pub.track) {
|
|
785
|
+
pub.track.setRoom(null);
|
|
786
|
+
}
|
|
770
787
|
pub.track = undefined;
|
|
771
788
|
}
|
|
772
789
|
this.trackPublications.delete(trackSid);
|
package/src/room.ts
CHANGED
|
@@ -444,6 +444,23 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
444
444
|
}
|
|
445
445
|
this.textStreamControllers.clear();
|
|
446
446
|
|
|
447
|
+
// Detach every track from this room so attached FrameProcessors receive
|
|
448
|
+
// onStreamInfoCleared/onCredentialsCleared and the tokenRefreshed listener
|
|
449
|
+
// is removed. Otherwise a server-initiated disconnect leaves processors with
|
|
450
|
+
// stale room context and the Room holding a strong ref to each Track's bound
|
|
451
|
+
// listener until GC. setRoom(null) is idempotent, so this is safe even if a
|
|
452
|
+
// track was already detached (e.g. via unsubscribe/unpublish).
|
|
453
|
+
if (this.localParticipant) {
|
|
454
|
+
for (const pub of this.localParticipant.trackPublications.values()) {
|
|
455
|
+
pub.track?.setRoom(null);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const participant of this.remoteParticipants.values()) {
|
|
459
|
+
for (const pub of participant.trackPublications.values()) {
|
|
460
|
+
pub.track?.setRoom(null);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
447
464
|
// Clear sidPromise before removing listeners so that a reconnect
|
|
448
465
|
// doesn't return a stale, permanently-pending promise.
|
|
449
466
|
this.sidPromise = undefined;
|
|
@@ -557,11 +574,27 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
557
574
|
}
|
|
558
575
|
} else if (ev.case == 'localTrackPublished') {
|
|
559
576
|
const publication = this.localParticipant.trackPublications.get(ev.value.trackSid!);
|
|
577
|
+
if (publication?.track) {
|
|
578
|
+
publication.track.setRoom(this);
|
|
579
|
+
}
|
|
560
580
|
this.emit(RoomEvent.LocalTrackPublished, publication!, this.localParticipant);
|
|
561
581
|
} else if (ev.case == 'localTrackUnpublished') {
|
|
562
582
|
const publication = this.localParticipant.trackPublications.get(ev.value.publicationSid!);
|
|
583
|
+
const track = publication?.track;
|
|
584
|
+
if (track) {
|
|
585
|
+
track.setRoom(null);
|
|
586
|
+
}
|
|
563
587
|
this.localParticipant.trackPublications.delete(ev.value.publicationSid!);
|
|
588
|
+
// Emit while `publication.track` is still set, preserving the pre-existing
|
|
589
|
+
// payload for callbacks. The handler is synchronous, so nulling the track
|
|
590
|
+
// right after still completes before any other turn can observe it.
|
|
564
591
|
this.emit(RoomEvent.LocalTrackUnpublished, publication!, this.localParticipant!);
|
|
592
|
+
// Mirror trackUnsubscribed: drop the publication's track reference. This
|
|
593
|
+
// also makes unpublishTrack's own setRoom(null) a no-op when it loses the
|
|
594
|
+
// race (its `pub.track` guard short-circuits), avoiding a redundant clear.
|
|
595
|
+
if (track && publication) {
|
|
596
|
+
publication.track = undefined;
|
|
597
|
+
}
|
|
565
598
|
} else if ((ev.case as string) == 'localTrackRepublished') {
|
|
566
599
|
const value = (ev as any).value;
|
|
567
600
|
const previousSid: string = value.previousSid!;
|
|
@@ -571,6 +604,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
571
604
|
publication.updateInfo(newInfo);
|
|
572
605
|
this.localParticipant.trackPublications.delete(previousSid);
|
|
573
606
|
this.localParticipant.trackPublications.set(publication.sid!, publication);
|
|
607
|
+
if (publication.track?.info) {
|
|
608
|
+
// Keep the local-track invariant (track.sid == publication.sid, set at
|
|
609
|
+
// publishTrack) intact across republish, then re-push metadata so any
|
|
610
|
+
// attached FrameProcessor learns the new publication SID / credentials.
|
|
611
|
+
// setRoom with the same room is a no-op for the tokenRefreshed listener
|
|
612
|
+
// but re-fans the metadata to every registered AudioStream.
|
|
613
|
+
publication.track.info.sid = publication.sid;
|
|
614
|
+
publication.track.setRoom(this);
|
|
615
|
+
}
|
|
574
616
|
this.emit(RoomEvent.LocalTrackRepublished, publication, previousSid, this.localParticipant);
|
|
575
617
|
} else {
|
|
576
618
|
log.warn(`RoomEvent.LocalTrackRepublished: previous publication not found: ${previousSid}`);
|
|
@@ -620,6 +662,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
620
662
|
} else if (trackInfo.kind == TrackKind.KIND_AUDIO) {
|
|
621
663
|
publication.track = new RemoteAudioTrack(ownedTrack);
|
|
622
664
|
}
|
|
665
|
+
publication.track?.setRoom(this);
|
|
623
666
|
|
|
624
667
|
this.emit(RoomEvent.TrackSubscribed, publication.track!, publication, participant);
|
|
625
668
|
} catch (e: unknown) {
|
|
@@ -632,6 +675,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
|
|
|
632
675
|
ev.value.trackSid!,
|
|
633
676
|
);
|
|
634
677
|
const track = publication.track!;
|
|
678
|
+
track.setRoom(null);
|
|
635
679
|
publication.track = undefined;
|
|
636
680
|
publication.subscribed = false;
|
|
637
681
|
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
|
package/src/track.ts
CHANGED
|
@@ -11,7 +11,9 @@ import type {
|
|
|
11
11
|
} from '@livekit/rtc-ffi-bindings';
|
|
12
12
|
import { CreateAudioTrackRequest, CreateVideoTrackRequest } from '@livekit/rtc-ffi-bindings';
|
|
13
13
|
import type { AudioSource } from './audio_source.js';
|
|
14
|
+
import type { AudioStreamSource } from './audio_stream.js';
|
|
14
15
|
import { FfiClient, FfiHandle } from './ffi_client.js';
|
|
16
|
+
import type { Room } from './room.js';
|
|
15
17
|
import type { VideoSource } from './video_source.js';
|
|
16
18
|
|
|
17
19
|
export abstract class Track {
|
|
@@ -21,9 +23,148 @@ export abstract class Track {
|
|
|
21
23
|
/** @internal */
|
|
22
24
|
ffi_handle: FfiHandle;
|
|
23
25
|
|
|
26
|
+
private roomRef: WeakRef<Room> | null = null;
|
|
27
|
+
private audioStreams: Set<WeakRef<AudioStreamSource>> = new Set();
|
|
28
|
+
private streamFinalizationRegistry: FinalizationRegistry<WeakRef<AudioStreamSource>>;
|
|
29
|
+
private onRoomTokenRefreshed = () => {
|
|
30
|
+
const room = this.resolveRoom();
|
|
31
|
+
if (!room || !room.token || !room.serverUrl) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const stream of this.iterateStreams()) {
|
|
35
|
+
const processor = stream.processor;
|
|
36
|
+
if (!processor) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
processor.onCredentialsUpdated({ token: room.token, url: room.serverUrl });
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
24
43
|
constructor(owned: OwnedTrack) {
|
|
25
44
|
this.info = owned.info;
|
|
26
45
|
this.ffi_handle = new FfiHandle(owned.handle!.id!);
|
|
46
|
+
this.streamFinalizationRegistry = new FinalizationRegistry<WeakRef<AudioStreamSource>>(
|
|
47
|
+
(ref) => {
|
|
48
|
+
this.audioStreams.delete(ref);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @internal */
|
|
54
|
+
resolveRoom(): Room | null {
|
|
55
|
+
return this.roomRef?.deref() ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @internal */
|
|
59
|
+
setRoom(room: Room | null): void {
|
|
60
|
+
const oldRoom = this.resolveRoom();
|
|
61
|
+
if (!oldRoom && !room) {
|
|
62
|
+
// Already roomless — nothing to detach and nothing to re-clear. Without
|
|
63
|
+
// this guard a second setRoom(null) (e.g. the unpublishTrack /
|
|
64
|
+
// localTrackUnpublished race calling it from both paths) would re-fire
|
|
65
|
+
// onStreamInfoCleared / onCredentialsCleared on every registered processor.
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (oldRoom && oldRoom !== room) {
|
|
69
|
+
oldRoom.off('tokenRefreshed', this.onRoomTokenRefreshed);
|
|
70
|
+
}
|
|
71
|
+
if (room) {
|
|
72
|
+
room.off('tokenRefreshed', this.onRoomTokenRefreshed);
|
|
73
|
+
room.on('tokenRefreshed', this.onRoomTokenRefreshed);
|
|
74
|
+
}
|
|
75
|
+
this.roomRef = room ? new WeakRef(room) : null;
|
|
76
|
+
for (const stream of this.iterateStreams()) {
|
|
77
|
+
this.pushProcessorMetadataToStream(stream, room);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @internal */
|
|
82
|
+
registerAudioStream(stream: AudioStreamSource): void {
|
|
83
|
+
const ref = new WeakRef(stream);
|
|
84
|
+
this.audioStreams.add(ref);
|
|
85
|
+
this.streamFinalizationRegistry.register(stream, ref);
|
|
86
|
+
const room = this.resolveRoom();
|
|
87
|
+
if (room) {
|
|
88
|
+
this.pushProcessorMetadataToStream(stream, room);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @internal */
|
|
93
|
+
unregisterAudioStream(stream: AudioStreamSource): void {
|
|
94
|
+
for (const ref of this.audioStreams) {
|
|
95
|
+
if (ref.deref() === stream) {
|
|
96
|
+
this.audioStreams.delete(ref);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private *iterateStreams(): Generator<AudioStreamSource> {
|
|
103
|
+
const dead: Array<WeakRef<AudioStreamSource>> = [];
|
|
104
|
+
for (const ref of this.audioStreams) {
|
|
105
|
+
const stream = ref.deref();
|
|
106
|
+
if (stream) {
|
|
107
|
+
yield stream;
|
|
108
|
+
} else {
|
|
109
|
+
dead.push(ref);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const ref of dead) {
|
|
113
|
+
this.audioStreams.delete(ref);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private pushProcessorMetadataToStream(stream: AudioStreamSource, room: Room | null): void {
|
|
118
|
+
const processor = stream.processor;
|
|
119
|
+
if (!processor) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!room) {
|
|
124
|
+
// Guard with optional-call: plugins built against an older @livekit/rtc-node
|
|
125
|
+
// inherit a FrameProcessor base class that doesn't define these methods,
|
|
126
|
+
// so they could be undefined on the prototype chain.
|
|
127
|
+
processor.onStreamInfoCleared?.();
|
|
128
|
+
processor.onCredentialsCleared?.();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let identity = '';
|
|
133
|
+
let publicationSid = '';
|
|
134
|
+
const trackSid = this.sid;
|
|
135
|
+
if (trackSid) {
|
|
136
|
+
let found = false;
|
|
137
|
+
for (const participant of room.remoteParticipants.values()) {
|
|
138
|
+
const publication = participant.trackPublications.get(trackSid);
|
|
139
|
+
if (publication) {
|
|
140
|
+
identity = participant.identity;
|
|
141
|
+
publicationSid = publication.sid ?? '';
|
|
142
|
+
found = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!found) {
|
|
147
|
+
const local = room.localParticipant;
|
|
148
|
+
if (local) {
|
|
149
|
+
for (const publication of local.trackPublications.values()) {
|
|
150
|
+
if (publication.sid === trackSid) {
|
|
151
|
+
identity = local.identity;
|
|
152
|
+
publicationSid = publication.sid ?? '';
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
processor.onStreamInfoUpdated({
|
|
161
|
+
roomName: room.name ?? '',
|
|
162
|
+
participantIdentity: identity,
|
|
163
|
+
publicationSid,
|
|
164
|
+
});
|
|
165
|
+
if (room.token && room.serverUrl) {
|
|
166
|
+
processor.onCredentialsUpdated({ token: room.token, url: room.serverUrl });
|
|
167
|
+
}
|
|
27
168
|
}
|
|
28
169
|
|
|
29
170
|
get sid(): string | undefined {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.13.
|
|
1
|
+
export const SDK_VERSION = "0.13.31";
|